From 9d7fd6d0361a2bf024c0756b56e964d83a834352 Mon Sep 17 00:00:00 2001 From: Graham TerMarsch Date: Wed, 30 Dec 2020 21:46:58 -0800 Subject: [PATCH 1/3] Release - v0.13 --- Changes | 80 +++ LICENSE | 379 +++++++++++++ MANIFEST | 66 +++ META.json | 92 ++++ META.yml | 32 ++ Makefile.PL | 50 ++ README | 102 ++++ XS.xs | 742 ++++++++++++++++++++++++++ cpanfile | 8 + lib/JavaScript/Minifier/XS.pm | 129 +++++ t/00-report-prereqs.dd | 55 ++ t/00-report-prereqs.t | 193 +++++++ t/01-loads.t | 5 + t/02-minify.t | 37 ++ t/03-minifies-to-nothing.t | 20 + t/04-not-javascript.t | 16 + t/author-benchmark.t | 63 +++ t/author-clean-namespaces.t | 19 + t/author-compile.t | 71 +++ t/author-distmeta.t | 14 + t/author-eof.t | 19 + t/author-eol.t | 28 + t/author-leaks.t | 32 ++ t/author-minimum-version.t | 14 + t/author-no-breakpoints.t | 19 + t/author-no-tabs.t | 28 + t/author-pod-coverage.t | 15 + t/author-pod-spell.t | 27 + t/author-pod-syntax.t | 15 + t/author-synopsis.t | 13 + t/author-test-compile.t | 75 +++ t/js/comments-before-a-regex.js | 18 + t/js/comments-before-a-regex.min | 1 + t/js/comments-ie-conditional.js | 7 + t/js/comments-ie-conditional.min | 2 + t/js/comments.js | 17 + t/js/comments.min | 3 + t/js/division-of-array-subscripts.js | 10 + t/js/division-of-array-subscripts.min | 2 + t/js/division.js | 2 + t/js/division.min | 1 + t/js/es6-templates.js | 10 + t/js/es6-templates.min | 7 + t/js/leading-whitespace.js | 4 + t/js/leading-whitespace.min | 1 + t/js/literals-double-quotes.js | 2 + t/js/literals-double-quotes.min | 1 + t/js/literals-regexp.js | 2 + t/js/literals-regexp.min | 1 + t/js/literals-single-quotes.js | 2 + t/js/literals-single-quotes.min | 1 + t/js/postfix-sigil.js | 8 + t/js/postfix-sigil.min | 2 + t/js/prefix-sigil.js | 4 + t/js/prefix-sigil.min | 1 + t/js/regexp-not-line-comment.js | 4 + t/js/regexp-not-line-comment.min | 1 + t/js/return-regex.js | 4 + t/js/return-regex.min | 1 + t/js/simple.js | 3 + t/js/simple.min | 1 + t/js/trailing-whitespace.js | 4 + t/js/trailing-whitespace.min | 1 + t/release-kwalitee.t | 17 + t/release-unused-vars.t | 22 + 65 files changed, 2625 insertions(+) create mode 100644 Changes create mode 100644 LICENSE create mode 100644 MANIFEST create mode 100644 META.json create mode 100644 META.yml create mode 100644 Makefile.PL create mode 100644 README create mode 100644 XS.xs create mode 100644 cpanfile create mode 100644 lib/JavaScript/Minifier/XS.pm create mode 100644 t/00-report-prereqs.dd create mode 100644 t/00-report-prereqs.t create mode 100644 t/01-loads.t create mode 100644 t/02-minify.t create mode 100644 t/03-minifies-to-nothing.t create mode 100644 t/04-not-javascript.t create mode 100644 t/author-benchmark.t create mode 100644 t/author-clean-namespaces.t create mode 100644 t/author-compile.t create mode 100644 t/author-distmeta.t create mode 100644 t/author-eof.t create mode 100644 t/author-eol.t create mode 100644 t/author-leaks.t create mode 100644 t/author-minimum-version.t create mode 100644 t/author-no-breakpoints.t create mode 100644 t/author-no-tabs.t create mode 100644 t/author-pod-coverage.t create mode 100644 t/author-pod-spell.t create mode 100644 t/author-pod-syntax.t create mode 100644 t/author-synopsis.t create mode 100644 t/author-test-compile.t create mode 100644 t/js/comments-before-a-regex.js create mode 100644 t/js/comments-before-a-regex.min create mode 100644 t/js/comments-ie-conditional.js create mode 100644 t/js/comments-ie-conditional.min create mode 100644 t/js/comments.js create mode 100644 t/js/comments.min create mode 100644 t/js/division-of-array-subscripts.js create mode 100644 t/js/division-of-array-subscripts.min create mode 100644 t/js/division.js create mode 100644 t/js/division.min create mode 100644 t/js/es6-templates.js create mode 100644 t/js/es6-templates.min create mode 100644 t/js/leading-whitespace.js create mode 100644 t/js/leading-whitespace.min create mode 100644 t/js/literals-double-quotes.js create mode 100644 t/js/literals-double-quotes.min create mode 100644 t/js/literals-regexp.js create mode 100644 t/js/literals-regexp.min create mode 100644 t/js/literals-single-quotes.js create mode 100644 t/js/literals-single-quotes.min create mode 100644 t/js/postfix-sigil.js create mode 100644 t/js/postfix-sigil.min create mode 100644 t/js/prefix-sigil.js create mode 100644 t/js/prefix-sigil.min create mode 100644 t/js/regexp-not-line-comment.js create mode 100644 t/js/regexp-not-line-comment.min create mode 100644 t/js/return-regex.js create mode 100644 t/js/return-regex.min create mode 100644 t/js/simple.js create mode 100644 t/js/simple.min create mode 100644 t/js/trailing-whitespace.js create mode 100644 t/js/trailing-whitespace.min create mode 100644 t/release-kwalitee.t create mode 100644 t/release-unused-vars.t diff --git a/Changes b/Changes new file mode 100644 index 0000000..b848fd2 --- /dev/null +++ b/Changes @@ -0,0 +1,80 @@ +Revision history for Perl extension JavaScript::Minifier::XS. + +0.13 2020-12-30 21:46:29-08:00 America/Vancouver + - POD cleanups; spelling, SYNOPSIS + - Switch to DZil Author Bundle + +0.12 2020-12-28 08:31:31-08:00 America/Vancouver + - Switch to GitHub Actions, from Travis-CI. + - Add META links to GitHub repository and issue tracker + - Switch to Dist::Zilla + - Bump minimum required Perl to 5.8.1 + - RT #130347; handle ES6 template literals. + Thanks to Robert Rothenberg. + +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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c9328d6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,379 @@ +This software is copyright (c) 2020 by Graham TerMarsch. + +This is free software; you can redistribute it and/or modify it under +the same terms as the Perl 5 programming language system itself. + +Terms of the Perl programming language system itself + +a) the GNU General Public License as published by the Free + Software Foundation; either version 1, or (at your option) any + later version, or +b) the "Artistic License" + +--- The GNU General Public License, Version 1, February 1989 --- + +This software is Copyright (c) 2020 by Graham TerMarsch. + +This is free software, licensed under: + + The GNU General Public License, Version 1, February 1989 + + GNU GENERAL PUBLIC LICENSE + Version 1, February 1989 + + Copyright (C) 1989 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The license agreements of most software companies try to keep users +at the mercy of those companies. By contrast, our General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. The +General Public License applies to the Free Software Foundation's +software and to any other program whose authors commit to using it. +You can use it for your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Specifically, the General Public License is designed to make +sure that you have the freedom to give away or sell copies of free +software, that you receive source code or can get it if you want it, +that you can change the software or use pieces of it in new free +programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of a such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must tell them their rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any program or other work which +contains a notice placed by the copyright holder saying it may be +distributed under the terms of this General Public License. The +"Program", below, refers to any such program or work, and a "work based +on the Program" means either the Program or any work containing the +Program or a portion of it, either verbatim or with modifications. Each +licensee is addressed as "you". + + 1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this +General Public License and to the absence of any warranty; and give any +other recipients of the Program a copy of this General Public License +along with the Program. You may charge a fee for the physical act of +transferring a copy. + + 2. You may modify your copy or copies of the Program or any portion of +it, and copy and distribute such modifications under the terms of Paragraph +1 above, provided that you also do the following: + + a) cause the modified files to carry prominent notices stating that + you changed the files and the date of any change; and + + b) cause the whole of any work that you distribute or publish, that + in whole or in part contains the Program or any part thereof, either + with or without modifications, to be licensed at no charge to all + third parties under the terms of this General Public License (except + that you may choose to grant warranty protection to some or all + third parties, at your option). + + c) If the modified program normally reads commands interactively when + run, you must cause it, when started running for such interactive use + in the simplest and most usual way, to print or display an + announcement including an appropriate copyright notice and a notice + that there is no warranty (or else, saying that you provide a + warranty) and that users may redistribute the program under these + conditions, and telling the user how to view a copy of this General + Public License. + + d) You may charge a fee for the physical act of transferring a + copy, and you may at your option offer warranty protection in + exchange for a fee. + +Mere aggregation of another independent work with the Program (or its +derivative) on a volume of a storage or distribution medium does not bring +the other work under the scope of these terms. + + 3. You may copy and distribute the Program (or a portion or derivative of +it, under Paragraph 2) in object code or executable form under the terms of +Paragraphs 1 and 2 above provided that you also do one of the following: + + a) accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of + Paragraphs 1 and 2 above; or, + + b) accompany it with a written offer, valid for at least three + years, to give any third party free (except for a nominal charge + for the cost of distribution) a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of + Paragraphs 1 and 2 above; or, + + c) accompany it with the information you received as to where the + corresponding source code may be obtained. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form alone.) + +Source code for a work means the preferred form of the work for making +modifications to it. For an executable file, complete source code means +all the source code for all modules it contains; but, as a special +exception, it need not include source code for modules which are standard +libraries that accompany the operating system on which the executable +file runs, or for standard header files or definitions files that +accompany that operating system. + + 4. You may not copy, modify, sublicense, distribute or transfer the +Program except as expressly provided under this General Public License. +Any attempt otherwise to copy, modify, sublicense, distribute or transfer +the Program is void, and will automatically terminate your rights to use +the Program under this License. However, parties who have received +copies, or rights to use copies, from you under this General Public +License will not have their licenses terminated so long as such parties +remain in full compliance. + + 5. By copying, distributing or modifying the Program (or any work based +on the Program) you indicate your acceptance of this license to do so, +and all its terms and conditions. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these +terms and conditions. You may not impose any further restrictions on the +recipients' exercise of the rights granted herein. + + 7. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of the license which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +the license, you may choose any version ever published by the Free Software +Foundation. + + 8. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + Appendix: How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to humanity, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively convey +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 1, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19xx name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the +appropriate parts of the General Public License. Of course, the +commands you use may be called something other than `show w' and `show +c'; they could even be mouse-clicks or menu items--whatever suits your +program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program `Gnomovision' (a program to direct compilers to make passes + at assemblers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +That's all there is to it! + + +--- The Artistic License 1.0 --- + +This software is Copyright (c) 2020 by Graham TerMarsch. + +This is free software, licensed under: + + The Artistic License 1.0 + +The Artistic License + +Preamble + +The intent of this document is to state the conditions under which a Package +may be copied, such that the Copyright Holder maintains some semblance of +artistic control over the development of the package, while giving the users of +the package the right to use and distribute the Package in a more-or-less +customary fashion, plus the right to make reasonable modifications. + +Definitions: + + - "Package" refers to the collection of files distributed by the Copyright + Holder, and derivatives of that collection of files created through + textual modification. + - "Standard Version" refers to such a Package if it has not been modified, + or has been modified in accordance with the wishes of the Copyright + Holder. + - "Copyright Holder" is whoever is named in the copyright or copyrights for + the package. + - "You" is you, if you're thinking about copying or distributing this Package. + - "Reasonable copying fee" is whatever you can justify on the basis of media + cost, duplication charges, time of people involved, and so on. (You will + not be required to justify it to the Copyright Holder, but only to the + computing community at large as a market that must bear the fee.) + - "Freely Available" means that no fee is charged for the item itself, though + there may be fees involved in handling the item. It also means that + recipients of the item may redistribute it under the same conditions they + received it. + +1. You may make and give away verbatim copies of the source form of the +Standard Version of this Package without restriction, provided that you +duplicate all of the original copyright notices and associated disclaimers. + +2. You may apply bug fixes, portability fixes and other modifications derived +from the Public Domain or from the Copyright Holder. A Package modified in such +a way shall still be considered the Standard Version. + +3. You may otherwise modify your copy of this Package in any way, provided that +you insert a prominent notice in each changed file stating how and when you +changed that file, and provided that you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise make them + Freely Available, such as by posting said modifications to Usenet or an + equivalent medium, or placing the modifications on a major archive site + such as ftp.uu.net, or by allowing the Copyright Holder to include your + modifications in the Standard Version of the Package. + + b) use the modified Package only within your corporation or organization. + + c) rename any non-standard executables so the names do not conflict with + standard executables, which must also be provided, and provide a separate + manual page for each non-standard executable that clearly documents how it + differs from the Standard Version. + + d) make other distribution arrangements with the Copyright Holder. + +4. You may distribute the programs of this Package in object code or executable +form, provided that you do at least ONE of the following: + + a) distribute a Standard Version of the executables and library files, + together with instructions (in the manual page or equivalent) on where to + get the Standard Version. + + b) accompany the distribution with the machine-readable source of the Package + with your modifications. + + c) accompany any non-standard executables with their corresponding Standard + Version executables, giving the non-standard executables non-standard + names, and clearly documenting the differences in manual pages (or + equivalent), together with instructions on where to get the Standard + Version. + + d) make other distribution arrangements with the Copyright Holder. + +5. You may charge a reasonable copying fee for any distribution of this +Package. You may charge any fee you choose for support of this Package. You +may not charge a fee for this Package itself. However, you may distribute this +Package in aggregate with other (possibly commercial) programs as part of a +larger (possibly commercial) software distribution provided that you do not +advertise this Package as a product of your own. + +6. The scripts and library files supplied as input to or produced as output +from the programs of this Package do not automatically fall under the copyright +of this Package, but belong to whomever generated them, and may be sold +commercially, and may be aggregated with this Package. + +7. C or perl subroutines supplied by you and linked into this Package shall not +be considered part of this Package. + +8. The name of the Copyright Holder may not be used to endorse or promote +products derived from this software without specific prior written permission. + +9. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +The End + diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..3be1b89 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,66 @@ +# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.017. +Changes +LICENSE +MANIFEST +META.json +META.yml +Makefile.PL +README +XS.xs +cpanfile +lib/JavaScript/Minifier/XS.pm +t/00-report-prereqs.dd +t/00-report-prereqs.t +t/01-loads.t +t/02-minify.t +t/03-minifies-to-nothing.t +t/04-not-javascript.t +t/author-benchmark.t +t/author-clean-namespaces.t +t/author-compile.t +t/author-distmeta.t +t/author-eof.t +t/author-eol.t +t/author-leaks.t +t/author-minimum-version.t +t/author-no-breakpoints.t +t/author-no-tabs.t +t/author-pod-coverage.t +t/author-pod-spell.t +t/author-pod-syntax.t +t/author-synopsis.t +t/author-test-compile.t +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/comments.js +t/js/comments.min +t/js/division-of-array-subscripts.js +t/js/division-of-array-subscripts.min +t/js/division.js +t/js/division.min +t/js/es6-templates.js +t/js/es6-templates.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 +t/release-kwalitee.t +t/release-unused-vars.t diff --git a/META.json b/META.json new file mode 100644 index 0000000..3ac67ad --- /dev/null +++ b/META.json @@ -0,0 +1,92 @@ +{ + "abstract" : "XS based JavaScript minifier", + "author" : [ + "Graham TerMarsch " + ], + "dynamic_config" : 0, + "generated_by" : "Dist::Zilla version 6.017, CPAN::Meta::Converter version 2.150010", + "license" : [ + "perl_5" + ], + "meta-spec" : { + "url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec", + "version" : 2 + }, + "name" : "JavaScript-Minifier-XS", + "no_index" : { + "directory" : [ + "t", + "xt" + ] + }, + "prereqs" : { + "configure" : { + "requires" : { + "ExtUtils::MakeMaker" : "0" + } + }, + "develop" : { + "requires" : { + "Dist::Zilla" : "5", + "Dist::Zilla::Plugin::FileFinder::ByName" : "0", + "Dist::Zilla::PluginBundle::Author::GTERMARS" : "0", + "File::Slurp" : "0", + "File::Spec" : "0", + "File::Temp" : "0", + "IO::Handle" : "0", + "IPC::Open3" : "0", + "IPC::Run" : "0", + "JavaScript::Minifier" : "0", + "Pod::Coverage::TrustPod" : "0", + "Software::License::Perl_5" : "0", + "Test::CPAN::Meta" : "0", + "Test::CleanNamespaces" : "0.15", + "Test::EOF" : "0", + "Test::EOL" : "0", + "Test::Kwalitee" : "1.21", + "Test::LeakTrace" : "0", + "Test::MinimumVersion" : "0", + "Test::More" : "0.94", + "Test::NoBreakpoints" : "0.15", + "Test::NoTabs" : "0", + "Test::Pod" : "1.41", + "Test::Pod::Coverage" : "1.08", + "Test::Spelling" : "0.12", + "Test::Synopsis" : "0" + } + }, + "runtime" : { + "requires" : { + "perl" : "5.008001" + } + }, + "test" : { + "recommends" : { + "CPAN::Meta" : "2.120900" + }, + "requires" : { + "ExtUtils::MakeMaker" : "0", + "File::Spec" : "0", + "Test::DiagINC" : "0.002", + "Test::More" : "0.96" + } + } + }, + "release_status" : "stable", + "resources" : { + "bugtracker" : { + "web" : "https://github.com/bleargh45/JavaScript-Minifier-XS/issues" + }, + "repository" : { + "type" : "git", + "url" : "git://github.com/bleargh45/JavaScript-Minifier-XS.git", + "web" : "https://github.com/bleargh45/JavaScript-Minifier-XS" + } + }, + "version" : "0.13", + "x_generated_by_perl" : "v5.32.0", + "x_serialization_backend" : "Cpanel::JSON::XS version 4.24", + "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later", + "x_static_install" : 0 +} + diff --git a/META.yml b/META.yml new file mode 100644 index 0000000..9d4d08a --- /dev/null +++ b/META.yml @@ -0,0 +1,32 @@ +--- +abstract: 'XS based JavaScript minifier' +author: + - 'Graham TerMarsch ' +build_requires: + ExtUtils::MakeMaker: '0' + File::Spec: '0' + Test::DiagINC: '0.002' + Test::More: '0.96' +configure_requires: + ExtUtils::MakeMaker: '0' +dynamic_config: 0 +generated_by: 'Dist::Zilla version 6.017, CPAN::Meta::Converter version 2.150010' +license: perl +meta-spec: + url: http://module-build.sourceforge.net/META-spec-v1.4.html + version: '1.4' +name: JavaScript-Minifier-XS +no_index: + directory: + - t + - xt +requires: + perl: '5.008001' +resources: + bugtracker: https://github.com/bleargh45/JavaScript-Minifier-XS/issues + repository: git://github.com/bleargh45/JavaScript-Minifier-XS.git +version: '0.13' +x_generated_by_perl: v5.32.0 +x_serialization_backend: 'YAML::Tiny version 1.73' +x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later' +x_static_install: 0 diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..f195890 --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,50 @@ +# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.017. +use strict; +use warnings; + +use 5.008001; + +use ExtUtils::MakeMaker; + +my %WriteMakefileArgs = ( + "ABSTRACT" => "XS based JavaScript minifier", + "AUTHOR" => "Graham TerMarsch ", + "CONFIGURE_REQUIRES" => { + "ExtUtils::MakeMaker" => 0 + }, + "DISTNAME" => "JavaScript-Minifier-XS", + "LICENSE" => "perl", + "MIN_PERL_VERSION" => "5.008001", + "NAME" => "JavaScript::Minifier::XS", + "PREREQ_PM" => {}, + "TEST_REQUIRES" => { + "ExtUtils::MakeMaker" => 0, + "File::Spec" => 0, + "Test::DiagINC" => "0.002", + "Test::More" => "0.96" + }, + "VERSION" => "0.13", + "test" => { + "TESTS" => "t/*.t" + } +); + + +my %FallbackPrereqs = ( + "ExtUtils::MakeMaker" => 0, + "File::Spec" => 0, + "Test::DiagINC" => "0.002", + "Test::More" => "0.96" +); + + +unless ( eval { ExtUtils::MakeMaker->VERSION(6.63_03) } ) { + delete $WriteMakefileArgs{TEST_REQUIRES}; + delete $WriteMakefileArgs{BUILD_REQUIRES}; + $WriteMakefileArgs{PREREQ_PM} = \%FallbackPrereqs; +} + +delete $WriteMakefileArgs{CONFIGURE_REQUIRES} + unless eval { ExtUtils::MakeMaker->VERSION(6.52) }; + +WriteMakefile(%WriteMakefileArgs); diff --git a/README b/README new file mode 100644 index 0000000..73461bf --- /dev/null +++ b/README @@ -0,0 +1,102 @@ +NAME + + JavaScript::Minifier::XS - XS based JavaScript minifier + +SYNOPSIS + + use JavaScript::Minifier::XS qw(minify); + my $js = '...'; + my $minified = minify($js); + +DESCRIPTION + + JavaScript::Minifier::XS is a JavaScript "minifier"; its designed to + remove unnecessary whitespace and comments from JavaScript files, which + also not breaking the JavaScript. + + JavaScript::Minifier::XS is similar in function to + JavaScript::Minifier, but is substantially faster as its written in XS + and not just pure Perl. + +METHODS + + minify($js) + + Minifies the given $js, returning the minified JavaScript back to the + caller. + +HOW IT WORKS + + JavaScript::Minifier::XS minifies the JavaScript by removing + unnecessary whitespace from JavaScript documents. Comments (both block + and line) are also removed, except 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: + + Pass 1: Tokenize + + First, we go through and parse the JavaScript document into a series of + tokens internally. The tokenizing process does not 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. + + Pass 2: Collapse + + We then march through the token list and collapse certain tokens down + to their smallest possible representation. If they're still included in + the final results we only want to include them at their shortest. + + 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 first EOL character + encountered. Otherwise, the result is the first whitespace character + in the run. + + Pass 3: Pruning + + We then go back through the token list and prune and remove unnecessary + tokens. + + Whitespace + + Wherever possible, whitespace is removed; before+after comment + blocks, and before+after various symbols/sigils. + + Comments + + Comments that are either (a) IE conditional compilation comments, or + that (b) contain the word "copyright" in them are preserved. All + other comments (line and block) are removed. + + Everything else + + We keep everything else; identifiers, quoted literal strings, + symbols/sigils, etc. + + 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. + +AUTHOR + + Graham TerMarsch (cpan@howlingfrog.com) + +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. + +SEE ALSO + + JavaScript::Minifier. + diff --git a/XS.xs b/XS.xs new file mode 100644 index 0000000..64500a5 --- /dev/null +++ b/XS.xs @@ -0,0 +1,742 @@ +#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; +#ifdef DEBUG +static char* strNodeTypes[] = { + "empty", + "whitespace", + "block comment", + "line comment", + "identifier", + "literal", + "sigil" + }; +#endif + +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] == '\'') || (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 diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..ee1bd36 --- /dev/null +++ b/cpanfile @@ -0,0 +1,8 @@ +requires 'perl', '>= 5.008001'; + +test_requires 'Test::More', '>= 0.96'; + +author_requires 'File::Slurp'; +author_requires 'IPC::Run'; +author_requires 'JavaScript::Minifier'; +author_requires 'Test::LeakTrace'; diff --git a/lib/JavaScript/Minifier/XS.pm b/lib/JavaScript/Minifier/XS.pm new file mode 100644 index 0000000..6ba0296 --- /dev/null +++ b/lib/JavaScript/Minifier/XS.pm @@ -0,0 +1,129 @@ +package JavaScript::Minifier::XS; + +use 5.8.1; +use strict; +use warnings; + +require Exporter; +require DynaLoader; +our @ISA = qw(Exporter DynaLoader); +our @EXPORT_OK = qw(minify); + +our $VERSION = '0.13'; + +bootstrap JavaScript::Minifier::XS $VERSION; + +1; + +=for stopwords minifies minified minifier minification tokenizing EOL + +=head1 NAME + +JavaScript::Minifier::XS - XS based JavaScript minifier + +=head1 SYNOPSIS + + use JavaScript::Minifier::XS qw(minify); + my $js = '...'; + my $minified = minify($js); + +=head1 DESCRIPTION + +C is a JavaScript "minifier"; its designed to remove +unnecessary 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 unnecessary +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 unnecessary +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 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 diff --git a/t/00-report-prereqs.dd b/t/00-report-prereqs.dd new file mode 100644 index 0000000..7fcf7d7 --- /dev/null +++ b/t/00-report-prereqs.dd @@ -0,0 +1,55 @@ +do { my $x = { + 'configure' => { + 'requires' => { + 'ExtUtils::MakeMaker' => '0' + } + }, + 'develop' => { + 'requires' => { + 'Dist::Zilla' => '5', + 'Dist::Zilla::Plugin::FileFinder::ByName' => '0', + 'Dist::Zilla::PluginBundle::Author::GTERMARS' => '0', + 'File::Slurp' => '0', + 'File::Spec' => '0', + 'File::Temp' => '0', + 'IO::Handle' => '0', + 'IPC::Open3' => '0', + 'IPC::Run' => '0', + 'JavaScript::Minifier' => '0', + 'Pod::Coverage::TrustPod' => '0', + 'Software::License::Perl_5' => '0', + 'Test::CPAN::Meta' => '0', + 'Test::CleanNamespaces' => '0.15', + 'Test::EOF' => '0', + 'Test::EOL' => '0', + 'Test::Kwalitee' => '1.21', + 'Test::LeakTrace' => '0', + 'Test::MinimumVersion' => '0', + 'Test::More' => '0.94', + 'Test::NoBreakpoints' => '0.15', + 'Test::NoTabs' => '0', + 'Test::Pod' => '1.41', + 'Test::Pod::Coverage' => '1.08', + 'Test::Spelling' => '0.12', + 'Test::Synopsis' => '0' + } + }, + 'runtime' => { + 'requires' => { + 'perl' => '5.008001' + } + }, + 'test' => { + 'recommends' => { + 'CPAN::Meta' => '2.120900' + }, + 'requires' => { + 'ExtUtils::MakeMaker' => '0', + 'File::Spec' => '0', + 'Test::DiagINC' => '0.002', + 'Test::More' => '0.96' + } + } + }; + $x; + } \ No newline at end of file diff --git a/t/00-report-prereqs.t b/t/00-report-prereqs.t new file mode 100644 index 0000000..b1a59f9 --- /dev/null +++ b/t/00-report-prereqs.t @@ -0,0 +1,193 @@ +#!perl + +use strict; +use warnings; + +# This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs 0.028 + +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; use Test::More tests => 1; + +use ExtUtils::MakeMaker; +use File::Spec; + +# from $version::LAX +my $lax_version_re = + qr/(?: undef | (?: (?:[0-9]+) (?: \. | (?:\.[0-9]+) (?:_[0-9]+)? )? + | + (?:\.[0-9]+) (?:_[0-9]+)? + ) | (?: + v (?:[0-9]+) (?: (?:\.[0-9]+)+ (?:_[0-9]+)? )? + | + (?:[0-9]+)? (?:\.[0-9]+){2,} (?:_[0-9]+)? + ) + )/x; + +# hide optional CPAN::Meta modules from prereq scanner +# and check if they are available +my $cpan_meta = "CPAN::Meta"; +my $cpan_meta_pre = "CPAN::Meta::Prereqs"; +my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic + +# Verify requirements? +my $DO_VERIFY_PREREQS = 1; + +sub _max { + my $max = shift; + $max = ( $_ > $max ) ? $_ : $max for @_; + return $max; +} + +sub _merge_prereqs { + my ($collector, $prereqs) = @_; + + # CPAN::Meta::Prereqs object + if (ref $collector eq $cpan_meta_pre) { + return $collector->with_merged_prereqs( + CPAN::Meta::Prereqs->new( $prereqs ) + ); + } + + # Raw hashrefs + for my $phase ( keys %$prereqs ) { + for my $type ( keys %{ $prereqs->{$phase} } ) { + for my $module ( keys %{ $prereqs->{$phase}{$type} } ) { + $collector->{$phase}{$type}{$module} = $prereqs->{$phase}{$type}{$module}; + } + } + } + + return $collector; +} + +my @include = qw( + +); + +my @exclude = qw( + +); + +# Add static prereqs to the included modules list +my $static_prereqs = do './t/00-report-prereqs.dd'; + +# Merge all prereqs (either with ::Prereqs or a hashref) +my $full_prereqs = _merge_prereqs( + ( $HAS_CPAN_META ? $cpan_meta_pre->new : {} ), + $static_prereqs +); + +# Add dynamic prereqs to the included modules list (if we can) +my ($source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; +my $cpan_meta_error; +if ( $source && $HAS_CPAN_META + && (my $meta = eval { CPAN::Meta->load_file($source) } ) +) { + $full_prereqs = _merge_prereqs($full_prereqs, $meta->prereqs); +} +else { + $cpan_meta_error = $@; # capture error from CPAN::Meta->load_file($source) + $source = 'static metadata'; +} + +my @full_reports; +my @dep_errors; +my $req_hash = $HAS_CPAN_META ? $full_prereqs->as_string_hash : $full_prereqs; + +# Add static includes into a fake section +for my $mod (@include) { + $req_hash->{other}{modules}{$mod} = 0; +} + +for my $phase ( qw(configure build test runtime develop other) ) { + next unless $req_hash->{$phase}; + next if ($phase eq 'develop' and not $ENV{AUTHOR_TESTING}); + + for my $type ( qw(requires recommends suggests conflicts modules) ) { + next unless $req_hash->{$phase}{$type}; + + my $title = ucfirst($phase).' '.ucfirst($type); + my @reports = [qw/Module Want Have/]; + + for my $mod ( sort keys %{ $req_hash->{$phase}{$type} } ) { + next if $mod eq 'perl'; + next if grep { $_ eq $mod } @exclude; + + my $file = $mod; + $file =~ s{::}{/}g; + $file .= ".pm"; + my ($prefix) = grep { -e File::Spec->catfile($_, $file) } @INC; + + my $want = $req_hash->{$phase}{$type}{$mod}; + $want = "undef" unless defined $want; + $want = "any" if !$want && $want == 0; + + my $req_string = $want eq 'any' ? 'any version required' : "version '$want' required"; + + if ($prefix) { + my $have = MM->parse_version( File::Spec->catfile($prefix, $file) ); + $have = "undef" unless defined $have; + push @reports, [$mod, $want, $have]; + + if ( $DO_VERIFY_PREREQS && $HAS_CPAN_META && $type eq 'requires' ) { + if ( $have !~ /\A$lax_version_re\z/ ) { + push @dep_errors, "$mod version '$have' cannot be parsed ($req_string)"; + } + elsif ( ! $full_prereqs->requirements_for( $phase, $type )->accepts_module( $mod => $have ) ) { + push @dep_errors, "$mod version '$have' is not in required range '$want'"; + } + } + } + else { + push @reports, [$mod, $want, "missing"]; + + if ( $DO_VERIFY_PREREQS && $type eq 'requires' ) { + push @dep_errors, "$mod is not installed ($req_string)"; + } + } + } + + if ( @reports ) { + push @full_reports, "=== $title ===\n\n"; + + my $ml = _max( map { length $_->[0] } @reports ); + my $wl = _max( map { length $_->[1] } @reports ); + my $hl = _max( map { length $_->[2] } @reports ); + + if ($type eq 'modules') { + splice @reports, 1, 0, ["-" x $ml, "", "-" x $hl]; + push @full_reports, map { sprintf(" %*s %*s\n", -$ml, $_->[0], $hl, $_->[2]) } @reports; + } + else { + splice @reports, 1, 0, ["-" x $ml, "-" x $wl, "-" x $hl]; + push @full_reports, map { sprintf(" %*s %*s %*s\n", -$ml, $_->[0], $wl, $_->[1], $hl, $_->[2]) } @reports; + } + + push @full_reports, "\n"; + } + } +} + +if ( @full_reports ) { + diag "\nVersions for all modules listed in $source (including optional ones):\n\n", @full_reports; +} + +if ( $cpan_meta_error || @dep_errors ) { + diag "\n*** WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING ***\n"; +} + +if ( $cpan_meta_error ) { + my ($orig_source) = grep { -f } 'MYMETA.json', 'MYMETA.yml'; + diag "\nCPAN::Meta->load_file('$orig_source') failed with: $cpan_meta_error\n"; +} + +if ( @dep_errors ) { + diag join("\n", + "\nThe following REQUIRED prerequisites were not satisfied:\n", + @dep_errors, + "\n" + ); +} + +pass('Reported prereqs'); + +# vim: ts=4 sts=4 sw=4 et: diff --git a/t/01-loads.t b/t/01-loads.t new file mode 100644 index 0000000..5e95bc4 --- /dev/null +++ b/t/01-loads.t @@ -0,0 +1,5 @@ +use strict; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; use Test::More tests=>1; +BEGIN { + use_ok( 'JavaScript::Minifier::XS' ); +} diff --git a/t/02-minify.t b/t/02-minify.t new file mode 100644 index 0000000..46b80e8 --- /dev/null +++ b/t/02-minify.t @@ -0,0 +1,37 @@ +use strict; +use warnings; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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; +} diff --git a/t/03-minifies-to-nothing.t b/t/03-minifies-to-nothing.t new file mode 100644 index 0000000..84e95cc --- /dev/null +++ b/t/03-minifies-to-nothing.t @@ -0,0 +1,20 @@ +use strict; +use warnings; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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" ); diff --git a/t/04-not-javascript.t b/t/04-not-javascript.t new file mode 100644 index 0000000..891c3dd --- /dev/null +++ b/t/04-not-javascript.t @@ -0,0 +1,16 @@ +use strict; +use warnings; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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(); diff --git a/t/author-benchmark.t b/t/author-benchmark.t new file mode 100644 index 0000000..ab73ad4 --- /dev/null +++ b/t/author-benchmark.t @@ -0,0 +1,63 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; +use Test::More; +use File::Slurp qw(slurp); +use Benchmark qw(countit); +use JavaScript::Minifier::XS; + +############################################################################### +# Only run Benchmark if asked for. +unless ($ENV{BENCHMARK}) { + plan skip_all => 'Skipping Benchmark; use BENCHMARK=1 to run'; +} + +############################################################################### +# 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'; +} diff --git a/t/author-clean-namespaces.t b/t/author-clean-namespaces.t new file mode 100644 index 0000000..a5531c3 --- /dev/null +++ b/t/author-clean-namespaces.t @@ -0,0 +1,19 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; + +# this test was generated with Dist::Zilla::Plugin::Test::CleanNamespaces 0.006 + +use Test::More 0.94; +use Test::CleanNamespaces 0.15; + +subtest all_namespaces_clean => sub { all_namespaces_clean() }; + +done_testing; diff --git a/t/author-compile.t b/t/author-compile.t new file mode 100644 index 0000000..c489ddf --- /dev/null +++ b/t/author-compile.t @@ -0,0 +1,71 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use 5.006; +use strict; +use warnings; + +# this test was generated with Dist::Zilla::Plugin::Test::Compile 2.058 + +use Test::More 0.94; + +plan tests => 2; + +my @module_files = ( + 'JavaScript/Minifier/XS.pm' +); + + + +# fake home for cpan-testers +use File::Temp; +local $ENV{HOME} = File::Temp::tempdir( CLEANUP => 1 ); + + +my @switches = ( + -d 'blib' ? '-Mblib' : '-Ilib', +); + +use File::Spec; +use IPC::Open3; +use IO::Handle; + +open my $stdin, '<', File::Spec->devnull or die "can't open devnull: $!"; + +my @warnings; +for my $lib (@module_files) +{ + # see L + my $stderr = IO::Handle->new; + + diag('Running: ', join(', ', map { my $str = $_; $str =~ s/'/\\'/g; q{'} . $str . q{'} } + $^X, @switches, '-e', "require q[$lib]")) + if $ENV{PERL_COMPILE_TEST_DEBUG}; + + my $pid = open3($stdin, '>&STDERR', $stderr, $^X, @switches, '-e', "require q[$lib]"); + binmode $stderr, ':crlf' if $^O eq 'MSWin32'; + my @_warnings = <$stderr>; + waitpid($pid, 0); + is($?, 0, "$lib loaded ok"); + + shift @_warnings if @_warnings and $_warnings[0] =~ /^Using .*\bblib/ + and not eval { +require blib; blib->VERSION('1.01') }; + + if (@_warnings) + { + warn @_warnings; + push @warnings, @_warnings; + } +} + + + +is(scalar(@warnings), 0, 'no warnings found') + or diag 'got warnings: ', explain(\@warnings); + +BAIL_OUT("Compilation problems") if !Test::More->builder->is_passing; diff --git a/t/author-distmeta.t b/t/author-distmeta.t new file mode 100644 index 0000000..8d9ce71 --- /dev/null +++ b/t/author-distmeta.t @@ -0,0 +1,14 @@ +#!perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +# This file was automatically generated by Dist::Zilla::Plugin::MetaTests. + +use Test::CPAN::Meta; + +meta_yaml_ok(); diff --git a/t/author-eof.t b/t/author-eof.t new file mode 100644 index 0000000..0ae7431 --- /dev/null +++ b/t/author-eof.t @@ -0,0 +1,19 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; +use Test::More; + +# Generated by Dist::Zilla::Plugin::Test::EOF 0.0600 +eval "use Test::EOF"; +plan skip_all => 'Test::EOF required to test for correct end of file flag' if $@; + +all_perl_files_ok({ minimum_newlines => 1, maximum_newlines => 4 }); + +done_testing(); diff --git a/t/author-eol.t b/t/author-eol.t new file mode 100644 index 0000000..5cf10d1 --- /dev/null +++ b/t/author-eol.t @@ -0,0 +1,28 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; + +# this test was generated with Dist::Zilla::Plugin::Test::EOL 0.19 + +use Test::More 0.88; +use Test::EOL; + +my @files = ( + 'lib/JavaScript/Minifier/XS.pm', + 't/00-report-prereqs.dd', + 't/00-report-prereqs.t', + 't/01-loads.t', + 't/02-minify.t', + 't/03-minifies-to-nothing.t', + 't/04-not-javascript.t' +); + +eol_unix_ok($_, { trailing_whitespace => 1 }) foreach @files; +done_testing; diff --git a/t/author-leaks.t b/t/author-leaks.t new file mode 100644 index 0000000..2da06b1 --- /dev/null +++ b/t/author-leaks.t @@ -0,0 +1,32 @@ +#!/usr/bin/perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + + +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'; diff --git a/t/author-minimum-version.t b/t/author-minimum-version.t new file mode 100644 index 0000000..db391de --- /dev/null +++ b/t/author-minimum-version.t @@ -0,0 +1,14 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; + +use Test::More; +use Test::MinimumVersion; +all_minimum_version_from_metayml_ok(); diff --git a/t/author-no-breakpoints.t b/t/author-no-breakpoints.t new file mode 100644 index 0000000..c714119 --- /dev/null +++ b/t/author-no-breakpoints.t @@ -0,0 +1,19 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; + +# this test was generated with Dist::Zilla::Plugin::Test::NoBreakpoints 0.0.2 + +use Test::More 0.88; +use Test::NoBreakpoints 0.15; + +all_files_no_breakpoints_ok(); + +done_testing; diff --git a/t/author-no-tabs.t b/t/author-no-tabs.t new file mode 100644 index 0000000..2084705 --- /dev/null +++ b/t/author-no-tabs.t @@ -0,0 +1,28 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; + +# this test was generated with Dist::Zilla::Plugin::Test::NoTabs 0.15 + +use Test::More 0.88; +use Test::NoTabs; + +my @files = ( + 'lib/JavaScript/Minifier/XS.pm', + 't/00-report-prereqs.dd', + 't/00-report-prereqs.t', + 't/01-loads.t', + 't/02-minify.t', + 't/03-minifies-to-nothing.t', + 't/04-not-javascript.t' +); + +notabs_ok($_) foreach @files; +done_testing; diff --git a/t/author-pod-coverage.t b/t/author-pod-coverage.t new file mode 100644 index 0000000..243340f --- /dev/null +++ b/t/author-pod-coverage.t @@ -0,0 +1,15 @@ +#!perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +# This file was automatically generated by Dist::Zilla::Plugin::PodCoverageTests. + +use Test::Pod::Coverage 1.08; +use Pod::Coverage::TrustPod; + +all_pod_coverage_ok({ coverage_class => 'Pod::Coverage::TrustPod' }); diff --git a/t/author-pod-spell.t b/t/author-pod-spell.t new file mode 100644 index 0000000..14a048f --- /dev/null +++ b/t/author-pod-spell.t @@ -0,0 +1,27 @@ + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +use strict; +use warnings; +use Test::More; + +# generated by Dist::Zilla::Plugin::Test::PodSpelling 2.007005 +use Test::Spelling 0.12; +use Pod::Wordlist; + + +add_stopwords(); +all_pod_files_spelling_ok( qw( bin lib ) ); +__DATA__ +Graham +JavaScript +Minifier +TerMarsch +XS +cpan +lib diff --git a/t/author-pod-syntax.t b/t/author-pod-syntax.t new file mode 100644 index 0000000..2233af0 --- /dev/null +++ b/t/author-pod-syntax.t @@ -0,0 +1,15 @@ +#!perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +# This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. +use strict; use warnings; +use Test::More; +use Test::Pod 1.41; + +all_pod_files_ok(); diff --git a/t/author-synopsis.t b/t/author-synopsis.t new file mode 100644 index 0000000..5d1d4a7 --- /dev/null +++ b/t/author-synopsis.t @@ -0,0 +1,13 @@ +#!perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + + +use Test::Synopsis; + +all_synopsis_ok(); diff --git a/t/author-test-compile.t b/t/author-test-compile.t new file mode 100644 index 0000000..b022edb --- /dev/null +++ b/t/author-test-compile.t @@ -0,0 +1,75 @@ +#!/usr/bin/perl + +BEGIN { + unless ($ENV{AUTHOR_TESTING}) { + print qq{1..0 # SKIP these tests are for testing by the author\n}; + exit + } +} + +# +# 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); + +############################################################################### +# 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(); diff --git a/t/js/comments-before-a-regex.js b/t/js/comments-before-a-regex.js new file mode 100644 index 0000000..f8476a1 --- /dev/null +++ b/t/js/comments-before-a-regex.js @@ -0,0 +1,18 @@ +/* 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/ + ]; diff --git a/t/js/comments-before-a-regex.min b/t/js/comments-before-a-regex.min new file mode 100644 index 0000000..1ceb219 --- /dev/null +++ b/t/js/comments-before-a-regex.min @@ -0,0 +1 @@ +var foo=[/^'/,/^"/,/foo/]; diff --git a/t/js/comments-ie-conditional.js b/t/js/comments-ie-conditional.js new file mode 100644 index 0000000..aacf833 --- /dev/null +++ b/t/js/comments-ie-conditional.js @@ -0,0 +1,7 @@ +/* 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 diff --git a/t/js/comments-ie-conditional.min b/t/js/comments-ie-conditional.min new file mode 100644 index 0000000..824c6cf --- /dev/null +++ b/t/js/comments-ie-conditional.min @@ -0,0 +1,2 @@ +/*@ except those that are "IE Conditional Compilation" comments @*/ +//@ except those that are "IE Conditional Compilation" line comments diff --git a/t/js/comments.js b/t/js/comments.js new file mode 100644 index 0000000..7247866 --- /dev/null +++ b/t/js/comments.js @@ -0,0 +1,17 @@ +/* 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; diff --git a/t/js/comments.min b/t/js/comments.min new file mode 100644 index 0000000..5be3fa4 --- /dev/null +++ b/t/js/comments.min @@ -0,0 +1,3 @@ +/* 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; diff --git a/t/js/division-of-array-subscripts.js b/t/js/division-of-array-subscripts.js new file mode 100644 index 0000000..42ba9fc --- /dev/null +++ b/t/js/division-of-array-subscripts.js @@ -0,0 +1,10 @@ +/* + * 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 +} diff --git a/t/js/division-of-array-subscripts.min b/t/js/division-of-array-subscripts.min new file mode 100644 index 0000000..b70060d --- /dev/null +++ b/t/js/division-of-array-subscripts.min @@ -0,0 +1,2 @@ +function foo(){var bar=someArray[2]/2;} +function bar(){foo();} diff --git a/t/js/division.js b/t/js/division.js new file mode 100644 index 0000000..ea6d368 --- /dev/null +++ b/t/js/division.js @@ -0,0 +1,2 @@ +/* use of "/" for division should get compacted */ +var foo = 10 / 2; diff --git a/t/js/division.min b/t/js/division.min new file mode 100644 index 0000000..6993e69 --- /dev/null +++ b/t/js/division.min @@ -0,0 +1 @@ +var foo=10/2; diff --git a/t/js/es6-templates.js b/t/js/es6-templates.js new file mode 100644 index 0000000..b696e3e --- /dev/null +++ b/t/js/es6-templates.js @@ -0,0 +1,10 @@ + +let vat = $(`#items-${i}-vat_id :selected`); + +let html = ` +
+ + + +
+`; diff --git a/t/js/es6-templates.min b/t/js/es6-templates.min new file mode 100644 index 0000000..0210b9a --- /dev/null +++ b/t/js/es6-templates.min @@ -0,0 +1,7 @@ +let vat=$(`#items-${i}-vat_id :selected`);let html=` +
+ + + +
+`; diff --git a/t/js/leading-whitespace.js b/t/js/leading-whitespace.js new file mode 100644 index 0000000..17d3958 --- /dev/null +++ b/t/js/leading-whitespace.js @@ -0,0 +1,4 @@ + + + +var leading="leading whitespace gets removed"; diff --git a/t/js/leading-whitespace.min b/t/js/leading-whitespace.min new file mode 100644 index 0000000..dfd5414 --- /dev/null +++ b/t/js/leading-whitespace.min @@ -0,0 +1 @@ +var leading="leading whitespace gets removed"; diff --git a/t/js/literals-double-quotes.js b/t/js/literals-double-quotes.js new file mode 100644 index 0000000..ad414d9 --- /dev/null +++ b/t/js/literals-double-quotes.js @@ -0,0 +1,2 @@ +/* quoted literals get preserved, in several forms */ +var double_quoted=" double quoted strings /* with block comments */ "; diff --git a/t/js/literals-double-quotes.min b/t/js/literals-double-quotes.min new file mode 100644 index 0000000..650e0f6 --- /dev/null +++ b/t/js/literals-double-quotes.min @@ -0,0 +1 @@ +var double_quoted=" double quoted strings /* with block comments */ "; diff --git a/t/js/literals-regexp.js b/t/js/literals-regexp.js new file mode 100644 index 0000000..44891cb --- /dev/null +++ b/t/js/literals-regexp.js @@ -0,0 +1,2 @@ +/* quoted literals get preserved, in several forms */ +var regexes=/ regexes stay /; diff --git a/t/js/literals-regexp.min b/t/js/literals-regexp.min new file mode 100644 index 0000000..f6a1758 --- /dev/null +++ b/t/js/literals-regexp.min @@ -0,0 +1 @@ +var regexes=/ regexes stay /; diff --git a/t/js/literals-single-quotes.js b/t/js/literals-single-quotes.js new file mode 100644 index 0000000..49d477f --- /dev/null +++ b/t/js/literals-single-quotes.js @@ -0,0 +1,2 @@ +/* quoted literals get preserved, in several forms */ +var single_quoted=' single quoted strings // with line comments '; diff --git a/t/js/literals-single-quotes.min b/t/js/literals-single-quotes.min new file mode 100644 index 0000000..c07e717 --- /dev/null +++ b/t/js/literals-single-quotes.min @@ -0,0 +1 @@ +var single_quoted=' single quoted strings // with line comments '; diff --git a/t/js/postfix-sigil.js b/t/js/postfix-sigil.js new file mode 100644 index 0000000..044cbf5 --- /dev/null +++ b/t/js/postfix-sigil.js @@ -0,0 +1,8 @@ +/* whitespace before "postfix" sigils should get removed */ +function foo( ) + +{ + alert("foo!") + ; + +} diff --git a/t/js/postfix-sigil.min b/t/js/postfix-sigil.min new file mode 100644 index 0000000..6d2baad --- /dev/null +++ b/t/js/postfix-sigil.min @@ -0,0 +1,2 @@ +function foo() +{alert("foo!");} diff --git a/t/js/prefix-sigil.js b/t/js/prefix-sigil.js new file mode 100644 index 0000000..21f8301 --- /dev/null +++ b/t/js/prefix-sigil.js @@ -0,0 +1,4 @@ +/* whitespace after "prefix" sigils should get removed */ +function foo( ){ + alert("foo!"); +} diff --git a/t/js/prefix-sigil.min b/t/js/prefix-sigil.min new file mode 100644 index 0000000..5ff7575 --- /dev/null +++ b/t/js/prefix-sigil.min @@ -0,0 +1 @@ +function foo(){alert("foo!");} diff --git a/t/js/regexp-not-line-comment.js b/t/js/regexp-not-line-comment.js new file mode 100644 index 0000000..0555440 --- /dev/null +++ b/t/js/regexp-not-line-comment.js @@ -0,0 +1,4 @@ +/* RT#80598; regexps containing an escaped "/" should not be treated as comments */ +function foo(url) { + return ( /\// ).test( url ); +} diff --git a/t/js/regexp-not-line-comment.min b/t/js/regexp-not-line-comment.min new file mode 100644 index 0000000..a8b1813 --- /dev/null +++ b/t/js/regexp-not-line-comment.min @@ -0,0 +1 @@ +function foo(url){return(/\//).test(url);} diff --git a/t/js/return-regex.js b/t/js/return-regex.js new file mode 100644 index 0000000..d1d0895 --- /dev/null +++ b/t/js/return-regex.js @@ -0,0 +1,4 @@ +/* should be able to return a regex from a function */ +function foo() { + return /\d{1,2}/; +} diff --git a/t/js/return-regex.min b/t/js/return-regex.min new file mode 100644 index 0000000..7991bd6 --- /dev/null +++ b/t/js/return-regex.min @@ -0,0 +1 @@ +function foo(){return/\d{1,2}/;} diff --git a/t/js/simple.js b/t/js/simple.js new file mode 100644 index 0000000..b3e75b4 --- /dev/null +++ b/t/js/simple.js @@ -0,0 +1,3 @@ +/* foo */ + +var x = 2; diff --git a/t/js/simple.min b/t/js/simple.min new file mode 100644 index 0000000..73d8752 --- /dev/null +++ b/t/js/simple.min @@ -0,0 +1 @@ +var x=2; diff --git a/t/js/trailing-whitespace.js b/t/js/trailing-whitespace.js new file mode 100644 index 0000000..279a302 --- /dev/null +++ b/t/js/trailing-whitespace.js @@ -0,0 +1,4 @@ +var trailing="trailing whitespace gets removed"; + + + diff --git a/t/js/trailing-whitespace.min b/t/js/trailing-whitespace.min new file mode 100644 index 0000000..52bba32 --- /dev/null +++ b/t/js/trailing-whitespace.min @@ -0,0 +1 @@ +var trailing="trailing whitespace gets removed"; diff --git a/t/release-kwalitee.t b/t/release-kwalitee.t new file mode 100644 index 0000000..60627db --- /dev/null +++ b/t/release-kwalitee.t @@ -0,0 +1,17 @@ + +BEGIN { + unless ($ENV{RELEASE_TESTING}) { + print qq{1..0 # SKIP these tests are for release candidate testing\n}; + exit + } +} + +# this test was generated with Dist::Zilla::Plugin::Test::Kwalitee 2.12 +use strict; +use warnings; +use Test::More 0.88; +use Test::Kwalitee 1.21 'kwalitee_ok'; + +kwalitee_ok(); + +done_testing; diff --git a/t/release-unused-vars.t b/t/release-unused-vars.t new file mode 100644 index 0000000..96db6d8 --- /dev/null +++ b/t/release-unused-vars.t @@ -0,0 +1,22 @@ +#!perl + +BEGIN { + unless ($ENV{RELEASE_TESTING}) { + print qq{1..0 # SKIP these tests are for release candidate testing\n}; + exit + } +} + + +use Test::More 0.96 tests => 1; +eval { require Test::Vars }; + +SKIP: { + skip 1 => 'Test::Vars required for testing for unused vars' + if $@; + Test::Vars->import; + + subtest 'unused vars' => sub { +all_vars_ok(); + }; +}; From 6001732e66b8e1b781a4d9d2b21a20e095c3cc31 Mon Sep 17 00:00:00 2001 From: Graham TerMarsch Date: Sat, 6 Feb 2021 23:39:04 -0800 Subject: [PATCH 2/3] Release - v0.14 --- Changes | 10 + LICENSE | 6 +- MANIFEST | 72 +--- META.json | 17 +- META.yml | 8 +- Makefile.PL | 8 +- XS.xs | 264 +++++++------ cpanfile | 8 - lib/JavaScript/Minifier/XS.pm | 2 +- t/00-report-prereqs.dd | 12 +- t/{author-compile.t => 01-compile.t} | 14 +- t/01-loads.t | 5 - t/02-minify.t | 37 -- t/03-minifies-to-nothing.t | 20 - t/04-not-javascript.t | 16 - t/author-benchmark.t | 63 --- t/author-distmeta.t | 14 - t/author-leaks.t | 32 -- t/author-minimum-version.t | 14 - t/author-synopsis.t | 13 - t/js/comments-before-a-regex.js | 18 - t/js/comments-before-a-regex.min | 1 - t/js/comments-ie-conditional.js | 7 - t/js/comments-ie-conditional.min | 2 - t/js/comments.js | 17 - t/js/comments.min | 3 - t/js/division-of-array-subscripts.js | 10 - t/js/division-of-array-subscripts.min | 2 - t/js/division.js | 2 - t/js/division.min | 1 - t/js/es6-templates.js | 10 - t/js/es6-templates.min | 7 - t/js/leading-whitespace.js | 4 - t/js/leading-whitespace.min | 1 - t/js/literals-double-quotes.js | 2 - t/js/literals-double-quotes.min | 1 - t/js/literals-regexp.js | 2 - t/js/literals-regexp.min | 1 - t/js/literals-single-quotes.js | 2 - t/js/literals-single-quotes.min | 1 - t/js/postfix-sigil.js | 8 - t/js/postfix-sigil.min | 2 - t/js/prefix-sigil.js | 4 - t/js/prefix-sigil.min | 1 - t/js/regexp-not-line-comment.js | 4 - t/js/regexp-not-line-comment.min | 1 - t/js/return-regex.js | 4 - t/js/return-regex.min | 1 - t/js/simple.js | 3 - t/js/simple.min | 1 - t/js/trailing-whitespace.js | 4 - t/js/trailing-whitespace.min | 1 - t/minify.t | 360 ++++++++++++++++++ xt/author/benchmark.t | 85 +++++ .../author/clean-namespaces.t | 8 - xt/author/distmeta.t | 6 + t/author-eof.t => xt/author/eof.t | 8 - t/author-eol.t => xt/author/eol.t | 14 +- xt/author/leaks.t | 35 ++ xt/author/minimum-version.t | 6 + .../author/no-breakpoints.t | 8 - t/author-no-tabs.t => xt/author/no-tabs.t | 14 +- .../author/pod-coverage.t | 8 - t/author-pod-spell.t => xt/author/pod-spell.t | 8 - .../author/pod-syntax.t | 8 - xt/author/synopsis.t | 5 + .../author/test-compile.t | 39 +- t/release-kwalitee.t => xt/release/kwalitee.t | 8 - .../release/unused-vars.t | 8 - 69 files changed, 728 insertions(+), 663 deletions(-) delete mode 100644 cpanfile rename t/{author-compile.t => 01-compile.t} (86%) delete mode 100644 t/01-loads.t delete mode 100644 t/02-minify.t delete mode 100644 t/03-minifies-to-nothing.t delete mode 100644 t/04-not-javascript.t delete mode 100644 t/author-benchmark.t delete mode 100644 t/author-distmeta.t delete mode 100644 t/author-leaks.t delete mode 100644 t/author-minimum-version.t delete mode 100644 t/author-synopsis.t delete mode 100644 t/js/comments-before-a-regex.js delete mode 100644 t/js/comments-before-a-regex.min delete mode 100644 t/js/comments-ie-conditional.js delete mode 100644 t/js/comments-ie-conditional.min delete mode 100644 t/js/comments.js delete mode 100644 t/js/comments.min delete mode 100644 t/js/division-of-array-subscripts.js delete mode 100644 t/js/division-of-array-subscripts.min delete mode 100644 t/js/division.js delete mode 100644 t/js/division.min delete mode 100644 t/js/es6-templates.js delete mode 100644 t/js/es6-templates.min delete mode 100644 t/js/leading-whitespace.js delete mode 100644 t/js/leading-whitespace.min delete mode 100644 t/js/literals-double-quotes.js delete mode 100644 t/js/literals-double-quotes.min delete mode 100644 t/js/literals-regexp.js delete mode 100644 t/js/literals-regexp.min delete mode 100644 t/js/literals-single-quotes.js delete mode 100644 t/js/literals-single-quotes.min delete mode 100644 t/js/postfix-sigil.js delete mode 100644 t/js/postfix-sigil.min delete mode 100644 t/js/prefix-sigil.js delete mode 100644 t/js/prefix-sigil.min delete mode 100644 t/js/regexp-not-line-comment.js delete mode 100644 t/js/regexp-not-line-comment.min delete mode 100644 t/js/return-regex.js delete mode 100644 t/js/return-regex.min delete mode 100644 t/js/simple.js delete mode 100644 t/js/simple.min delete mode 100644 t/js/trailing-whitespace.js delete mode 100644 t/js/trailing-whitespace.min create mode 100755 t/minify.t create mode 100644 xt/author/benchmark.t rename t/author-clean-namespaces.t => xt/author/clean-namespaces.t (64%) create mode 100644 xt/author/distmeta.t rename t/author-eof.t => xt/author/eof.t (68%) rename t/author-eol.t => xt/author/eol.t (58%) create mode 100644 xt/author/leaks.t create mode 100644 xt/author/minimum-version.t rename t/author-no-breakpoints.t => xt/author/no-breakpoints.t (61%) rename t/author-no-tabs.t => xt/author/no-tabs.t (55%) rename t/author-pod-coverage.t => xt/author/pod-coverage.t (62%) rename t/author-pod-spell.t => xt/author/pod-spell.t (68%) rename t/author-pod-syntax.t => xt/author/pod-syntax.t (56%) create mode 100644 xt/author/synopsis.t rename t/author-test-compile.t => xt/author/test-compile.t (76%) rename t/release-kwalitee.t => xt/release/kwalitee.t (58%) rename t/release-unused-vars.t => xt/release/unused-vars.t (64%) diff --git a/Changes b/Changes index b848fd2..0e37d8d 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,15 @@ Revision history for Perl extension JavaScript::Minifier::XS. +0.14 2021-02-06 23:36:36-08:00 America/Vancouver + - rewrote test suite into a single ".t" test + - optimized memory allocations, by allocating Nodes in bulk, and being + smarter about when we need to free/reallocate content buffers in Nodes + - optimize whitespace collapsing + - GH#3 / RT#108682; fix whitespace reduction at end of preserved line + comment. Thanks to Dan Goodliffe + - GH#6; fix unescaped slash in character set, inside of a regex, with thanks + to @faf + 0.13 2020-12-30 21:46:29-08:00 America/Vancouver - POD cleanups; spelling, SYNOPSIS - Switch to DZil Author Bundle diff --git a/LICENSE b/LICENSE index c9328d6..b628f59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -This software is copyright (c) 2020 by Graham TerMarsch. +This software is copyright (c) 2021 by Graham TerMarsch. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. @@ -12,7 +12,7 @@ b) the "Artistic License" --- The GNU General Public License, Version 1, February 1989 --- -This software is Copyright (c) 2020 by Graham TerMarsch. +This software is Copyright (c) 2021 by Graham TerMarsch. This is free software, licensed under: @@ -272,7 +272,7 @@ That's all there is to it! --- The Artistic License 1.0 --- -This software is Copyright (c) 2020 by Graham TerMarsch. +This software is Copyright (c) 2021 by Graham TerMarsch. This is free software, licensed under: diff --git a/MANIFEST b/MANIFEST index 3be1b89..a3e6249 100644 --- a/MANIFEST +++ b/MANIFEST @@ -7,60 +7,24 @@ META.yml Makefile.PL README XS.xs -cpanfile lib/JavaScript/Minifier/XS.pm t/00-report-prereqs.dd t/00-report-prereqs.t -t/01-loads.t -t/02-minify.t -t/03-minifies-to-nothing.t -t/04-not-javascript.t -t/author-benchmark.t -t/author-clean-namespaces.t -t/author-compile.t -t/author-distmeta.t -t/author-eof.t -t/author-eol.t -t/author-leaks.t -t/author-minimum-version.t -t/author-no-breakpoints.t -t/author-no-tabs.t -t/author-pod-coverage.t -t/author-pod-spell.t -t/author-pod-syntax.t -t/author-synopsis.t -t/author-test-compile.t -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/comments.js -t/js/comments.min -t/js/division-of-array-subscripts.js -t/js/division-of-array-subscripts.min -t/js/division.js -t/js/division.min -t/js/es6-templates.js -t/js/es6-templates.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 -t/release-kwalitee.t -t/release-unused-vars.t +t/01-compile.t +t/minify.t +xt/author/benchmark.t +xt/author/clean-namespaces.t +xt/author/distmeta.t +xt/author/eof.t +xt/author/eol.t +xt/author/leaks.t +xt/author/minimum-version.t +xt/author/no-breakpoints.t +xt/author/no-tabs.t +xt/author/pod-coverage.t +xt/author/pod-spell.t +xt/author/pod-syntax.t +xt/author/synopsis.t +xt/author/test-compile.t +xt/release/kwalitee.t +xt/release/unused-vars.t diff --git a/META.json b/META.json index 3ac67ad..7d10b7d 100644 --- a/META.json +++ b/META.json @@ -28,15 +28,12 @@ "develop" : { "requires" : { "Dist::Zilla" : "5", - "Dist::Zilla::Plugin::FileFinder::ByName" : "0", "Dist::Zilla::PluginBundle::Author::GTERMARS" : "0", "File::Slurp" : "0", - "File::Spec" : "0", - "File::Temp" : "0", - "IO::Handle" : "0", - "IPC::Open3" : "0", + "File::Which" : "0", "IPC::Run" : "0", "JavaScript::Minifier" : "0", + "Number::Format" : "0", "Pod::Coverage::TrustPod" : "0", "Software::License::Perl_5" : "0", "Test::CPAN::Meta" : "0", @@ -46,7 +43,7 @@ "Test::Kwalitee" : "1.21", "Test::LeakTrace" : "0", "Test::MinimumVersion" : "0", - "Test::More" : "0.94", + "Test::More" : "0.88", "Test::NoBreakpoints" : "0.15", "Test::NoTabs" : "0", "Test::Pod" : "1.41", @@ -67,6 +64,9 @@ "requires" : { "ExtUtils::MakeMaker" : "0", "File::Spec" : "0", + "File::Temp" : "0", + "IO::Handle" : "0", + "IPC::Open3" : "0", "Test::DiagINC" : "0.002", "Test::More" : "0.96" } @@ -77,14 +77,15 @@ "bugtracker" : { "web" : "https://github.com/bleargh45/JavaScript-Minifier-XS/issues" }, + "homepage" : "http://metacpan.org/release/JavaScript-Minifier-XS/", "repository" : { "type" : "git", "url" : "git://github.com/bleargh45/JavaScript-Minifier-XS.git", "web" : "https://github.com/bleargh45/JavaScript-Minifier-XS" } }, - "version" : "0.13", - "x_generated_by_perl" : "v5.32.0", + "version" : "0.14", + "x_generated_by_perl" : "v5.32.1", "x_serialization_backend" : "Cpanel::JSON::XS version 4.24", "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later", "x_static_install" : 0 diff --git a/META.yml b/META.yml index 9d4d08a..06953b6 100644 --- a/META.yml +++ b/META.yml @@ -5,6 +5,9 @@ author: build_requires: ExtUtils::MakeMaker: '0' File::Spec: '0' + File::Temp: '0' + IO::Handle: '0' + IPC::Open3: '0' Test::DiagINC: '0.002' Test::More: '0.96' configure_requires: @@ -24,9 +27,10 @@ requires: perl: '5.008001' resources: bugtracker: https://github.com/bleargh45/JavaScript-Minifier-XS/issues + homepage: http://metacpan.org/release/JavaScript-Minifier-XS/ repository: git://github.com/bleargh45/JavaScript-Minifier-XS.git -version: '0.13' -x_generated_by_perl: v5.32.0 +version: '0.14' +x_generated_by_perl: v5.32.1 x_serialization_backend: 'YAML::Tiny version 1.73' x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later' x_static_install: 0 diff --git a/Makefile.PL b/Makefile.PL index f195890..3a3161c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -20,10 +20,13 @@ my %WriteMakefileArgs = ( "TEST_REQUIRES" => { "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, + "File::Temp" => 0, + "IO::Handle" => 0, + "IPC::Open3" => 0, "Test::DiagINC" => "0.002", "Test::More" => "0.96" }, - "VERSION" => "0.13", + "VERSION" => "0.14", "test" => { "TESTS" => "t/*.t" } @@ -33,6 +36,9 @@ my %WriteMakefileArgs = ( my %FallbackPrereqs = ( "ExtUtils::MakeMaker" => 0, "File::Spec" => 0, + "File::Temp" => 0, + "IO::Handle" => 0, + "IPC::Open3" => 0, "Test::DiagINC" => "0.002", "Test::More" => "0.96" ); diff --git a/XS.xs b/XS.xs index 64500a5..56e22ee 100644 --- a/XS.xs +++ b/XS.xs @@ -14,21 +14,21 @@ * CHARACTER CLASS METHODS * **************************************************************************** */ -int charIsSpace(char ch) { +bool charIsSpace(char ch) { if (ch == ' ') return 1; if (ch == '\t') return 1; return 0; } -int charIsEndspace(char ch) { +bool charIsEndspace(char ch) { if (ch == '\n') return 1; if (ch == '\r') return 1; if (ch == '\f') return 1; return 0; } -int charIsWhitespace(char ch) { +bool charIsWhitespace(char ch) { return charIsSpace(ch) || charIsEndspace(ch); } -int charIsIdentifier(char ch) { +bool charIsIdentifier(char ch) { if ((ch >= 'a') && (ch <= 'z')) return 1; if ((ch >= 'A') && (ch <= 'Z')) return 1; if ((ch >= '0') && (ch <= '9')) return 1; @@ -38,7 +38,7 @@ int charIsIdentifier(char ch) { if (ch > 126) return 1; return 0; } -int charIsInfix(char ch) { +bool charIsInfix(char ch) { /* EOL characters before+after these characters can be removed */ if (ch == ',') return 1; if (ch == ';') return 1; @@ -54,7 +54,7 @@ int charIsInfix(char ch) { if (ch == '\n') return 1; return 0; } -int charIsPrefix(char ch) { +bool charIsPrefix(char ch) { /* EOL characters after these characters can be removed */ if (ch == '{') return 1; if (ch == '(') return 1; @@ -62,7 +62,7 @@ int charIsPrefix(char ch) { if (ch == '!') return 1; return charIsInfix(ch); } -int charIsPostfix(char ch) { +bool charIsPostfix(char ch) { /* EOL characters before these characters can be removed */ if (ch == '}') return 1; if (ch == ')') return 1; @@ -107,7 +107,22 @@ struct _Node { NodeType type; }; +#define NODE_SET_SIZE 50000 + +struct _NodeSet; +typedef struct _NodeSet NodeSet; +struct _NodeSet { + /* link to next NodeSet */ + NodeSet* next; + /* Nodes in this Set */ + Node nodes[NODE_SET_SIZE]; + size_t next_node; +}; + typedef struct { + /* singly linked list of NodeSets */ + NodeSet* head_set; + NodeSet* tail_set; /* linked list pointers */ Node* head; Node* tail; @@ -124,12 +139,12 @@ typedef struct { */ /* checks to see if the node is the given string, case INSENSITIVELY */ -int nodeEquals(Node* node, const char* string) { +bool 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) { +bool nodeContains(Node* node, const char* string) { const char* haystack = node->contents; size_t len = strlen(string); char ul_start[2] = { tolower(*string), toupper(*string) }; @@ -154,9 +169,10 @@ int nodeContains(Node* node, const char* string) { /* no match */ return 0; } + /* checks to see if the node begins with the given string, case INSENSITIVELY */ -int nodeBeginsWith(Node* node, const char* string) { +bool nodeBeginsWith(Node* node, const char* string) { size_t len = strlen(string); if (len > node->length) return 0; @@ -164,7 +180,7 @@ int nodeBeginsWith(Node* node, const char* string) { } /* checks to see if the node ends with the given string, case INSENSITIVELY */ -int nodeEndsWith(Node* node, const char* string) { +bool nodeEndsWith(Node* node, const char* string) { size_t len = strlen(string); size_t off = node->length - len; if (len > node->length) @@ -195,9 +211,24 @@ int nodeEndsWith(Node* node, const char* string) { * **************************************************************************** */ /* allocates a new node */ -Node* JsAllocNode() { +Node* JsAllocNode(JsDoc* doc) { Node* node; - Newz(0, node, 1, Node); + NodeSet* set = doc->tail_set; + + /* if our current NodeSet is full, allocate a new NodeSet */ + if (set->next_node >= NODE_SET_SIZE) { + NodeSet* next_set; + Newz(0, next_set, 1, NodeSet); + set->next = next_set; + doc->tail_set = next_set; + set = next_set; + } + + /* grab the next Node out of the NodeSet */ + node = set->nodes + set->next_node; + set->next_node ++; + + /* initialize the node */ node->prev = NULL; node->next = NULL; node->contents = NULL; @@ -206,20 +237,6 @@ Node* JsAllocNode() { 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) @@ -230,13 +247,20 @@ void JsClearNodeContents(Node* node) { /* 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 ); + /* if the buffer is already big enough, just overwrite it */ + if (node->length >= len) { + memcpy( node->contents, string, len ); + node->contents[len] = '\0'; + node->length = len; + } + /* otherwise free the buffer, allocate a new one, and copy it in */ + else { + JsClearNodeContents(node); + node->length = len; + /* allocate string, fill with NULLs, and copy */ + Newz(0, node->contents, (len+1), char); + memcpy( node->contents, string, len ); + } } /* removes the node from the list and discards it entirely */ @@ -245,7 +269,6 @@ void JsDiscardNode(Node* node) { node->prev->next = node->next; if (node->next) node->next->prev = node->prev; - JsFreeNode(node); } /* appends the node to the given element */ @@ -257,43 +280,14 @@ void JsAppendNode(Node* element, Node* node) { 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. - */ +/* collapses a node to a single whitespace character */ 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); + node->length = 1; + node->contents[1] = '\0'; } } -/* 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 * **************************************************************************** @@ -304,6 +298,7 @@ void _JsExtractLiteral(JsDoc* doc, Node* node) { const char* buf = doc->buffer; size_t offset = doc->offset; char delimiter = buf[offset]; + bool in_char_class = 0; /* skip start of literal */ offset ++; /* search for end of literal */ @@ -312,12 +307,24 @@ void _JsExtractLiteral(JsDoc* doc, Node* node) { /* 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; + else { + /* if in a regex, track if we're in a character class */ + if (delimiter == '/') { + if ((buf[offset] == '[') && !in_char_class) { + in_char_class = 1; + } + if ((buf[offset] == ']') && in_char_class) { + in_char_class = 0; + } + } + /* if we have found the end of the literal, store it */ + if ((buf[offset] == delimiter) && !in_char_class) { + 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 ++; @@ -401,36 +408,27 @@ void _JsExtractSigil(JsDoc* doc, Node* node) { } /* 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; - +Node* JsTokenizeString(JsDoc* doc, const char* string) { /* parse the JS */ - while ((doc.offset < doc.length) && (doc.buffer[doc.offset])) { + 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; + Node* node = JsAllocNode(doc); + 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); + 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; + Node* last = doc->tail; char ch = 0; /* find last non-whitespace, non-comment node */ @@ -442,54 +440,54 @@ Node* JsTokenizeString(const char* string) { /* see if we're "division" or "regexp" */ if (nodeIsIDENTIFIER(last) && nodeEquals(last, "return")) { /* returning a regexp from a function */ - _JsExtractLiteral(&doc, node); + _JsExtractLiteral(doc, node); } else if (ch && ((ch == ')') || (ch == '.') || (ch == ']') || (charIsIdentifier(ch)))) { /* looks like an identifier; guess its division */ - _JsExtractSigil(&doc, node); + _JsExtractSigil(doc, node); } else { /* presume its a regexp */ - _JsExtractLiteral(&doc, node); + _JsExtractLiteral(doc, node); } } } - else if ((doc.buffer[doc.offset] == '"') || (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 if ((doc->buffer[doc->offset] == '"') || (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); + _JsExtractSigil(doc, node); /* move ahead to the end of the parsed node */ - doc.offset += node->length; + doc->offset += node->length; /* add the node to our list of nodes */ - if (node != doc.tail) - JsAppendNode(doc.tail, node); - doc.tail = node; + 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: '"); + 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]); + if ((doc->offset+idx) >= doc->length) break; + if (!doc->buffer[doc->offset+idx]) break; + printf("%c", doc->buffer[doc->offset+idx]); } - printf("'\n"); + printf("]\n"); } #endif } /* return the node list */ - return doc.head; + return doc->head; } /* **************************************************************************** @@ -507,11 +505,15 @@ void JsCollapseNodes(Node* curr) { JsCollapseNodeToWhitespace(curr); break; case NODE_BLOCKCOMMENT: + /* IE Conditional Compilation comments do not get collapsed */ + if (nodeIsIECONDITIONALBLOCKCOMMENT(curr)) { + break; + } /* 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; + { + bool convert_to_ws = 0; /* find surrounding non-WS nodes */ Node* nonws_prev = curr->prev; Node* nonws_next = curr->next; @@ -610,7 +612,7 @@ int JsCanPrune(Node* node) { if (nodeIsPREFIXSIGIL(node) && next && nodeIsWHITESPACE(next)) return PRUNE_NEXT; /* remove whitespace before "postfix" sigils */ - if (nodeIsPOSTFIXSIGIL(node) && prev && nodeIsWHITESPACE(prev)) + if (nodeIsPOSTFIXSIGIL(node) && prev && nodeIsWHITESPACE(prev) && prev->prev && !nodeIsLINECOMMENT(prev->prev)) return PRUNE_PREVIOUS; /* remove whitespace (but NOT endspace) after closing brackets */ if (next && nodeIsWHITESPACE(next) && !nodeIsENDSPACE(next) && (nodeIsCHAR(node,')') || nodeIsCHAR(node,'}') || nodeIsCHAR(node,']'))) @@ -686,8 +688,19 @@ Node* JsPruneNodes(Node *head) { */ char* JsMinify(const char* string) { char* results; + JsDoc doc; + + /* initialize our JS document object */ + doc.head = NULL; + doc.tail = NULL; + doc.buffer = string; + doc.length = strlen(string); + doc.offset = 0; + Newz(0, doc.head_set, 1, NodeSet); + doc.tail_set = doc.head_set; + /* PASS 1: tokenize JS into a list of nodes */ - Node* head = JsTokenizeString(string); + Node* head = JsTokenizeString(&doc, string); if (!head) return NULL; /* PASS 2: collapse nodes */ JsCollapseNodes(head); @@ -712,8 +725,15 @@ char* JsMinify(const char* string) { } *ptr = 0; } - /* free memory used by node list */ - JsFreeNodeList(head); + /* free memory used by the NodeSets */ + { + NodeSet* curr = doc.head_set; + while (curr) { + NodeSet* next = curr->next; + Safefree(curr); + curr = next; + } + } /* return resulting minified JS back to caller */ return results; } diff --git a/cpanfile b/cpanfile deleted file mode 100644 index ee1bd36..0000000 --- a/cpanfile +++ /dev/null @@ -1,8 +0,0 @@ -requires 'perl', '>= 5.008001'; - -test_requires 'Test::More', '>= 0.96'; - -author_requires 'File::Slurp'; -author_requires 'IPC::Run'; -author_requires 'JavaScript::Minifier'; -author_requires 'Test::LeakTrace'; diff --git a/lib/JavaScript/Minifier/XS.pm b/lib/JavaScript/Minifier/XS.pm index 6ba0296..fa20c63 100644 --- a/lib/JavaScript/Minifier/XS.pm +++ b/lib/JavaScript/Minifier/XS.pm @@ -9,7 +9,7 @@ require DynaLoader; our @ISA = qw(Exporter DynaLoader); our @EXPORT_OK = qw(minify); -our $VERSION = '0.13'; +our $VERSION = '0.14'; bootstrap JavaScript::Minifier::XS $VERSION; diff --git a/t/00-report-prereqs.dd b/t/00-report-prereqs.dd index 7fcf7d7..a13275f 100644 --- a/t/00-report-prereqs.dd +++ b/t/00-report-prereqs.dd @@ -7,15 +7,12 @@ do { my $x = { 'develop' => { 'requires' => { 'Dist::Zilla' => '5', - 'Dist::Zilla::Plugin::FileFinder::ByName' => '0', 'Dist::Zilla::PluginBundle::Author::GTERMARS' => '0', 'File::Slurp' => '0', - 'File::Spec' => '0', - 'File::Temp' => '0', - 'IO::Handle' => '0', - 'IPC::Open3' => '0', + 'File::Which' => '0', 'IPC::Run' => '0', 'JavaScript::Minifier' => '0', + 'Number::Format' => '0', 'Pod::Coverage::TrustPod' => '0', 'Software::License::Perl_5' => '0', 'Test::CPAN::Meta' => '0', @@ -25,7 +22,7 @@ do { my $x = { 'Test::Kwalitee' => '1.21', 'Test::LeakTrace' => '0', 'Test::MinimumVersion' => '0', - 'Test::More' => '0.94', + 'Test::More' => '0.88', 'Test::NoBreakpoints' => '0.15', 'Test::NoTabs' => '0', 'Test::Pod' => '1.41', @@ -46,6 +43,9 @@ do { my $x = { 'requires' => { 'ExtUtils::MakeMaker' => '0', 'File::Spec' => '0', + 'File::Temp' => '0', + 'IO::Handle' => '0', + 'IPC::Open3' => '0', 'Test::DiagINC' => '0.002', 'Test::More' => '0.96' } diff --git a/t/author-compile.t b/t/01-compile.t similarity index 86% rename from t/author-compile.t rename to t/01-compile.t index c489ddf..5d9ab1f 100644 --- a/t/author-compile.t +++ b/t/01-compile.t @@ -1,20 +1,12 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use 5.006; use strict; use warnings; # this test was generated with Dist::Zilla::Plugin::Test::Compile 2.058 -use Test::More 0.94; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; use Test::More 0.94; -plan tests => 2; +plan tests => 1 + ($ENV{AUTHOR_TESTING} ? 1 : 0); my @module_files = ( 'JavaScript/Minifier/XS.pm' @@ -66,6 +58,6 @@ for my $lib (@module_files) is(scalar(@warnings), 0, 'no warnings found') - or diag 'got warnings: ', explain(\@warnings); + or diag 'got warnings: ', explain(\@warnings) if $ENV{AUTHOR_TESTING}; BAIL_OUT("Compilation problems") if !Test::More->builder->is_passing; diff --git a/t/01-loads.t b/t/01-loads.t deleted file mode 100644 index 5e95bc4..0000000 --- a/t/01-loads.t +++ /dev/null @@ -1,5 +0,0 @@ -use strict; -use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; use Test::More tests=>1; -BEGIN { - use_ok( 'JavaScript::Minifier::XS' ); -} diff --git a/t/02-minify.t b/t/02-minify.t deleted file mode 100644 index 46b80e8..0000000 --- a/t/02-minify.t +++ /dev/null @@ -1,37 +0,0 @@ -use strict; -use warnings; -use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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; -} diff --git a/t/03-minifies-to-nothing.t b/t/03-minifies-to-nothing.t deleted file mode 100644 index 84e95cc..0000000 --- a/t/03-minifies-to-nothing.t +++ /dev/null @@ -1,20 +0,0 @@ -use strict; -use warnings; -use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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" ); diff --git a/t/04-not-javascript.t b/t/04-not-javascript.t deleted file mode 100644 index 891c3dd..0000000 --- a/t/04-not-javascript.t +++ /dev/null @@ -1,16 +0,0 @@ -use strict; -use warnings; -use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; 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(); diff --git a/t/author-benchmark.t b/t/author-benchmark.t deleted file mode 100644 index ab73ad4..0000000 --- a/t/author-benchmark.t +++ /dev/null @@ -1,63 +0,0 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - -use strict; -use warnings; -use Test::More; -use File::Slurp qw(slurp); -use Benchmark qw(countit); -use JavaScript::Minifier::XS; - -############################################################################### -# Only run Benchmark if asked for. -unless ($ENV{BENCHMARK}) { - plan skip_all => 'Skipping Benchmark; use BENCHMARK=1 to run'; -} - -############################################################################### -# 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'; -} diff --git a/t/author-distmeta.t b/t/author-distmeta.t deleted file mode 100644 index 8d9ce71..0000000 --- a/t/author-distmeta.t +++ /dev/null @@ -1,14 +0,0 @@ -#!perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - -# This file was automatically generated by Dist::Zilla::Plugin::MetaTests. - -use Test::CPAN::Meta; - -meta_yaml_ok(); diff --git a/t/author-leaks.t b/t/author-leaks.t deleted file mode 100644 index 2da06b1..0000000 --- a/t/author-leaks.t +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - - -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'; diff --git a/t/author-minimum-version.t b/t/author-minimum-version.t deleted file mode 100644 index db391de..0000000 --- a/t/author-minimum-version.t +++ /dev/null @@ -1,14 +0,0 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - -use strict; -use warnings; - -use Test::More; -use Test::MinimumVersion; -all_minimum_version_from_metayml_ok(); diff --git a/t/author-synopsis.t b/t/author-synopsis.t deleted file mode 100644 index 5d1d4a7..0000000 --- a/t/author-synopsis.t +++ /dev/null @@ -1,13 +0,0 @@ -#!perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - - -use Test::Synopsis; - -all_synopsis_ok(); diff --git a/t/js/comments-before-a-regex.js b/t/js/comments-before-a-regex.js deleted file mode 100644 index f8476a1..0000000 --- a/t/js/comments-before-a-regex.js +++ /dev/null @@ -1,18 +0,0 @@ -/* 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/ - ]; diff --git a/t/js/comments-before-a-regex.min b/t/js/comments-before-a-regex.min deleted file mode 100644 index 1ceb219..0000000 --- a/t/js/comments-before-a-regex.min +++ /dev/null @@ -1 +0,0 @@ -var foo=[/^'/,/^"/,/foo/]; diff --git a/t/js/comments-ie-conditional.js b/t/js/comments-ie-conditional.js deleted file mode 100644 index aacf833..0000000 --- a/t/js/comments-ie-conditional.js +++ /dev/null @@ -1,7 +0,0 @@ -/* 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 diff --git a/t/js/comments-ie-conditional.min b/t/js/comments-ie-conditional.min deleted file mode 100644 index 824c6cf..0000000 --- a/t/js/comments-ie-conditional.min +++ /dev/null @@ -1,2 +0,0 @@ -/*@ except those that are "IE Conditional Compilation" comments @*/ -//@ except those that are "IE Conditional Compilation" line comments diff --git a/t/js/comments.js b/t/js/comments.js deleted file mode 100644 index 7247866..0000000 --- a/t/js/comments.js +++ /dev/null @@ -1,17 +0,0 @@ -/* 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; diff --git a/t/js/comments.min b/t/js/comments.min deleted file mode 100644 index 5be3fa4..0000000 --- a/t/js/comments.min +++ /dev/null @@ -1,3 +0,0 @@ -/* 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; diff --git a/t/js/division-of-array-subscripts.js b/t/js/division-of-array-subscripts.js deleted file mode 100644 index 42ba9fc..0000000 --- a/t/js/division-of-array-subscripts.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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 -} diff --git a/t/js/division-of-array-subscripts.min b/t/js/division-of-array-subscripts.min deleted file mode 100644 index b70060d..0000000 --- a/t/js/division-of-array-subscripts.min +++ /dev/null @@ -1,2 +0,0 @@ -function foo(){var bar=someArray[2]/2;} -function bar(){foo();} diff --git a/t/js/division.js b/t/js/division.js deleted file mode 100644 index ea6d368..0000000 --- a/t/js/division.js +++ /dev/null @@ -1,2 +0,0 @@ -/* use of "/" for division should get compacted */ -var foo = 10 / 2; diff --git a/t/js/division.min b/t/js/division.min deleted file mode 100644 index 6993e69..0000000 --- a/t/js/division.min +++ /dev/null @@ -1 +0,0 @@ -var foo=10/2; diff --git a/t/js/es6-templates.js b/t/js/es6-templates.js deleted file mode 100644 index b696e3e..0000000 --- a/t/js/es6-templates.js +++ /dev/null @@ -1,10 +0,0 @@ - -let vat = $(`#items-${i}-vat_id :selected`); - -let html = ` -
- - - -
-`; diff --git a/t/js/es6-templates.min b/t/js/es6-templates.min deleted file mode 100644 index 0210b9a..0000000 --- a/t/js/es6-templates.min +++ /dev/null @@ -1,7 +0,0 @@ -let vat=$(`#items-${i}-vat_id :selected`);let html=` -
- - - -
-`; diff --git a/t/js/leading-whitespace.js b/t/js/leading-whitespace.js deleted file mode 100644 index 17d3958..0000000 --- a/t/js/leading-whitespace.js +++ /dev/null @@ -1,4 +0,0 @@ - - - -var leading="leading whitespace gets removed"; diff --git a/t/js/leading-whitespace.min b/t/js/leading-whitespace.min deleted file mode 100644 index dfd5414..0000000 --- a/t/js/leading-whitespace.min +++ /dev/null @@ -1 +0,0 @@ -var leading="leading whitespace gets removed"; diff --git a/t/js/literals-double-quotes.js b/t/js/literals-double-quotes.js deleted file mode 100644 index ad414d9..0000000 --- a/t/js/literals-double-quotes.js +++ /dev/null @@ -1,2 +0,0 @@ -/* quoted literals get preserved, in several forms */ -var double_quoted=" double quoted strings /* with block comments */ "; diff --git a/t/js/literals-double-quotes.min b/t/js/literals-double-quotes.min deleted file mode 100644 index 650e0f6..0000000 --- a/t/js/literals-double-quotes.min +++ /dev/null @@ -1 +0,0 @@ -var double_quoted=" double quoted strings /* with block comments */ "; diff --git a/t/js/literals-regexp.js b/t/js/literals-regexp.js deleted file mode 100644 index 44891cb..0000000 --- a/t/js/literals-regexp.js +++ /dev/null @@ -1,2 +0,0 @@ -/* quoted literals get preserved, in several forms */ -var regexes=/ regexes stay /; diff --git a/t/js/literals-regexp.min b/t/js/literals-regexp.min deleted file mode 100644 index f6a1758..0000000 --- a/t/js/literals-regexp.min +++ /dev/null @@ -1 +0,0 @@ -var regexes=/ regexes stay /; diff --git a/t/js/literals-single-quotes.js b/t/js/literals-single-quotes.js deleted file mode 100644 index 49d477f..0000000 --- a/t/js/literals-single-quotes.js +++ /dev/null @@ -1,2 +0,0 @@ -/* quoted literals get preserved, in several forms */ -var single_quoted=' single quoted strings // with line comments '; diff --git a/t/js/literals-single-quotes.min b/t/js/literals-single-quotes.min deleted file mode 100644 index c07e717..0000000 --- a/t/js/literals-single-quotes.min +++ /dev/null @@ -1 +0,0 @@ -var single_quoted=' single quoted strings // with line comments '; diff --git a/t/js/postfix-sigil.js b/t/js/postfix-sigil.js deleted file mode 100644 index 044cbf5..0000000 --- a/t/js/postfix-sigil.js +++ /dev/null @@ -1,8 +0,0 @@ -/* whitespace before "postfix" sigils should get removed */ -function foo( ) - -{ - alert("foo!") - ; - -} diff --git a/t/js/postfix-sigil.min b/t/js/postfix-sigil.min deleted file mode 100644 index 6d2baad..0000000 --- a/t/js/postfix-sigil.min +++ /dev/null @@ -1,2 +0,0 @@ -function foo() -{alert("foo!");} diff --git a/t/js/prefix-sigil.js b/t/js/prefix-sigil.js deleted file mode 100644 index 21f8301..0000000 --- a/t/js/prefix-sigil.js +++ /dev/null @@ -1,4 +0,0 @@ -/* whitespace after "prefix" sigils should get removed */ -function foo( ){ - alert("foo!"); -} diff --git a/t/js/prefix-sigil.min b/t/js/prefix-sigil.min deleted file mode 100644 index 5ff7575..0000000 --- a/t/js/prefix-sigil.min +++ /dev/null @@ -1 +0,0 @@ -function foo(){alert("foo!");} diff --git a/t/js/regexp-not-line-comment.js b/t/js/regexp-not-line-comment.js deleted file mode 100644 index 0555440..0000000 --- a/t/js/regexp-not-line-comment.js +++ /dev/null @@ -1,4 +0,0 @@ -/* RT#80598; regexps containing an escaped "/" should not be treated as comments */ -function foo(url) { - return ( /\// ).test( url ); -} diff --git a/t/js/regexp-not-line-comment.min b/t/js/regexp-not-line-comment.min deleted file mode 100644 index a8b1813..0000000 --- a/t/js/regexp-not-line-comment.min +++ /dev/null @@ -1 +0,0 @@ -function foo(url){return(/\//).test(url);} diff --git a/t/js/return-regex.js b/t/js/return-regex.js deleted file mode 100644 index d1d0895..0000000 --- a/t/js/return-regex.js +++ /dev/null @@ -1,4 +0,0 @@ -/* should be able to return a regex from a function */ -function foo() { - return /\d{1,2}/; -} diff --git a/t/js/return-regex.min b/t/js/return-regex.min deleted file mode 100644 index 7991bd6..0000000 --- a/t/js/return-regex.min +++ /dev/null @@ -1 +0,0 @@ -function foo(){return/\d{1,2}/;} diff --git a/t/js/simple.js b/t/js/simple.js deleted file mode 100644 index b3e75b4..0000000 --- a/t/js/simple.js +++ /dev/null @@ -1,3 +0,0 @@ -/* foo */ - -var x = 2; diff --git a/t/js/simple.min b/t/js/simple.min deleted file mode 100644 index 73d8752..0000000 --- a/t/js/simple.min +++ /dev/null @@ -1 +0,0 @@ -var x=2; diff --git a/t/js/trailing-whitespace.js b/t/js/trailing-whitespace.js deleted file mode 100644 index 279a302..0000000 --- a/t/js/trailing-whitespace.js +++ /dev/null @@ -1,4 +0,0 @@ -var trailing="trailing whitespace gets removed"; - - - diff --git a/t/js/trailing-whitespace.min b/t/js/trailing-whitespace.min deleted file mode 100644 index 52bba32..0000000 --- a/t/js/trailing-whitespace.min +++ /dev/null @@ -1 +0,0 @@ -var trailing="trailing whitespace gets removed"; diff --git a/t/minify.t b/t/minify.t new file mode 100755 index 0000000..b5e772c --- /dev/null +++ b/t/minify.t @@ -0,0 +1,360 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use if $ENV{AUTOMATED_TESTING}, 'Test::DiagINC'; use Test::More; +use JavaScript::Minifier::XS qw(minify); + +############################################################################### +subtest 'leading whitespace can be removed' => sub { + my $given = qq{\n\n\r\t\n \n var leading="leading whitespace gets removed";}; + my $expect = qq{var leading="leading whitespace gets removed";}; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'trailing whitespace can be removed' => sub { + my $given = qq{var trailing="trailing whitespace gets removed"; \t\n\r \n}; + my $expect =qq{var trailing="trailing whitespace gets removed";}; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'comments' => sub {; + subtest 'block comments' => sub { + my $given = ";/* block comments get removed */;"; + my $expect = ";;"; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'line comments' => sub { + my $given = ";// line comments get removed\n;"; + my $expect = ";;"; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'copyright in block comment' => sub { + my $given = qq{/* comments containing the word "Copyright" are left in */}; + my $expect = qq{/* comments containing the word "Copyright" are left in */}; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'copyright in line comment' => sub { + my $given = qq{// line comments with "CoPyRiGhT", case in-sensitive}; + my $expect = qq{// line comments with "CoPyRiGhT", case in-sensitive}; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'preserved copyright line comment gets EOL' => sub { # GH#3 + my $given = q| + function foo() { + // copyright is preserved + } + |; + my $expect = qq|function foo(){// copyright is preserved\n}|; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'inline block comment' => sub { + my $given = 'var foo /* remove */ = /* me too */ 3;'; + my $expect = 'var foo=3;'; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'inline block comment' => sub { + my $given = 'var bar = /* and me */ 4;'; + my $expect = 'var bar=4;'; + my $got = minify($given); + is $got, $expect; + }; + + + subtest 'inline block comment' => sub { + my $given = 'var replaced_with_ws = foo + /* ws */ +bar;'; + my $expect = 'var replaced_with_ws=foo+ +bar;'; + my $got = minify($given); + is $got, $expect; + }; + + + subtest 'inline block comment' => sub { + my $given = 'var also_replaced = foo - /* ws */ -bar;'; + my $expect = 'var also_replaced=foo- -bar;'; + my $got = minify($given); + is $got, $expect; + }; + + + subtest 'inline block comment' => sub { + my $given = 'var removed_outright = foo + /* me gone */ -bar;'; + my $expect = 'var removed_outright=foo+-bar;'; + my $got = minify($given); + is $got, $expect; + }; + + + subtest 'inline block comment' => sub { + my $given = 'var also_removed = foo - /* me gone */ +bar;'; + my $expect = 'var also_removed=foo-+bar;'; + my $got = minify($given); + is $got, $expect; + }; +}; + +############################################################################### +# 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. +subtest 'comments before a regex' => sub { + my $given = qq{ + 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/ + ]; + }; + my $expect = qq{var foo=[/^'/,/^"/,/foo/];}; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'MSIE conditional compilation comments' => sub { + my $given = q{ + /* 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 @*/ + }; + my $expect = q{/*@ except those that are "IE Conditional Compilation" comments @*/}; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# whitespace after "prefix" sigils should get removed +subtest 'prefix sigils' => sub { + my $given = q| + function foo( ){ + alert("foo!"); + } + |; + my $expect = q|function foo(){alert("foo!");}|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# whitespace before "postfix" sigils should get removed +subtest 'postfix sigils' => sub { + my $given = q| + function foo( ) + + { + alert("foo!") + ; + + } + |; + my $expect = qq|function foo()\n{alert("foo!");}|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'simple' => sub { + my $given = q| + /* foo */ + + var x = 2; + |; + my $expect = q|var x=2;|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# quoted literals get preserved, in several forms +subtest 'literals' => sub { + subtest 'single quoted literal' => sub { + my $given = q{var single_quoted=' single quoted strings // with line comments ';}; + my $expect = q{var single_quoted=' single quoted strings // with line comments ';}; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'double quoted literal' => sub { + my $given = q{var double_quoted=" double quoted strings /* with block comments */ ";}; + my $expect = q{var double_quoted=" double quoted strings /* with block comments */ ";}; + my $got = minify($given); + is $got, $expect; + }; + + subtest 'regexp literal' => sub { + my $given = q{var regexes=/ regexes stay /;}; + my $expect = q{var regexes=/ regexes stay /;}; + my $got = minify($given); + is $got, $expect; + }; +}; + +############################################################################### +# use of "/" for division should get compacted +subtest 'division' => sub { + my $given = "var foo = 10 / 2;"; + my $expect = "var foo=10/2;"; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# RT#80598; regexps containing an escaped "/" should not be treated as comments +subtest 'regexp escaped "/" is not a comment' => sub { + my $given = q| + function foo(url) { + return ( /\// ).test( url ); + } + |; + my $expect = q|function foo(url){return(/\//).test(url);}|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# GH#6; regexps can contain an unescaped slash inside a character set +subtest 'regexp with unescaped slash in character set' => sub { + my $given = q|var a = ""; var re = /[/"]/i; console.log(re.test(a));|; + my $expect = q|var a="";var re=/[/"]/i;console.log(re.test(a));|; + my $got = minify($given); + is $got, $expect; +}; + +subtest 'regexp with escaped slash in character set' => sub { + my $given = q|var a = ""; var re = /[\/"]/i; console.log(re.test(a));|; + my $expect = q|var a="";var re=/[\/"]/i;console.log(re.test(a));|; + my $got = minify($given); + is $got, $expect; +}; + +subtest 'regexp with not really a character set' => sub { + my $given = q|var a = ""; var re = /\[/i; console.log(re.test(a));|; + my $expect = q|var a="";var re=/\[/i;console.log(re.test(a));|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# should be able to return a regex from a function +subtest 'return regex from function' => sub { + my $given = q| + function foo() { + return /\d{1,2}/; + } + |; + my $expect = q|function foo(){return/\d{1,2}/;}|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# Division of an array subscript should NOT be treated as opening a regexp, but +# should be treated as division. +subtest 'division of array subscript' => sub { + my $given = q| + function foo() { + var bar = someArray[2]/2; + } + function bar() { + foo(); // this / is not a regexp close, its just part of a line comment + } + |; + my $expect = qq|function foo(){var bar=someArray[2]/2;}\nfunction bar(){foo();}|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'ES6 templates' => sub { + my $given = q| +let vat = $(`#items-${i}-vat_id :selected`); + +let html = ` +
+ + + +
+`; +|; + my $expect = q|let vat=$(`#items-${i}-vat_id :selected`);let html=` +
+ + + +
+`;|; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +# JS that minifies to "nothing" +subtest 'minifies to nothing' => sub { + subtest 'block comment' => sub { + my $given = '/* */'; + my $expect = undef; + my $got = minify($given); + is $got, $expect + }; + + subtest 'line comment' => sub { + my $given = '// foo'; + my $expect = undef; + my $got = minify($given); + is $got, $expect + }; + + subtest 'just whitespace' => sub { + my $given = " \r\n \t "; + my $expect = undef; + my $got = minify($given); + is $got, $expect + }; + + subtest 'empty string' => sub { + my $given = ''; + my $expect = undef; + my $got = minify($given); + is $got, $expect + }; +}; + +############################################################################### +# 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 $given = 'not javascript'; + my $expect = 'not javascript'; + my $got = minify($given); + is $got, $expect +}; + +############################################################################### +done_testing(); diff --git a/xt/author/benchmark.t b/xt/author/benchmark.t new file mode 100644 index 0000000..ad52775 --- /dev/null +++ b/xt/author/benchmark.t @@ -0,0 +1,85 @@ +use strict; +use warnings; +use Test::More; +use File::Basename qw(basename); +use File::Slurp qw(write_file); +use File::Which qw(which); +use Benchmark qw(countit); +use JavaScript::Minifier; +use JavaScript::Minifier::XS; +use Number::Format qw(format_bytes); + +############################################################################### +# Only run Benchmark if asked for. +unless ($ENV{BENCHMARK}) { + plan skip_all => 'Skipping Benchmark; use BENCHMARK=1 to run'; +} + +############################################################################### +# Find "curl" +my $curl = which('curl'); +unless ($curl) { + plan skip_all => 'curl required for comparison'; +} + +############################################################################### +# What JS files do we want to try compressing? +my @libs = ( + 'http://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js', + 'http://code.jquery.com/jquery-3.5.1.js', + 'http://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/cjs/react.development.js', +); + +############################################################################### +# time test the PurePerl version against the XS version. +my $time = 1; +foreach my $uri (@libs) { + subtest $uri => sub { + my $content = qx{$curl --silent $uri}; + ok defined $content, 'fetched JS'; + BAIL_OUT("No JS fetched!") unless (length($content)); + + # Run the benchmarks + do_compress('JavaScript::Minifier', $content, sub { + my $js = shift; + my $small = JavaScript::Minifier::minify(input => $js); + return $small; + } ); + + do_compress('JavaScript::Minifier::XS', $content, sub { + my $js = shift; + my $small = JavaScript::Minifier::XS::minify($js); + return $small; + } ); + }; +} + +############################################################################### +done_testing(); + + +sub do_compress { + my $name = shift; + my $js = shift; + my $cb = shift; + + # Compress the JS + my $small; + my $count = countit($time, sub { $small = $cb->($js) } ); + + # Stuff the compressed JS out to file for examination + my $fname = lc($name); + $fname =~ s{\W+}{-}g; + write_file("$fname.out", $small); + + # Calculate length, speed, and percent savings + my $before = length($js); + my $after = length($small); + my $rate = sprintf('%ld', ($count->iters / $time) * $before); + my $savings = sprintf('%0.2f%%', (($before - $after) / $before) * 100); + + my $results = sprintf("%30s before[%7d] after[%7d] savings[%6s] rate[%8s/sec]", + $name, $before, $after, $savings, format_bytes($rate, unit => 'K', precision => 0), + ); + pass $results; +} diff --git a/t/author-clean-namespaces.t b/xt/author/clean-namespaces.t similarity index 64% rename from t/author-clean-namespaces.t rename to xt/author/clean-namespaces.t index a5531c3..36387da 100644 --- a/t/author-clean-namespaces.t +++ b/xt/author/clean-namespaces.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; diff --git a/xt/author/distmeta.t b/xt/author/distmeta.t new file mode 100644 index 0000000..c2280dc --- /dev/null +++ b/xt/author/distmeta.t @@ -0,0 +1,6 @@ +#!perl +# This file was automatically generated by Dist::Zilla::Plugin::MetaTests. + +use Test::CPAN::Meta; + +meta_yaml_ok(); diff --git a/t/author-eof.t b/xt/author/eof.t similarity index 68% rename from t/author-eof.t rename to xt/author/eof.t index 0ae7431..f5f286c 100644 --- a/t/author-eof.t +++ b/xt/author/eof.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; use Test::More; diff --git a/t/author-eol.t b/xt/author/eol.t similarity index 58% rename from t/author-eol.t rename to xt/author/eol.t index 5cf10d1..16e5135 100644 --- a/t/author-eol.t +++ b/xt/author/eol.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; @@ -18,10 +10,8 @@ my @files = ( 'lib/JavaScript/Minifier/XS.pm', 't/00-report-prereqs.dd', 't/00-report-prereqs.t', - 't/01-loads.t', - 't/02-minify.t', - 't/03-minifies-to-nothing.t', - 't/04-not-javascript.t' + 't/01-compile.t', + 't/minify.t' ); eol_unix_ok($_, { trailing_whitespace => 1 }) foreach @files; diff --git a/xt/author/leaks.t b/xt/author/leaks.t new file mode 100644 index 0000000..7fcc6c8 --- /dev/null +++ b/xt/author/leaks.t @@ -0,0 +1,35 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More; +use File::Which qw(which); +use JavaScript::Minifier::XS qw(minify); + +BEGIN { + eval "use Test::LeakTrace"; + plan skip_all => "Test::LeakTrace required for leak testing" if $@; +} +use Test::LeakTrace; + +############################################################################### +## What CSS docs do we want to try compressing? +my $curl = which('curl'); +my @libs = ( + 'http://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.js', + 'http://code.jquery.com/jquery-3.5.1.js', + 'http://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/cjs/react.development.js', +); + +############################################################################### +# Make sure we're not leaking memory when we minify +foreach my $url (@libs) { + subtest $url => sub { + my $js = qx{$curl --silent $url}; + ok $js, 'got some JS to minify'; + no_leaks_ok { minify($js) } "no leaks when minifying; $url"; + }; +} + +############################################################################### +done_testing(); diff --git a/xt/author/minimum-version.t b/xt/author/minimum-version.t new file mode 100644 index 0000000..bed9e18 --- /dev/null +++ b/xt/author/minimum-version.t @@ -0,0 +1,6 @@ +use strict; +use warnings; + +use Test::More; +use Test::MinimumVersion; +all_minimum_version_from_metayml_ok(); diff --git a/t/author-no-breakpoints.t b/xt/author/no-breakpoints.t similarity index 61% rename from t/author-no-breakpoints.t rename to xt/author/no-breakpoints.t index c714119..1f4bb20 100644 --- a/t/author-no-breakpoints.t +++ b/xt/author/no-breakpoints.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; diff --git a/t/author-no-tabs.t b/xt/author/no-tabs.t similarity index 55% rename from t/author-no-tabs.t rename to xt/author/no-tabs.t index 2084705..820d3ec 100644 --- a/t/author-no-tabs.t +++ b/xt/author/no-tabs.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; @@ -18,10 +10,8 @@ my @files = ( 'lib/JavaScript/Minifier/XS.pm', 't/00-report-prereqs.dd', 't/00-report-prereqs.t', - 't/01-loads.t', - 't/02-minify.t', - 't/03-minifies-to-nothing.t', - 't/04-not-javascript.t' + 't/01-compile.t', + 't/minify.t' ); notabs_ok($_) foreach @files; diff --git a/t/author-pod-coverage.t b/xt/author/pod-coverage.t similarity index 62% rename from t/author-pod-coverage.t rename to xt/author/pod-coverage.t index 243340f..66b3b64 100644 --- a/t/author-pod-coverage.t +++ b/xt/author/pod-coverage.t @@ -1,12 +1,4 @@ #!perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - # This file was automatically generated by Dist::Zilla::Plugin::PodCoverageTests. use Test::Pod::Coverage 1.08; diff --git a/t/author-pod-spell.t b/xt/author/pod-spell.t similarity index 68% rename from t/author-pod-spell.t rename to xt/author/pod-spell.t index 14a048f..50de034 100644 --- a/t/author-pod-spell.t +++ b/xt/author/pod-spell.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - use strict; use warnings; use Test::More; diff --git a/t/author-pod-syntax.t b/xt/author/pod-syntax.t similarity index 56% rename from t/author-pod-syntax.t rename to xt/author/pod-syntax.t index 2233af0..e563e5d 100644 --- a/t/author-pod-syntax.t +++ b/xt/author/pod-syntax.t @@ -1,12 +1,4 @@ #!perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - # This file was automatically generated by Dist::Zilla::Plugin::PodSyntaxTests. use strict; use warnings; use Test::More; diff --git a/xt/author/synopsis.t b/xt/author/synopsis.t new file mode 100644 index 0000000..3e03427 --- /dev/null +++ b/xt/author/synopsis.t @@ -0,0 +1,5 @@ +#!perl + +use Test::Synopsis; + +all_synopsis_ok(); diff --git a/t/author-test-compile.t b/xt/author/test-compile.t similarity index 76% rename from t/author-test-compile.t rename to xt/author/test-compile.t index b022edb..629ab9e 100644 --- a/t/author-test-compile.t +++ b/xt/author/test-compile.t @@ -1,12 +1,4 @@ #!/usr/bin/perl - -BEGIN { - unless ($ENV{AUTHOR_TESTING}) { - print qq{1..0 # SKIP these tests are for testing by the author\n}; - exit - } -} - # # test-compile.t # @@ -49,27 +41,40 @@ foreach my $uri (@libs) { return unless (defined $content); # try to compile the original JS - ok js_compile($content), 'original JS compiles'; + my $res_original = js_compile($content); # minify the JS my $minified = minify($content); ok $minified, 'minified JS'; # try to compile the minified JS - ok js_compile($minified), 'minified JS compiles'; + my $res_minified = js_compile($minified); + is $res_minified, $res_original, 'same errors/warnings as the original' + and note $res_minified; }; } +############################################################################### +# All done! +done_testing(); + + + +############################################################################### sub js_compile { my $js = shift; - my ($rc, $out, $err); + my ($out, $err); + + run [$jsl, '-stdin'], \$js, \$out, \$err; + + my $res = (split /^/, $out)[-1]; + $res =~ s{(\d+\s+error.*?),.*}{$1}; - run [$jsl, '-stdin', '-nosummary'], \$js, \$out, \$err; + unless ($res =~ /\d+\s+error/) { + fail "Unexpected output from jsl"; + diag $res; + } - $rc = $? >> 8; - return $rc <= 2; + return $res; } -############################################################################### -# All done! -done_testing(); diff --git a/t/release-kwalitee.t b/xt/release/kwalitee.t similarity index 58% rename from t/release-kwalitee.t rename to xt/release/kwalitee.t index 60627db..c986546 100644 --- a/t/release-kwalitee.t +++ b/xt/release/kwalitee.t @@ -1,11 +1,3 @@ - -BEGIN { - unless ($ENV{RELEASE_TESTING}) { - print qq{1..0 # SKIP these tests are for release candidate testing\n}; - exit - } -} - # this test was generated with Dist::Zilla::Plugin::Test::Kwalitee 2.12 use strict; use warnings; diff --git a/t/release-unused-vars.t b/xt/release/unused-vars.t similarity index 64% rename from t/release-unused-vars.t rename to xt/release/unused-vars.t index 96db6d8..e601076 100644 --- a/t/release-unused-vars.t +++ b/xt/release/unused-vars.t @@ -1,13 +1,5 @@ #!perl -BEGIN { - unless ($ENV{RELEASE_TESTING}) { - print qq{1..0 # SKIP these tests are for release candidate testing\n}; - exit - } -} - - use Test::More 0.96 tests => 1; eval { require Test::Vars }; From 91c23f297a3a4896eb83fc78b0d231d6796a6979 Mon Sep 17 00:00:00 2001 From: Graham TerMarsch Date: Fri, 15 Oct 2021 20:22:02 -0700 Subject: [PATCH 3/3] Release - v0.15 --- Changes | 5 +++++ MANIFEST | 2 +- META.json | 6 +++--- META.yml | 4 ++-- Makefile.PL | 4 ++-- XS.xs | 16 +++++++++++++++- lib/JavaScript/Minifier/XS.pm | 2 +- t/minify.t | 10 +++++++++- 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Changes b/Changes index 0e37d8d..35f37dc 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,10 @@ Revision history for Perl extension JavaScript::Minifier::XS. +0.15 2021-10-15 20:21:23-07:00 America/Vancouver + - GH#8 - preserve newlines when collapsing whitespace; if a block of + whitespace contains a newline, then when collapsing we collapse to a + newline character, not just "the first whitespace char we found" + 0.14 2021-02-06 23:36:36-08:00 America/Vancouver - rewrote test suite into a single ".t" test - optimized memory allocations, by allocating Nodes in bulk, and being diff --git a/MANIFEST b/MANIFEST index a3e6249..b9ac13c 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,4 +1,4 @@ -# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.017. +# This file was automatically generated by Dist::Zilla::Plugin::Manifest v6.023. Changes LICENSE MANIFEST diff --git a/META.json b/META.json index 7d10b7d..0bfd92b 100644 --- a/META.json +++ b/META.json @@ -4,7 +4,7 @@ "Graham TerMarsch " ], "dynamic_config" : 0, - "generated_by" : "Dist::Zilla version 6.017, CPAN::Meta::Converter version 2.150010", + "generated_by" : "Dist::Zilla version 6.023, CPAN::Meta::Converter version 2.150010", "license" : [ "perl_5" ], @@ -84,9 +84,9 @@ "web" : "https://github.com/bleargh45/JavaScript-Minifier-XS" } }, - "version" : "0.14", + "version" : "0.15", "x_generated_by_perl" : "v5.32.1", - "x_serialization_backend" : "Cpanel::JSON::XS version 4.24", + "x_serialization_backend" : "Cpanel::JSON::XS version 4.25", "x_spdx_expression" : "Artistic-1.0-Perl OR GPL-1.0-or-later", "x_static_install" : 0 } diff --git a/META.yml b/META.yml index 06953b6..4e0f2d9 100644 --- a/META.yml +++ b/META.yml @@ -13,7 +13,7 @@ build_requires: configure_requires: ExtUtils::MakeMaker: '0' dynamic_config: 0 -generated_by: 'Dist::Zilla version 6.017, CPAN::Meta::Converter version 2.150010' +generated_by: 'Dist::Zilla version 6.023, CPAN::Meta::Converter version 2.150010' license: perl meta-spec: url: http://module-build.sourceforge.net/META-spec-v1.4.html @@ -29,7 +29,7 @@ resources: bugtracker: https://github.com/bleargh45/JavaScript-Minifier-XS/issues homepage: http://metacpan.org/release/JavaScript-Minifier-XS/ repository: git://github.com/bleargh45/JavaScript-Minifier-XS.git -version: '0.14' +version: '0.15' x_generated_by_perl: v5.32.1 x_serialization_backend: 'YAML::Tiny version 1.73' x_spdx_expression: 'Artistic-1.0-Perl OR GPL-1.0-or-later' diff --git a/Makefile.PL b/Makefile.PL index 3a3161c..c746ca9 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -1,4 +1,4 @@ -# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.017. +# This file was automatically generated by Dist::Zilla::Plugin::MakeMaker v6.023. use strict; use warnings; @@ -26,7 +26,7 @@ my %WriteMakefileArgs = ( "Test::DiagINC" => "0.002", "Test::More" => "0.96" }, - "VERSION" => "0.14", + "VERSION" => "0.15", "test" => { "TESTS" => "t/*.t" } diff --git a/XS.xs b/XS.xs index 56e22ee..5123328 100644 --- a/XS.xs +++ b/XS.xs @@ -282,8 +282,22 @@ void JsAppendNode(Node* element, Node* node) { /* collapses a node to a single whitespace character */ void JsCollapseNodeToWhitespace(Node* node) { - if (node->contents) { + if (node->contents && (node->length > 1)) { + /* does the node contain endspace? */ + size_t offset = 0; + bool hasEndspace = 0; + while (offset < node->length) { + if (charIsEndspace(node->contents[offset++])) { + hasEndspace = 1; + break; + } + } + + /* collapse node, either to endspace, or to the first whitespace char */ node->length = 1; + if (hasEndspace) { + node->contents[0] = '\n'; + } node->contents[1] = '\0'; } } diff --git a/lib/JavaScript/Minifier/XS.pm b/lib/JavaScript/Minifier/XS.pm index fa20c63..fd426e0 100644 --- a/lib/JavaScript/Minifier/XS.pm +++ b/lib/JavaScript/Minifier/XS.pm @@ -9,7 +9,7 @@ require DynaLoader; our @ISA = qw(Exporter DynaLoader); our @EXPORT_OK = qw(minify); -our $VERSION = '0.14'; +our $VERSION = '0.15'; bootstrap JavaScript::Minifier::XS $VERSION; diff --git a/t/minify.t b/t/minify.t index b5e772c..a5feee8 100755 --- a/t/minify.t +++ b/t/minify.t @@ -16,7 +16,15 @@ subtest 'leading whitespace can be removed' => sub { ############################################################################### subtest 'trailing whitespace can be removed' => sub { my $given = qq{var trailing="trailing whitespace gets removed"; \t\n\r \n}; - my $expect =qq{var trailing="trailing whitespace gets removed";}; + my $expect = qq{var trailing="trailing whitespace gets removed";}; + my $got = minify($given); + is $got, $expect; +}; + +############################################################################### +subtest 'whitespace collapsing preserves newlines' => sub { + my $given = qq{var a=3 \n\rvar a=5}; + my $expect = qq{var a=3\nvar a=5}; my $got = minify($given); is $got, $expect; };