Now all modules are in core modules folder

This commit is contained in:
Manuel Cillero 2017-08-08 12:14:45 +02:00
parent 5ba1cdfa0b
commit 05b6a91b0c
1907 changed files with 0 additions and 0 deletions

339
modules/advagg/LICENSE.txt Normal file
View file

@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, 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 licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU 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. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), 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 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 show them these terms so they know 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.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
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 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 derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
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 License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
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.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary 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
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 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 Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing 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 for copying, distributing or modifying
the Program or works based on it.
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.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. 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 this 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
this License, you may choose any version ever published by the Free Software
Foundation.
10. 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
11. 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.
12. 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
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 the public, 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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 2 of the License, 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) year 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 is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

372
modules/advagg/README.txt Normal file
View file

@ -0,0 +1,372 @@
----------------------------------
ADVANCED CSS/JS AGGREGATION MODULE
----------------------------------
CONTENTS OF THIS FILE
---------------------
* Fast 404
* Features & benefits
* Configuration
* Notes
* JavaScript Bookmarklet
* Technical Details & Hooks
* Single htaccess rules
* nginx Configuration
FAST 404
--------
Assuming that this guide was followed:
http://2bits.com/drupal-planet/reducing-server-resource-utilization-busy-sites-implementing-fast-404s-drupal.html
and your having issues with Advanced CSS/JS Aggregation. Advagg works similar
to imagecache, thus we need to add in an exceptions for the directories that
advagg uses. Replace this if statement
if (!strpos($_SERVER['QUERY_STRING'], 'imagecache')) {
with one like this:
if (!strpos($_SERVER['QUERY_STRING'], 'imagecache') && !strpos($_SERVER['QUERY_STRING'], '/advagg_')) {
This will most likely be in your settings.php file.
If using Nginx make sure there is a rule similar to this in your configuration.
http://drupal.org/node/1116618#comment-4321724
If this is still an issue you can try setting the
"IP Address to send all asynchronous requests to" setting on the
admin/settings/advagg/config page to -1. This will use the hostname instead of
an IP address when making the http request.
If you are still having problems, open an issue on the advagg issue queue:
http://drupal.org/project/issues/advagg
FEATURES & BENEFITS
-------------------
Advanced CSS/JS Aggregation Core Module:
* Imagecache style CSS/JS Aggregation. If the file doesn't exist it will be
generated on demand.
* Stampede protection for CSS and JS aggregation. Uses locking so multiple
requests for the same thing will result in only one thread doing the work.
* Zero file I/O if the Aggregated file already exists. Results in better page
generation performance.
* Fully cached CSS/JS assets making this module faster than drupal core.
* Smarter aggregate deletion. CSS/JS aggregates only get removed from the cache
if they have not been used/accessed in the last 3 days.
* Smarter cache flushing. Scans all CSS/JS files that have been added to any
aggregate; if that file has changed then rebuild all aggregates that contain
the updated file and give the newly aggregated file a new name. The new name
ensures changes go out when using CDNs.
* Works with Drupal's private file system. Can Use a separate directory for
serving aggregated files from.
* Footer JS gets aggregated as well.
* One can add JS to any region & have it aggregated.
drupal_add_js($data, 'module', 'left') is now possible; JS is appended to the
the end of that region.
* One can add external JS/CSS resources.
drupal_add_js('http://example.org/example.js', 'external');
drupal_add_css('http://example.org/example.css', 'external');
is now possible.
* Url query string to turn off aggregation for that request. ?advagg=0 will
turn off file aggregation if the user has the "bypass advanced aggregation"
permission. ?advagg=-1 will completely bypass all of Advanced CSS/JS
Aggregations modules and submodules.
* Button on the admin page for dropping a cookie that will turn off file
aggregation. Useful for theme development.
* Url query string to turn on advagg debugging for that request.
?advagg-debug=1 will output a large debug string to the watchdog if the user
has the "bypass advanced aggregation" permission.
* Gzip support. All aggregated files can be pre-compressed into a .gz file and
served from Apache. This is faster then gzipping the file on each request.
* IE Unlimited CSS support. If using ?advagg=0 the CSS output will change
to use @import style in order to get around the 31 CSS files limit in IE.
* CDN support. Advagg integrates with this module.
* jQuery Update support. Advagg integrates with this module.
* LABjs support. Advagg integrates with this module.
* One year browser cache lifetimes for all aggregated files. This is a good
thing.
* Drush support. "cc advagg" will issue a smart cache flush.
* Admin menu support. Cache flushing Advanced CSS/JS Aggregation is available
in the "Flush all caches" menu.
Advanced CSS/JS Aggregation Submodules:
CSS:
* CSSTidy library support. Can compress the generated CSS files with the
CSSTidy library.
* CSS Compressor 3.0 support. https://github.com/codenothing/css-compressor
JS:
* JSMin+ library support. Can compress the generated JS files with the jsmin+
library.
* JSMin PHP Extension support. http://www.ypass.net/software/php_jsmin/
* Use Dean Edwards Packer on non gzipped files. .gz files will not be packed.
CDN:
* Google's CDN network. Load jquery.js & jquery-ui.js from using the Google
Libraries API. This is a good thing.
Bundler:
* Bundler. Will split up an aggregate into sub aggregates for better load
times throughout your site.
3rd Party modules:
CSS:
* Parallel CSS - AdvAgg Plugin. Have url()'s in css files reference different
CDN domains.
CONFIGURATION
-------------
Settings page is located at:
admin/settings/advagg
* Enable Advanced Aggregation. You can disable the module here. Same effect as
placing ?advagg=-1 in the URL.
* Use AdvAgg in closure. If enabled javascript files in the closure region will
be aggregated by advagg.
* Generate CSS/JS files on request (async mode). If advagg doesn't have a route
back to its self and this is enabled then you will have a broken site. With
this enabled one can expect much quicker page generation times after a cache
flush.
* Gzip CSS/JS files. For every Aggregated file generated, this will create a
gzip version of that and then serve that out if the browser accepts gzip
compression.
* Generate .htaccess files in the advagg_* dirs. If your using the rules
located at the bottom of this document in your webroots htaccess file then
you can disable this checkbox.
* Regenerate flushed bundles in the cache flush request. You can enable if your
server will not timeout on a request. This will call advagg_rebuild_bundle()
as a shutdown function for every bundle that has been marked as expired;
thus rebuilding that bundle in the same request as the flush.
* Use a different directory for storing advagg files. Only available if your
using a private file system. Allows you to save the generated aggregated
files in a different directory. This gets around the private file system
restrictions. If boost is installed, you can safely use the cache directory.
* Aggregation Inclusion Mode. Should the page wait for the aggregate to be
built before including the file, or should it send out the page with
aggregates not included.
* Disable page caching if all aggregates are not included on the page.
* File Checksum Mode. mtime is the file modification time. md5 is a hash of the
files contents.
* IP Address to send all asynchronous requests to. If you wish to have one
server generate all CSS/JS aggregated files then this allows for that to
happen.
* Smart cache flush button. Scan all files referenced in aggregated files. If any of
them have changed, increment the counters containing that file and rebuild
the bundle.
* Cache Rebuild button. Recreate all aggregated files. Useful if JS or CSS
compression was just enabled.
* Forced Cache Rebuild. Recreate all aggregated files by incrementing internal
counter for every bundle. One should never have to use this option.
* Master Reset. Clean slate; same affect as uninstalling the module.
* Rebuild htaccess files. Recreate the generated htaccess files.
* Aggregation Bypass Cookie. This will set or remove a cookie that disables
aggregation for the remainder of the browser session. It acts almost the same
as adding ?advagg=0 to every URL.
Additional information is available at:
admin/settings/advagg/info
* Hook Theme Info. Displays the preprocess_page order. Used for debugging.
* CSS files. Displays how often a files checksum has changed and any data
stored about it.
* JS files. Displays how often a files checksum has changed and any data
stored about it.
* Modules implementing advagg hooks. Lets you know what modules are using
advagg.
* Missing files. Lets you know the files that are trying to be added but are
not there.
* Asynchronous debug info. Outputs the the full object returned from
drupal_http_request() which is helpful when debugging async issues.
NOTES
-----
--Bundler Sub Module--
When using the bundler sub module, tools like Google Page Speed and YSlow will
complain that not all CSS/JS files are in one aggregate. This has to do with
how drupal_add_js/drupal_add_css works. You can get a better score on these
tools by placing all files into one aggregate, but the issue with doing that
is, different pages load different css/js files, thus on page 2 of the users
visit, you will get worse performance because the browser has to re-download a
whole new css & js aggregate rather then the smaller aggregate that only
changed. The bundler attempts to work around this issue by creating various
bundles, each one being chosen fairly smartly so that instead of downloading a
200KB js file you only have to download a 20KB file on the 2nd page.
The bundler sub module is all about balancing trade offs. You can make a site
that had really good performance stats according to the tools but you would
then have to re-download just about everything on a different page of your
site, because not all your pages are the same. If you don't care about this and
want a good score from pagespeed, disable the bundler sub-module. That will
give you a better score, but then you have to download a new (large) aggregate
on different parts of your website and already downloaded file reuse will be
lower.
--Cron--
The cron job for AdvAgg is there as a garbage collector. It only needs to run
once a week; running it every hour isn't going to be bad, it isn't necessary
though.
JAVASCRIPT BOOKMARKLET
----------------------
You can use this JS code as a bookmarklet for toggling the AdvAgg URL parameter.
See http://en.wikipedia.org/wiki/Bookmarklet for more details.
javascript:(function(){var loc = document.location.href,qs = document.location.search,regex_off = /\&?advagg=-1/,goto = loc;if(qs.match(regex_off)) {goto = loc.replace(regex_off, '');} else {qs = qs ? qs + '&advagg=-1' : '?advagg=-1';goto = document.location.pathname + qs;}window.location = goto;})();
TECHNICAL DETAILS & HOOKS
-------------------------
Technical Details:
* There are two database tables and two cache table used by advagg.
* Files are generated by this pattern: css_[MD5]_[Counter].css
* Every JS file is tested for compressibility. This is necessary because jsmin+
can run out of memory on certain files. This allows us to catch these bad
files and mark them. Also allows us to skip files that are already
compressed.
Hooks:
* hook_advagg_css_alter. Modify the data before it gets written to the file.
Useful for compression.
* hook_advagg_css_inline_alter. Modify the data before it gets embedded in the
page. Useful for compression.
* hook_advagg_css_pre_alter. Modify the raw $variables['css'] before it gets
processed. Useful for file replacement.
* hook_advagg_css_extra_alter. Allows one to set the a prefix and suffix to be
added into the HTML DOM. Useful for CSS conditionals.
* hook_advagg_js_alter. Modify the data before it gets written to the file.
Useful for compression.
* hook_advagg_js_inline_alter. Modify the data before it gets embedded in the
page. Useful for compression.
* hook_advagg_js_pre_alter. Modify the raw $javascript before it gets
processed. Useful for file replacement.
* hook_advagg_js_extra_alter. Allows one to set the a prefix and suffix to be
added into the HTML DOM.
* hook_advagg_js_header_footer_alter. Allows one to move JS from the header to
the footer. Also one can look at both header and footer JS arrays before they
get processed.
* hook_advagg_filenames_alter. Allows for a one to many relationship. A single
request for a bundle name can result in multiple bundles being returned.
* hook_advagg_files_table. Allows for modules to mark a file as expired.
* advagg_master_reset. Allows other modules to take part in a master reset.
* advagg_disable_processor. Allows one to turn off advagg from a hook. See the
advagg_advagg_disable_processor() function for example usage.
* advagg_disable_page_cache. Allows 3rd party page cache plugins like boost or
varnish to not cache this page.
* advagg_bundler_analysis_alter. Give installed modules a chance to alter the
bundler's analysis array.
JS/CSS Theme Override:
$conf['advagg_css_render_function'] - advagg_unlimited_css_builder
$conf['advagg_js_render_function'] - advagg_js_builder
JS/CSS File Save Override:
$conf['advagg_file_save_function'] - advagg_file_saver
Public Functions:
* advagg_add_css_inline. Adds the ability to add in inline CSS to the page with
a prefix and suffix being set as well.
SINGLE HTACCESS RULES
---------------------
If the directory level htaccess rules are interfering with your server, you can
place these rules in the Drupal root htaccess file. Place these rules after
"RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]" but before "</IfModule>"
# Rules to correctly serve gzip compressed CSS and JS files.
# Requires both mod_rewrite and mod_headers to be enabled.
<IfModule mod_headers.c>
# Serve gzip compressed CSS/JS files if they exist and client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_URI} (^/(.+)/advagg_(j|cs)s/(.+)\.(j|cs)s) [NC]
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.(j|cs)s$ $1\.$2s\.gz [QSA]
# Serve correct content types, and prevent mod_deflate double gzip.
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1]
<FilesMatch "\.(j|cs)s\.gz$">
# Serve correct encoding type.
Header set Content-Encoding gzip
# Force proxies to cache gzipped & non-gzipped css/js files separately.
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
You also need to place these rules at the very end of your htaccess file, after
"</IfModule>".
# AdvAgg Rules Start.
<FilesMatch "^(j|cs)s_[0-9a-f]{32}_.+\.(j|cs)s(\.gz)?">
# No mod_headers
<IfModule !mod_headers.c>
# No mod_expires
<IfModule !mod_expires.c>
# Use ETags.
FileETag MTime Size
</IfModule>
# Use Expires Directive.
<IfModule mod_expires.c>
# Do not use ETags.
FileETag None
# Enable expirations.
ExpiresActive On
# Cache all aggregated CSS/JS files for 480 weeks after access (A).
ExpiresDefault A290304000
</IfModule>
</IfModule>
<IfModule mod_headers.c>
# Set a far future Cache-Control header to 480 weeks.
Header set Cache-Control "max-age=290304000, no-transform, public"
# Set a far future Expires header.
Header set Expires "Tue, 20 Jan 2037 04:20:42 GMT"
# Pretend the file was last modified a long time ago in the past.
Header set Last-Modified "Wed, 20 Jan 1988 04:20:42 GMT"
# Do not use etags for cache validation.
Header unset ETag
</IfModule>
</FilesMatch>
# AdvAgg Rules End.
Be sure to disable the "Generate .htaccess files in the advagg_* dirs" setting
on the admin/settings/advagg page after placing these rules in the webroots
htaccess file. This is located at the same directory level as Drupal's
index.php.
NGINX CONFIGURATION
-------------------
http://drupal.org/node/1116618
###
### advagg_css and advagg_js support
###
location ~* files/advagg_(?:css|js)/ {
access_log off;
gzip_static on;
expires max;
add_header ETag "";
add_header Cache-Control "max-age=31449600, no-transform, public";
try_files $uri @drupal;
}

View file

@ -0,0 +1,730 @@
<?php
/**
* @file
* Admin page callbacks for the advanced CSS/JS aggregation module.
*/
/**
* Page generation function for admin/settings/advagg
*/
function advagg_admin_page() {
$output = '';
$file_path = drupal_get_path('module', 'advagg');
drupal_add_js(array('advagg' => array('key' => md5(drupal_get_private_key()))), 'setting');
drupal_add_js($file_path . '/advagg.admin.js');
$output .= drupal_get_form('advagg_admin_settings_form');
return $output;
}
/**
* Page generation function for admin/settings/advagg/info
*/
function advagg_admin_info_page() {
$output = '';
return $output . drupal_get_form('advagg_admin_info_form');
}
/**
* Form builder; Configure advagg settings.
*
* @ingroup forms
* @see system_settings_form()
*/
function advagg_admin_info_form() {
$form = array();
$form['info'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Hook Theme Info'),
);
init_theme();
$hooks = theme_get_registry();
$data = implode("\n", $hooks['page']['preprocess functions']);
$form['info']['advagg_debug_info'] = array(
'#type' => 'textarea',
'#default_value' => $data,
'#rows' => count($hooks['page']['preprocess functions']) + 1,
);
$types = db_query("SELECT DISTINCT(filetype) FROM {advagg_files}");
while ($type = db_result($types)) {
$form[$type] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('@type files', array('@type' => drupal_strtoupper($type))),
);
// filename filename_md5 checksum filetype counter data
$results = db_query("SELECT * FROM {advagg_files} WHERE filetype = '%s'", $type);
while ($row = db_fetch_array($results)) {
$data = advagg_get_file_data($row['filename_md5']);
if (!empty($data)) {
list($data, $rows) = advagg_form_print_r($data);
$form[$type][$row['filename_md5']] = array(
'#type' => 'textarea',
'#title' => check_plain($row['filename']),
'#default_value' => $data,
'#rows' => $rows -1,
'#description' => t('File has changed %counter times', array('%counter' => $row['counter'])),
);
}
else {
$form[$type][$row['filename_md5']] = array(
'#type' => 'markup',
'#value' => '<div>' . format_plural($row['counter'], 'changed 1 time - %file<br />', 'changed %counter times - %file<br /></div>', array(
'%counter' => $row['counter'],
'%file' => $row['filename'],
)),
);
}
}
}
// Get hooks in use.
$hooks = array(
'advagg_css_alter' => array(),
'advagg_css_inline_alter' => array(),
'advagg_css_pre_alter' => array(),
'advagg_css_extra_alter' => array(),
'advagg_js_alter' => array(),
'advagg_js_inline_alter' => array(),
'advagg_js_pre_alter' => array(),
'advagg_js_extra_alter' => array(),
'advagg_js_header_footer_alter' => array(),
'advagg_filenames_alter' => array(),
'advagg_files_table' => array(),
'advagg_master_reset' => array(),
'advagg_disable_processor' => array(),
'advagg_disable_page_cache' => array(),
'advagg_bundler_analysis_alter' => array(),
);
foreach ($hooks as $hook => $values) {
$hooks[$hook] = module_implements($hook);
}
// Record function overrides. Not working currently.
// $hooks['advagg_css_render_function'] = array(variable_get('advagg_css_render_function', ADVAGG_CSS_RENDER_FUNCTION));
// $hooks['advagg_js_render_function'] = array(variable_get('advagg_js_render_function', ADVAGG_JS_RENDER_FUNCTION));
// $hooks['advagg_file_save_function'] = array(variable_get('advagg_file_save_function', ADVAGG_FILE_SAVE_FUNCTION));
// Output hooks in form.
foreach ($hooks as $hook => $values) {
if (empty($values)) {
$form['hooks_implemented'][$hook] = array(
'#type' => 'markup',
'#value' => '<div><strong>' . check_plain($hook) . ':</strong><br />&nbsp;&nbsp;&nbsp;&nbsp;' . t('None') . '</div>',
);
}
else {
$form['hooks_implemented'][$hook] = array(
'#type' => 'markup',
'#value' => '<div><strong>' . check_plain($hook) . ':</strong><br />&nbsp;&nbsp;&nbsp;&nbsp;' . filter_xss(implode('<br />&nbsp;&nbsp;&nbsp;&nbsp;', $values)) . '</div>',
);
}
}
$form['hooks_implemented'] += array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Modules implementing advagg hooks'),
);
// If any files are missing list them.
// TODO: Reset missing files list.
$results = db_query("SELECT * FROM {advagg_files} WHERE checksum = '-1' ORDER BY filetype ASC");
while ($row = db_fetch_array($results)) {
$form['missing'][$row['filename_md5']] = array(
'#type' => 'markup',
'#value' => '<div>' . check_plain($row['filename']) . '<br /></div>',
);
}
if (!empty($form['missing'])) {
$form['missing'] += array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => FALSE,
'#title' => t('Missing files'),
);
}
// Asynchronous raw output
$form['async'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Asynchronous debug info'),
);
list($css_path, $js_path) = advagg_get_root_files_dir();
$filepath = $css_path . '/css_missing' . mt_rand() . time() . '_0.css';
$url = _advagg_build_url($filepath);
$htauth_user = variable_get('advagg_async_test_username', '');
$htauth_pass = variable_get('advagg_async_test_password', '');
$headers = array(
'Host' => $_SERVER['HTTP_HOST'],
'Connection' => 'close',
);
if ($htauth_user && $htauth_pass) {
$headers['Authorization'] = 'Basic ' . base64_encode($htauth_user . ':' . $htauth_pass);
}
timer_start(__FUNCTION__ . 'local');
$data_local = drupal_http_request($url, $headers);
$data_local->timer = timer_stop(__FUNCTION__ . 'local');
list($data, $rows) = advagg_form_print_r($data_local);
$form['async']['normal'] = array(
'#type' => 'textarea',
'#title' => check_plain($url),
'#default_value' => $data,
'#rows' => min($rows + 1, 50),
);
if (module_exists('cdn')) {
global $conf;
$path_blacklist = variable_get(CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE, CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_DEFAULT);
$conf[CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE] = '';
$url_cdn = advagg_build_uri($filepath);
$conf[CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE] = $path_blacklist;
// Send request and also time it.
timer_start(__FUNCTION__ . 'cdn');
$data_cdn = drupal_http_request($url_cdn);
$data_cdn->timer = timer_stop(__FUNCTION__ . 'cdn');
list($data, $rows) = advagg_form_print_r($data_cdn);
$form['async']['cdn'] = array(
'#type' => 'textarea',
'#title' => check_plain($url_cdn),
'#default_value' => $data,
'#rows' => min($rows + 1, 50),
);
}
return $form;
}
/**
* Form builder; Configure advagg settings.
*
* @ingroup forms
* @see system_settings_form()
*/
function advagg_admin_settings_form() {
$form = array();
$readme = drupal_get_path('module', 'advagg') . '/README.txt';
$bundle_count = db_result(db_query("SELECT COUNT(*) FROM (SELECT bundle_md5 FROM {advagg_bundles} GROUP BY bundle_md5) as temp"));
$form['advagg_enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Advanced Aggregation'),
'#default_value' => variable_get('advagg_enabled', ADVAGG_ENABLED),
);
$form['advagg_closure'] = array(
'#type' => 'checkbox',
'#title' => t('Use AdvAgg in closure'),
'#default_value' => variable_get('advagg_closure', ADVAGG_CLOSURE),
'#description' => t('If enabled javascript files in the closure region will be aggregated.'),
);
// Make sure the advagg_check_missing_handler function is available.
module_load_install('advagg');
$ret = advagg_check_missing_handler();
$form['advagg_async_generation'] = array(
'#type' => 'checkbox',
'#title' => t('Generate CSS/JS files on request (async mode)'),
'#default_value' => variable_get('advagg_async_generation', ADVAGG_ASYNC_GENERATION),
'#disabled' => $ret['advagg_async_generation']['severity'] == REQUIREMENT_ERROR ? TRUE : FALSE,
'#description' => t('Current State: !value', array(
'!value' => filter_xss(' '
. $ret['advagg_async_generation']['value'] . ' '
. (!empty($ret['advagg_async_generation']['description']) ? $ret['advagg_async_generation']['description'] : '') . ' '
),
)),
);
$form['advagg_async_test'] = array(
'#type' => 'fieldset',
'#title' => t('Async Test HTTP Basic Credentials'),
'#collapsed' => TRUE,
'#collapsible' => TRUE,
);
$form['advagg_async_test']['advagg_async_test_username'] = array(
'#type' => 'textfield',
'#title' => t('Username'),
'#default_value' => variable_get('advagg_async_test_username', ''),
);
$form['advagg_async_test']['advagg_async_test_password'] = array(
'#type' => 'password',
'#title' => t('Password'),
);
$form['advagg_gzip_compression'] = array(
'#type' => 'checkbox',
'#title' => t('Gzip CSS/JS files'),
'#default_value' => variable_get('advagg_gzip_compression', ADVAGG_GZIP_COMPRESSION),
'#description' => t('This might break CSS/JS handling at the Apache level. If it does, use the rules for your webroot level htaccess file before re-enabling. Directions on what to change are located in the <a href="@readme">readme</a> file. In short, be sure to test this out.', array('@readme' => url($readme))),
);
$form['advagg_dir_htaccess'] = array(
'#type' => 'checkbox',
'#title' => t('Generate .htaccess files in the advagg_* dirs'),
'#default_value' => variable_get('advagg_dir_htaccess', ADVAGG_DIR_HTACCESS),
'#description' => t('Disable if your using the rules from the <a href="@readme">readme</a> file in your webroot level htaccess file.', array('@readme' => url($readme))),
);
$form['advagg_rebuild_on_flush'] = array(
'#type' => 'checkbox',
'#title' => t('Regenerate flushed bundles in the cache flush request'),
'#default_value' => variable_get('advagg_rebuild_on_flush', ADVAGG_REBUILD_ON_FLUSH),
'#description' => t('You can enable if your server will not timeout on a request. This will call advagg_rebuild_bundle() as a <a href="http://php.net/register-shutdown-function">shutdown function</a> for every bundle that has been marked as expired; thus rebuilding that bundle in the same request as the flush.'),
);
$public_downloads = (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC);
if (!$public_downloads) {
$extra = module_exists('boost') ? t('If boost is installed, you can use the cache directory for advagg files.') : '';
$form['advagg_custom_files_dir'] = array(
'#type' => 'textfield',
'#field_prefix' => '<div>' . t('You are using a private file system. You must serve aggregated files via a public folder.') . '</div>',
'#title' => t('Use a different directory for storing advagg files'),
'#default_value' => variable_get('advagg_custom_files_dir', ADVAGG_CUSTOM_FILES_DIR),
'#description' => check_plain(t('You need to create a publicly writable directory (same permissions as Drupals files folder) and it needs to be relative to the Drupal index.php file.') . ' ' . $extra . ' ' . t('If you do not need this, you can put a space in this box.')),
'#required' => TRUE,
);
}
$options = array(
0 => t('Wait for locks'),
1 => t('Do not wait for locks'),
2 => t('Only serve aggregated files if they are already built (only works if async is enabled)'),
);
$form['advagg_aggregate_mode'] = array(
'#type' => 'radios',
'#title' => t('Aggregation Inclusion Mode'),
'#default_value' => variable_get('advagg_aggregate_mode', ADVAGG_AGGREGATE_MODE),
'#options' => $options,
'#description' => t('Should the page wait for the aggregate to be built before including the file, or should it send out the page with aggregates not included.'),
);
$form['advagg_page_cache_mode'] = array(
'#type' => 'checkbox',
'#title' => t('Disable page caching if all aggregates are not included on the page.'),
'#default_value' => variable_get('advagg_page_cache_mode', ADVAGG_PAGE_CACHE_MODE),
);
$form['advagg_checksum_mode'] = array(
'#type' => 'radios',
'#title' => t('File Checksum Mode'),
'#default_value' => variable_get('advagg_checksum_mode', ADVAGG_CHECKSUM_MODE),
'#options' => array(
'mtime' => 'mtime',
'md5' => 'md5',
),
'#description' => t('If Drupal is on multiple servers and the file system is not shared; using md5 is recommended. The file modification time (mtime) could be different with this type of setup. WARNING: Changing this value will invalidate all bundles and new ones will have to be built.'),
);
$form['advagg_server_addr'] = array(
'#type' => 'textfield',
'#title' => t('IP Address to send all asynchronous requests to'),
'#default_value' => variable_get('advagg_server_addr', FALSE),
'#description' => t('If left blank it will use the same server as the request. If set to -1 it will use the host name instead of an IP address.'),
);
// $form['advagg_debug'] = array(
// '#type' => 'checkbox',
// '#title' => t('Debug to watchdog.'),
// '#default_value' => variable_get('advagg_debug', ADVAGG_DEBUG),
// );
$form['flush'] = array(
'#type' => 'fieldset',
'#title' => t('Smart cache flush'),
'#description' => t('Scan all files referenced in aggregated files. If any of them have changed, increment the counters containing that file and rebuild the bundle.'),
);
$form['flush']['advagg_flush'] = array(
'#type' => 'submit',
'#value' => t('Flush AdvAgg Cache'),
'#submit' => array('advagg_admin_flush_cache_button'),
);
$form['rebuild'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Cache Rebuild'),
'#description' => t('Recreate all aggregated files. Useful if JS or CSS compression was just enabled. %count Files', array('%count' => $bundle_count)),
);
$form['rebuild']['advagg_rebuild'] = array(
'#type' => 'submit',
'#value' => t('Rebuild AdvAgg Cache'),
'#submit' => array('advagg_admin_batch_rebuild'),
);
$form['forced_rebuild'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Forced Cache Rebuild'),
'#description' => t('Recreate all aggregated files by incrementing internal counter for every bundle. %count Files', array('%count' => $bundle_count)),
);
$form['forced_rebuild']['advagg_forced_build'] = array(
'#type' => 'submit',
'#value' => t('Force all counters to be increment by one'),
'#submit' => array('advagg_admin_batch_rebuild'),
);
$form['master_reset'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Master Reset'),
'#description' => t('Clean Slate - Truncate all advagg tables and delete all advagg files. Useful for testing purposes. Running this on a production site is not a good idea.'),
);
$form['master_reset']['advagg_reset'] = array(
'#type' => 'submit',
'#value' => t('Master Reset'),
'#submit' => array('advagg_admin_master_reset'),
);
$form['htaccess'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Rebuild htaccess files'),
'#description' => t('This will recreate the htaccess files located in the advagg_* directories.'),
);
$form['htaccess']['advagg_recreate_htaccess'] = array(
'#type' => 'submit',
'#value' => t('Recreate htaccess files'),
'#submit' => array('advagg_admin_recreate_htaccess'),
);
$form['bypass'] = array(
'#type' => 'fieldset',
'#title' => t('Aggregation Bypass Cookie'),
'#description' => t('This will set or remove a cookie that disables aggregation for the remainder of the browser session.'),
);
$form['bypass']['submit'] = array(
'#type' => 'submit',
#'#button_type' => 'button',
'#value' => t('Toggle the "aggregation bypass cookie" for this browser'),
'#attributes' => array('onclick' => 'javascript:return advagg_toggle_cookie()'),
'#submit' => array('advagg_admin_toggle_bypass_cookie'),
);
$form['#submit'][] = 'advagg_admin_settings_form_submit';
return system_settings_form($form);
}
/**
* Validate form values. Used to unset variables before they get saved.
*/
function advagg_admin_settings_form_validate($form, &$form_state) {
global $conf;
// Custom directory handling.
if (!empty($form_state['values']['advagg_custom_files_dir'])) {
$form_state['values']['advagg_custom_files_dir'] = trim($form_state['values']['advagg_custom_files_dir']);
$files_dir = $form_state['values']['advagg_custom_files_dir'];
if (!empty($files_dir) && $files_dir != $conf['advagg_custom_files_dir']) {
// Try to create dir structure.
$cumulative = '';
$path_array = explode('/', $files_dir);
foreach ($path_array as $dir) {
if (empty($cumulative)) {
$cumulative = $dir;
}
else {
$cumulative .= '/' . $dir;
}
// If dir creation fails, bail out of loop.
if (!file_check_directory($cumulative, FILE_CREATE_DIRECTORY)) {
break;
}
}
// See if dir exists and is writable.
if (file_check_directory($files_dir) == TRUE) {
// Remove old files.
list($css_path, $js_path) = advagg_get_root_files_dir();
file_scan_directory($css_path, '.*', array('.', '..', 'CVS'), 'file_delete', TRUE);
@unlink($css_path);
file_scan_directory($js_path, '.*', array('.', '..', 'CVS'), 'file_delete', TRUE);
@unlink($js_path);
// Set new path.
$conf['advagg_custom_files_dir'] = $files_dir;
advagg_get_root_files_dir(TRUE);
menu_rebuild();
}
else {
form_set_error('advagg_custom_files_dir', t('%dir is not a directory or is not writable by the web server.', array('%dir' => $files_dir)));
}
}
}
// If the IP field is not blank, check that its a valid address.
if (!empty($form_state['values']['advagg_server_addr']) && $form_state['values']['advagg_server_addr'] != -1 && ip2long($form_state['values']['advagg_server_addr']) === FALSE) {
form_set_error('advagg_server_addr', t('Must be a valid IP address.'));
}
// Remove non variable form info.
unset($form_state['values']['advagg_flush']);
unset($form_state['values']['advagg_rebuild']);
unset($form_state['values']['advagg_forced_build']);
unset($form_state['values']['advagg_recreate_htaccess']);
unset($form_state['values']['advagg_reset']);
}
/**
* Validate form values. Used to unset variables before they get saved.
*/
function advagg_admin_settings_form_submit($form, &$form_state) {
global $conf;
// Pull the old password if it wasn't changed by the user.
if (!empty($form_state['values']['advagg_async_test_username']) && empty($form_state['values']['advagg_async_test_password'])) {
$form_state['values']['advagg_async_test_password'] = variable_get('advagg_async_test_password', '');
}
// Gzip & htaccess checks.
list($css_path, $js_path) = advagg_get_root_files_dir();
$css_path .= '/.htaccess';
$js_path .= '/.htaccess';
if ($conf['advagg_gzip_compression'] != $form_state['values']['advagg_gzip_compression'] && $form_state['values']['advagg_dir_htaccess']) {
$conf['advagg_gzip_compression'] = $form_state['values']['advagg_gzip_compression'];
$conf['advagg_dir_htaccess'] = $form_state['values']['advagg_dir_htaccess'];
advagg_admin_recreate_htaccess();
}
if ($form_state['values']['advagg_dir_htaccess'] == FALSE) {
$conf['advagg_dir_htaccess'] = FALSE;
advagg_clearstatcache(TRUE, $css_path);
advagg_clearstatcache(TRUE, $js_path);
if (file_exists($css_path) || file_exists($js_path)) {
file_delete($css_path);
file_delete($js_path);
drupal_set_message(t('Advanced CSS/JS Aggregation directory level htaccess files have been removed.'));
}
}
elseif ($conf['advagg_dir_htaccess'] != $form_state['values']['advagg_dir_htaccess']) {
$conf['advagg_dir_htaccess'] = $form_state['values']['advagg_dir_htaccess'];
advagg_admin_recreate_htaccess();
}
advagg_clearstatcache(TRUE, $css_path);
advagg_clearstatcache(TRUE, $js_path);
if ($conf['advagg_dir_htaccess'] && (!file_exists($css_path) || !file_exists($js_path))) {
advagg_admin_recreate_htaccess();
}
// If checksum mode changed, smart flush cache with new checksum mode selected.
if ($conf['advagg_checksum_mode'] != $form_state['values']['advagg_checksum_mode']) {
$conf['advagg_checksum_mode'] = $form_state['values']['advagg_checksum_mode'];
advagg_admin_flush_cache_button();
}
// If advagg is enabled/disabled, smart flush the cache.
if ($conf['advagg_enabled'] != $form_state['values']['advagg_enabled']) {
advagg_admin_flush_cache_button();
}
}
/**
* Master reset button.
*/
function advagg_admin_master_reset() {
cache_clear_all('*', 'cache_advagg', TRUE);
cache_clear_all('*', 'cache_advagg_files_data', TRUE);
cache_clear_all('*', 'cache_advagg_bundle_reuse', TRUE);
db_query("TRUNCATE TABLE {advagg_files}");
db_query("TRUNCATE TABLE {advagg_bundles}");
module_invoke_all('advagg_master_reset');
list($css_path, $js_path) = advagg_get_root_files_dir();
file_scan_directory($css_path, '.*', array('.', '..', 'CVS'), 'file_delete', TRUE);
file_scan_directory($js_path, '.*', array('.', '..', 'CVS'), 'file_delete', TRUE);
drupal_set_message(t('Advanced CSS/JS Aggregation has been reset.'));
}
/**
* Cache clear button.
*/
function advagg_admin_flush_cache_button() {
global $_advagg;
_drupal_flush_css_js();
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
if (empty($_advagg['files'])) {
drupal_set_message(t('Advanced CSS/JS Aggregation cache scanned and no out of date bundles detected.'));
}
else {
if (empty($_advagg['rebuilt'])) {
drupal_set_message(t('Advanced CSS/JS Aggregation cache scanned and out of date bundles have been marked. <br />Old Files: <br />!files <br />Marked Bundles Count: %count', array(
'!files' => nl2br(filter_xss(implode("\n", $_advagg['files']))),
'%count' => count($_advagg['bundles']),
)
));
}
else {
drupal_set_message(t('Advanced CSS/JS Aggregation cache scanned and out of date bundles have been incremented and rebuilt. <br />Old Files: <br />%files <br />%count done.', array(
'%files' => implode('<br />', $_advagg['files']),
'%count' => count($_advagg['rebuilt']),
)
));
}
}
}
/**
* Cache clear callback for admin_menu/flush-cache/advagg.
*/
function advagg_admin_flush_cache() {
advagg_admin_flush_cache_button();
drupal_goto();
}
/**
* Set up batch for first and last name loading.
*
* This is where Drupal's Batch API comes into play.
* It's as simple as defining $batch, and then calling batch_set($batch)
*/
function advagg_admin_batch_rebuild($form, &$form_state) {
$batch = array();
// Set up the type of batch operation we will do.
if ($form_state['clicked_button']['#post']['op'] == t('Force all counters to be increment by one')) {
$increment = TRUE;
$batch['title'] = t('Increment and rebuild aggregated files');
}
else {
$increment = FALSE;
$batch['title'] = t('Rebuilding aggregated files');
}
// Build batch operational data.
$batch += array(
'operations' => array(
array('advagg_admin_rebuild_bundles', array($increment)),
),
'finished' => 'advagg_admin_rebuild_bundles_done',
'init_message' => t('Initializing...'),
'progress_message' => t('@current of @total batch operations done.'),
'error_message' => t('Rebuilding aggregated files encountered an error.'),
'file' => drupal_get_path('module', 'advagg') . '/advagg.admin.inc',
);
// Run it
batch_set($batch);
batch_process('admin/settings/advagg');
}
/**
* Rebuild bundles.
*/
function advagg_admin_rebuild_bundles($increment, &$context) {
// init batch.
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = db_result(db_query("SELECT COUNT(*) FROM (SELECT bundle_md5 FROM {advagg_bundles} GROUP BY bundle_md5) as temp"));
// Increment All Counters.
if ($increment) {
db_query("UPDATE {advagg_bundles} SET counter = counter + 1");
}
}
// Set limit.
$limit = 1;
// Get list of all bundles.
$result = db_query_range("SELECT bundle_md5 FROM {advagg_bundles} GROUP BY bundle_md5", $context['sandbox']['progress'], $limit);
while ($bundle_md5 = db_result($result)) {
$filenames = advagg_rebuild_bundle($bundle_md5, '', TRUE);
$filename = array();
foreach ($filenames as $name => $data) {
$filename[] = $name;
}
// Update our progress information.
$context['sandbox']['progress']++;
$context['message'] = t('%md5 Created', array('%md5' => $bundle_md5));
$context['results'][] = $filename;
}
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
function advagg_admin_rebuild_bundles_done($success, $results, $operations) {
if ($success) {
// Here we do something meaningful with the results.
$message = count($results) . ' files generated.';
$message .= theme('item_list', $results);
}
else {
// An error occurred.
// $operations contains the operations that remained unprocessed.
$error_operation = reset($operations);
$message = t('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE)));
}
drupal_set_message($message);
}
/**
* Rebuild htaccess files.
*/
function advagg_admin_recreate_htaccess() {
list($css_path, $js_path) = advagg_get_root_files_dir();
$css_path .= '/.htaccess';
$js_path .= '/.htaccess';
file_delete($css_path);
advagg_htaccess_check_generate($css_path, TRUE);
file_delete($js_path);
advagg_htaccess_check_generate($js_path, TRUE);
drupal_set_message(t('Advanced CSS/JS Aggregation htaccess files rebuilt.'));
}
/**
* Set or remove the AdvAggDisabled cookie.
*/
function advagg_admin_toggle_bypass_cookie() {
global $base_path;
$cookie_name = 'AdvAggDisabled';
$key = md5(drupal_get_private_key());
// If the cookie does exist then remove it.
if (!empty($_COOKIE[$cookie_name]) && $_COOKIE[$cookie_name] == $key) {
setcookie($cookie_name, '', -1, $base_path, '.' . $_SERVER['HTTP_HOST']);
unset($_COOKIE[$cookie_name]);
drupal_set_message(t('AdvAgg Bypass Cookie Removed.'));
}
// If the cookie does not exist then set it.
else {
setcookie($cookie_name, $key, 0, $base_path, '.' . $_SERVER['HTTP_HOST']);
$_COOKIE[$cookie_name] = $key;
drupal_set_message(t('AdvAgg Bypass Cookie Set.'));
}
}
function advagg_form_print_r($data) {
$data = print_r($data, TRUE);
$data = explode("\n", $data);
$new_data = '';
foreach ($data as $text) {
$new_text = str_replace(' ', '', $text);
if ($new_text == "(") {
$new_data .= ' ' . $new_text;
}
else {
$new_data .= "\n" . $text;
}
}
$new_data = explode("\n", $new_data);
$new_data = array_filter($new_data);
$rows = count($new_data);
$new_data = implode("\n", $new_data);
return array($new_data, $rows);
}

View file

@ -0,0 +1,30 @@
/**
* Toggle the advagg cookie
*/
function advagg_toggle_cookie() {
var cookie_name = 'AdvAggDisabled';
// See if the cookie exists.
var cookie_pos = document.cookie.indexOf(cookie_name + '=' + Drupal.settings.advagg.key);
// If the cookie does exist then remove it.
if (cookie_pos !== -1) {
document.cookie = cookie_name + '=;'
+ 'expires=-1;'
+ ' path=' + Drupal.settings.basePath + ';'
+ ' domain=.' + document.location.hostname + ';';
alert(Drupal.t('AdvAgg Bypass Cookie Removed'));
}
// If the cookie does not exsit then set it.
else {
document.cookie = cookie_name + '=' + Drupal.settings.advagg.key + ';'
+ ' path=' + Drupal.settings.basePath + ';'
+ ' domain=.' + document.location.hostname + ';';
alert(Drupal.t('AdvAgg Bypass Cookie Set'));
}
// Must return false, if returning true then form gets submitted.
return false;
}

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* Drush commands for Advanced CSS/JS Aggregation.
*/
/**
* Implement hook_drush_cache_clear.
*
* @param $types
* List of cache types that can be cleared.
*/
function advagg_drush_cache_clear(&$types) {
// Add in Advanced CSS/JS Aggregation
$types['advagg'] = 'drush_advagg_cache_scan';
}
/**
* Rescan bundles and rebuild if needed.
*/
function drush_advagg_cache_scan() {
global $_advagg;
_drupal_flush_css_js();
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
if (empty($_advagg['files'])) {
drush_log(dt('Advanced CSS/JS Aggregation cache scanned and no out of date bundles detected.'));
}
else {
if (empty($_advagg['rebuilt'])) {
drush_log(dt("Advanced CSS/JS Aggregation cache scanned and out of date bundles have marked.\nOld Files:\n!files\nMarked Bundles Count: !count", array(
'!files' => implode("\n", $_advagg['files']),
'!count' => count($_advagg['bundles']),
)
));
}
else {
drush_log(dt("Advanced CSS/JS Aggregation cache scanned and out of date bundles have been incremented and rebuilt.\nOld Files:\n%files\n%count done.", array(
'%files' => implode("\n", $_advagg['files']),
'%count' => count($_advagg['rebuilt']),
)
));
}
}
}

View file

@ -0,0 +1,11 @@
name = Advanced CSS/JS Aggregation
description = Aggregates multiple CSS/JS files, serves them with gzip encoding and smart client-side cache headers.
package = Advanced CSS/JS Aggregation
core = 6.x
; Information added by Drupal.org packaging script on 2017-03-18
version = "6.x-1.11"
core = "6.x"
project = "advagg"
datestamp = "1489800488"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,351 @@
<?php
/**
* @file
* Advanced aggregation module; 404 handler.
*
*/
/**
* Menu Callback; regenerates a missing css file.
*/
function advagg_missing_css() {
ignore_user_abort();
// Try to regenerate missing file
$msg = advagg_missing_regenerate();
// If here send out fast 404.
advagg_missing_fast404($msg);
}
/**
* Menu Callback; regenerates a missing js file.
*/
function advagg_missing_js() {
ignore_user_abort();
// Try to regenerate missing file
$msg = advagg_missing_regenerate();
// If here send out fast 404.
advagg_missing_fast404($msg);
}
/**
* regenerates a missing css file.
*
* @param $filename
* filename
* @param $type
* css or js
* @return
* false if bundle couldn't be generated.
*/
function advagg_missing_regenerate() {
global $base_path, $conf;
// Get filename from request.
$arg = arg();
$filename = array_pop($arg);
$filename = explode('?', $filename);
$filename = array_shift($filename);
$data = advagg_get_bundle_from_filename($filename);
if (is_array($data)) {
list($type, $md5, $counter) = $data;
}
else {
return $data;
}
$_GET['redirect_counter'] = isset($_GET['redirect_counter']) ? intval($_GET['redirect_counter']) : 0;
if ($_GET['redirect_counter'] > 5) {
watchdog('advagg', 'This request could not generate correctly. Loop detected. Request data: %info', array('%info' => $_GET['q']));
return t('In a Loop.');
}
// Counter in database.
$counter_in_db = db_result(db_query("SELECT counter FROM {advagg_bundles} WHERE bundle_md5 = '%s'", $md5));
if ($counter_in_db === FALSE) {
return t('Not a valid bundle.');
}
// Cast counter as int
$counter_in_db = intval($counter_in_db);
$counter = intval($counter);
// Set file(s) in cache to FALSE.
$arg[] = $filename;
cache_set(implode('/', $arg), FALSE, 'cache_advagg', TRUE);
advagg_missing_remove_cache($md5);
// Build filepath.
list($css_path, $js_path) = advagg_get_root_files_dir();
if ($type == 'js') {
$file_type_path = $js_path;
}
if ($type == 'css') {
$file_type_path = $css_path;
}
$new_filename = advagg_build_filename($type, $md5, $counter);
$filepath = $file_type_path . '/' . $new_filename;
// Only process if we got an older counter.
// If we have an out of range counter see if a simlar file exists and serve
// that up.
if ($counter > $counter_in_db || $counter < 0) {
$new_filename = advagg_build_filename($type, $md5, $counter_in_db);
$filepath = $file_type_path . '/' . $new_filename;
advagg_missing_send_file($filepath, advagg_build_uri($filepath), $type);
exit;
}
// Break connection and do generation in the background.
if (!empty($_GET['generator'])) {
advagg_missing_async_opp($md5 . ' ' . $counter);
}
// Rebuild file.
$conf['advagg_async_generation'] = FALSE;
$good = advagg_rebuild_bundle($md5, $counter, TRUE);
if (!$good) {
watchdog('advagg', 'This request could not generate correctly. Aggregate not generated. Request data: %info', array('%info' => $_GET['q']));
return t('Rebuild Failed.');
}
// Serve direct or redirect to file.
$_GET['redirect_counter']++;
$uri = $base_path . $_GET['q'] . '?redirect_counter=' . $_GET['redirect_counter'];
advagg_missing_send_file($filepath, $uri, $type);
exit;
}
/**
* Send the file or send a 307 redirect.
*
* @param $filepath
* filename
* @param $uri
* css or js
*/
function advagg_missing_send_file($filepath, $uri, $type) {
if (!headers_sent()) {
// Code from file_download.
if (file_exists($filepath)) {
$headers = module_invoke_all('file_download', $filepath, $type);
if ($key = array_search(-1, $headers)) {
unset($headers[$key]);
}
// Remove all previously set Cache-Control headers, because we're going to
// override it. Since multiple Cache-Control headers might have been set,
// simply setting a new, overriding header isn't enough: that would only
// override the *last* Cache-Control header. Yay for PHP!
foreach ($headers as $key => $header) {
if (strpos($header, 'Cache-Control:') === 0) {
unset($headers[$key]);
}
elseif (strpos($header, 'ETag:') === 0) {
unset($headers[$key]);
}
}
if (function_exists('header_remove')) {
header_remove('Cache-Control');
header_remove('ETag');
}
else {
drupal_set_header("Cache-Control:");
drupal_set_header("Cache-Control:");
drupal_set_header("ETag:");
drupal_set_header("ETag:");
}
// Set a far future Cache-Control header (480 weeks), which prevents
// intermediate caches from transforming the data and allows any
// intermediate cache to cache it, since it's marked as a public resource.
$headers[] = "Cache-Control: max-age=290304000, no-transform, public";
if (count($headers)) {
advagg_missing_file_transfer($filepath, $headers);
}
}
// advagg_missing_file_transfer didn't run/send data, redirect via header.
header('Location: ' . $uri, TRUE, 307);
usleep(250000); // Sleep for 250ms
}
}
/**
* Set cache value to FALSE.
*
* @param $bundle_md5
* Bundle's machine name.
*/
function advagg_missing_remove_cache($bundle_md5) {
$files = array();
$results = db_query("SELECT filename, filetype FROM {advagg_files} AS af INNER JOIN {advagg_bundles} AS ab USING ( filename_md5 ) WHERE bundle_md5 = '%s' ORDER BY porder ASC", $bundle_md5);
while ($row = db_fetch_array($results)) {
$files[] = $row['filename'];
$type = $row['filetype'];
}
list($css_path, $js_path) = advagg_get_root_files_dir();
if ($type == 'js') {
$file_type_path = $js_path;
}
if ($type == 'css') {
$file_type_path = $css_path;
}
$filenames = advagg_get_filename($files, $type, '', $bundle_md5);
if (!empty($filenames)) {
foreach ($filenames as $key => $info) {
$filename = $info['filename'];
$filepath = $file_type_path . '/' . $filename;
cache_set($filepath, FALSE, 'cache_advagg', TRUE);
}
}
}
/**
* Output text & set php in async mode.
*
* @param $output
* string - Text to output to open connection.
* @param $wait
* bool - Wait 1 second?
* @param $content_type
* string - Content type header.
* @param $length
* int - Content length.
*/
function advagg_missing_async_opp($output, $wait = TRUE, $content_type = "text/html; charset=utf-8", $length = 0) {
if (headers_sent()) {
return FALSE;
}
// Calculate Content Length
if ($length == 0) {
$output .= "\n";
$length = (advagg_missing_strlen($output) -1);
}
// Prime php for background operations
$loop = 0;
while (ob_get_level() && $loop < 25) {
ob_end_clean();
$loop++;
}
header("Connection: close");
ignore_user_abort();
// Output headers & data
ob_start();
header("Content-type: " . $content_type);
header("Expires: Sun, 19 Nov 1978 05:00:00 GMT");
header("Cache-Control: no-cache");
header("Cache-Control: must-revalidate");
header("Content-Length: " . $length);
header("Connection: close");
print($output);
ob_end_flush();
flush();
// wait for 1 second
if ($wait) {
sleep(1);
}
// text returned and connection closed.
// Do background processing. Time taken after should not effect page load times.
return TRUE;
}
/**
* Get the length of a string in bytes
*
* @param $string
* get string length
*/
function advagg_missing_strlen($string) {
if (function_exists('mb_strlen')) {
return mb_strlen($string, '8bit');
}
else {
return strlen($string);
}
}
/**
* Transfer file using http to client. Pipes a file through Drupal to the
* client.
*
* @param $source File to transfer.
* @param $headers An array of http headers to send along with file.
*/
function advagg_missing_file_transfer($source, $headers) {
$source = advagg_missing_file_create_path($source);
$fd = fopen($source, 'rb');
// Return if we can't open the file. Will try a 307 in browser to send file.
if (!$fd) {
return;
}
// Clear the buffer.
if (ob_get_level()) {
ob_end_clean();
}
// Add in headers.
foreach ($headers as $header) {
// To prevent HTTP header injection, we delete new lines that are
// not followed by a space or a tab.
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
$header = preg_replace('/\r?\n(?!\t| )/', '', $header);
drupal_set_header($header);
}
// Transfer file in 8096 byte chunks.
while (!feof($fd)) {
print fread($fd, 8096);
}
fclose($fd);
exit();
}
/**
* Make sure the destination is a complete path and resides in the file system
* directory, if it is not prepend the file system directory.
*
* @param $dest A string containing the path to verify. If this value is
* omitted, Drupal's 'files' directory will be used.
* @return A string containing the path to file, with file system directory
* appended if necessary, or FALSE if the path is invalid (i.e. outside the
* configured 'files' or temp directories).
*/
function advagg_missing_file_create_path($dest = 0) {
list($css_path, $js_path) = advagg_get_root_files_dir();
$valid_paths = array();
$valid_paths[] = file_directory_path();
$valid_paths[] = $css_path;
$valid_paths[] = $js_path;
foreach ($valid_paths as $file_path) {
if (!$dest) {
return $file_path;
}
// file_check_location() checks whether the destination is inside the Drupal files directory.
if (file_check_location($dest, $file_path)) {
return $dest;
}
// check if the destination is instead inside the Drupal temporary files directory.
elseif (file_check_location($dest, file_directory_temp())) {
return $dest;
}
// Not found, try again with prefixed directory path.
elseif (file_check_location($file_path . '/' . $dest, $file_path)) {
return $file_path . '/' . $dest;
}
}
// File not found.
return FALSE;
}

3988
modules/advagg/advagg.module Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
<?php
/**
* @file
* Admin page callbacks for the advagg bundler module.
*/
/**
* Page generation function for admin/settings/bundler
*/
function advagg_bundler_admin_page() {
$output = '';
return $output . drupal_get_form('advagg_bundler_admin_settings_form');
}
/**
* Form builder; Configure advagg settings.
*
* @ingroup forms
* @see system_settings_form()
*/
function advagg_bundler_admin_settings_form() {
$form = array();
$form['advagg_bundler_active'] = array(
'#type' => 'checkbox',
'#title' => t('Bundler is Active'),
'#default_value' => variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE),
'#description' => t('If not checked, the bundler will passively monitor your site, but it will not split up aggregates.'),
);
$options = array(
0 => 0,
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
10 => 10,
);
$form['advagg_bundler_max_css'] = array(
'#type' => 'select',
'#title' => t('Max Number Of CSS Bundles Per Page'),
'#default_value' => variable_get('advagg_bundler_max_css', ADVAGG_BUNDLER_MAX_CSS),
'#options' => $options,
'#description' => t('If 0 is selected then the bundler is disabled'),
);
$form['advagg_bundler_max_js'] = array(
'#type' => 'select',
'#title' => t('Max Number Of JS Bundles Per Page'),
'#default_value' => variable_get('advagg_bundler_max_js', ADVAGG_BUNDLER_MAX_JS),
'#options' => $options,
'#description' => t('If 0 is selected then the bundler is disabled'),
);
$form['info'] = array(
'#type' => 'fieldset',
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#title' => t('Raw Grouping Info'),
);
module_load_include('inc', 'advagg', 'advagg.admin');
$analysis = advagg_bundler_analysis('', TRUE);
asort($analysis);
$analysis = array_reverse($analysis);
$data = array();
foreach ($analysis as $filename => $key) {
$data[$key][] = $filename;
}
list($rawtext, $rows) = advagg_form_print_r($data);
$form['info']['advagg_bundler_info'] = array(
'#type' => 'textarea',
'#title' => t('%count different groupings', array('%count' => count($data))),
'#default_value' => $rawtext,
'#rows' => $rows -1,
);
return system_settings_form($form);
}
/**
* Validate form values. Used to unset variables before they get saved.
*/
function advagg_bundler_admin_settings_form_validate($form, &$form_state) {
global $conf;
cache_clear_all('*', 'cache_advagg_bundle_reuse', TRUE);
// Remove non variable form info.
unset($form_state['values']['advagg_bundler_info']);
}

View file

@ -0,0 +1,12 @@
name = AdvAgg Bundler
description = Provides intelligent bundling of CSS and JS files by grouping files that belong together.
package = Advanced CSS/JS Aggregation
core = 6.x
dependencies[] = advagg
; Information added by Drupal.org packaging script on 2017-03-18
version = "6.x-1.11"
core = "6.x"
project = "advagg"
datestamp = "1489800488"

View file

@ -0,0 +1,136 @@
<?php
/**
* @file
* Handles AdvAgg Bundler installation and upgrade tasks.
*/
/**
* Implements hook_install().
*/
function advagg_bundler_install() {
drupal_install_schema('advagg_bundler');
}
/**
* Implements hook_enable().
*/
function advagg_bundler_enable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
}
/**
* Implements hook_disable().
*/
function advagg_bundler_disable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
}
/**
* Implementation of hook_uninstall().
*/
function advagg_bundler_uninstall() {
// Remove variables.
variable_del('advagg_bundler_outdated');
variable_del('advagg_bundler_max_css');
variable_del('advagg_bundler_max_js');
variable_del('advagg_bundler_active');
drupal_uninstall_schema('advagg_bundler');
}
/**
* Implementation of hook_requirements().
*/
function advagg_bundler_requirements($phase) {
$requirements = array();
// Ensure translations don't break at install time
$t = get_t();
if ($phase == 'runtime') {
}
return $requirements;
}
/**
* Implements hook_schema().
*/
function advagg_bundler_schema() {
$schema['advagg_bundler_selector_count'] = array(
'description' => 'Keep track of when the files were modified.',
'fields' => array(
'filename' => array(
'description' => 'Path of the file relative to Drupal webroot.',
'type' => 'text',
'size' => 'normal',
'not null' => TRUE,
),
'filename_md5' => array(
'description' => 'MD5 hash of filename',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'selector_count' => array(
'description' => 'CSS selector count of the file.',
'type' => 'int',
'not null' => TRUE,
),
'timestamp' => array(
'description' => 'Last modified timestamp of the file.',
'type' => 'int',
'not null' => TRUE,
),
),
'primary key' => array('filename_md5'),
);
return $schema;
}
/**
* Create new database table {advagg_bundler_selector_count}.
*/
function advagg_bundler_update_6100() {
$schema['advagg_bundler_selector_count'] = array(
'description' => 'Keep track of when the files were modified.',
'fields' => array(
'filename' => array(
'description' => 'Path of the file relative to Drupal webroot.',
'type' => 'text',
'size' => 'normal',
'not null' => TRUE,
),
'filename_md5' => array(
'description' => 'MD5 hash of filename',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
'selector_count' => array(
'description' => 'CSS selector count of the file.',
'type' => 'int',
'not null' => TRUE,
),
'timestamp' => array(
'description' => 'Last modified timestamp of the file.',
'type' => 'int',
'not null' => TRUE,
),
),
'primary key' => array('filename_md5'),
);
$ret = array();
db_create_table($ret, 'advagg_bundler_selector_count', $schema['advagg_bundler_selector_count']);
return $ret;
}

View file

@ -0,0 +1,476 @@
<?php
/**
* @file
* Advanced aggregation bundler module.
*
*/
/**
* Default value of the maximum number of CSS bundles that can be generated in
* a single request.
*/
define('ADVAGG_BUNDLER_MAX_CSS', 4);
/**
* Default value of the maximum number of JS bundles that can be generated in
* a single request.
*/
define('ADVAGG_BUNDLER_MAX_JS', 4);
/**
* Default value of the last used time before the bundle is considered outdated.
* 2 weeks in seconds.
*/
define('ADVAGG_BUNDLER_OUTDATED', 1209600);
/**
* Default value to see if the bundler should be active or passive. If it is
* passive, the bundler will only do analysis and not split up the aggregate.
*/
define('ADVAGG_BUNDLER_ACTIVE', TRUE);
/**
* CSS selector limit in a single stylesheet on IE9 and below.
*/
define('SELECTOR_SPLIT_VALUE', 4095);
/**
* Implementation of hook_menu
*/
function advagg_bundler_menu() {
$items = array();
$file_path = drupal_get_path('module', 'advagg_bundler');
$items['admin/settings/advagg/bundler'] = array(
'title' => 'Bundler',
'description' => 'Adjust Bundler settings.',
'page callback' => 'advagg_bundler_admin_page',
'type' => MENU_LOCAL_TASK,
'access arguments' => array('administer site configuration'),
'file path' => $file_path,
'file' => 'advagg_bundler.admin.inc',
'weight' => 10,
);
return $items;
}
/**
* Implement hook_advagg_filenames_alter.
*/
function advagg_bundler_advagg_filenames_alter(&$filenames) {
// Get max number of sub aggregates to create.
$max_css = variable_get('advagg_bundler_max_css', ADVAGG_BUNDLER_MAX_CSS);
$max_js = variable_get('advagg_bundler_max_js', ADVAGG_BUNDLER_MAX_JS);
$output = array();
foreach ($filenames as $values) {
// Extract values and set them.
$filetype = $values['filetype'];
$files = $values['files'];
$bundle_md5 = $values['bundle_md5'];
$cached_data_key = 'bundler_' . $bundle_md5;
// Try cache first; cache table is cache_advagg_bundle_reuse.
$cached_data = advagg_cached_bundle_get($cached_data_key, 'advagg_bundler_filenames_alter');
if (!empty($cached_data)) {
$output = array_merge($output, $cached_data);
continue;
}
// If cache miss then start to do processing.
// Set the max value based off of the filetype.
if ($filetype == 'css') {
$max = $max_css;
}
if ($filetype == 'js') {
$max = $max_js;
}
// If we are only going to have one bundle then do not do any more
// processing.
if (empty($max) || $max == 1) {
$output[] = $values;
$data = array($values);
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
continue;
}
// Load up each file.
$groupings = array();
// Preserve the order while grouping.
$last_group = '';
foreach ($files as $key => $filename) {
// Assign each file to their group.
$group = advagg_bundler_analysis($filename);
// Set $last_group if this is the first run of this foreach loop.
if (empty($last_group)) {
$last_group = $group;
}
if ($last_group == $group) {
// Include this new file into the last used group.
$groupings[$group][] = $filename;
}
else {
// In order to preserve CSS/JS execution order we need to move the last
// group to a unique name. Use the array key to make this group unique.
$groupings[$key . ' ' . $last_group] = $groupings[$last_group];
unset($groupings[$last_group]);
// Place the new file into the new group and set $last_group.
$groupings[$group][] = $filename;
$last_group = $group;
}
}
// If only one group then don't do any more processing.
if (count($groupings) == 1) {
$output[] = $values;
$data = array($values);
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
continue;
}
// Make sure we didn't go over the max; if we did merge the smallest bundles
// together.
advagg_bundler_merge($groupings, $max, $filetype);
// If only one group then don't do any more processing. The merge algorithm
// could have reduce the groupings down to one.
if (count($groupings) == 1) {
$output[] = $values;
$data = array($values);
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
continue;
}
// Set groups.
$data = array();
foreach ($groupings as $bundle) {
$values['files'] = $bundle;
$values['bundle_md5'] = md5(implode('', $bundle));
$data[] = $values;
$output[] = $values;
}
cache_set($cached_data_key, $data, 'cache_advagg_bundle_reuse', CACHE_TEMPORARY);
}
// Return groupings.
if (variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE)) {
$filenames = $output;
}
}
/**
* Given a filename return a bundle key.
*
* @param $filename
* filename
* @param $force
* bypass the cache and get a fresh version of the analysis.
* @return
* string to be used for the grouping key.
*/
function advagg_bundler_analysis($filename = '', $force = FALSE) {
// Cache query in a static.
static $analysis = array();
if (empty($analysis)) {
// See if we have a cached version of this.
$count = db_result(db_query("SELECT COUNT(*) FROM {advagg_bundles} WHERE root=1"));
$cache = cache_get('advagg_bundler_analysis:' . $count);
if (empty($cache->data) || $force) {
// "Magic Query"; only needs to run once.
// Return a count of how many root bundles all files are used in. Count is
// padded with eight zeros so the count can be key sorted as a string
// without worrying about it getting put in the wrong order.
// Return the bundle_md5's value; we need something more unique than count
// when grouping together.
// Return the filename. Used for lookup.
// We join the advagg bundles and files together making sure to only use
// root bundles that have been used in the last 2 weeks. This prevents an
// old site structure from influencing new bundles.
// Grouping by the filename gives us the count and makes it so we don't
// return a lot of rows;
$result = db_query("
SELECT
count,
bundle_md5,
filename
FROM (
SELECT
LPAD(COUNT(*), 8, '00000000') AS count,
bundle_md5,
filename_md5
FROM
{advagg_bundles}
WHERE
root = 1
AND
timestamp > %d
GROUP BY
filename_md5) AS ab
INNER JOIN {advagg_files} AS af USING ( filename_md5 )
", time() - variable_get('advagg_bundler_outdated', ADVAGG_BUNDLER_OUTDATED));
// Save query into a static array with the filename as the key.
while ($row = db_fetch_array($result)) {
$analysis[$row['filename']] = $row['count'] . ' ' . $row['bundle_md5'];
}
// Invoke hook_advagg_bundler_analysis_alter() to give installed modules a
// chance to alter the analysis array.
drupal_alter('advagg_bundler_analysis', $analysis);
// Save results to the cache.
cache_set('advagg_bundler_analysis:' . $count, $analysis, 'cache', CACHE_TEMPORARY);
}
else {
$analysis = $cache->data;
}
}
// If no filename is given pass back then entire query results.
if (empty($filename)) {
return $analysis;
}
// Return a key to be used in groupings.
if (!empty($analysis[$filename])) {
return $analysis[$filename];
}
// We need to return a value that can be used as an array key if the query
// didn't give us anything.
return 0;
}
/**
* Merge bundles together if too many where created.
*
* This preserves the order.
*
* @param $groupings
* array of requested groups
* @param $max
* max number of grouping
*/
function advagg_bundler_merge(&$groupings, $max, $filetype) {
$group_count = count($groupings);
if (!empty($max)) {
// Keep going till array has been merged to the desired size.
while ($group_count > $max) {
// Get array sizes.
// Counts the number of files that are placed into each bundle.
$counts = array();
foreach ($groupings as $key => $values) {
$counts[$key] = count($values);
}
// Create mapping.
// Calculates the file count of potential merges. It only merges with
// neighbors in order to preserve execution order.
$map = array();
$prev_key = '';
foreach ($counts as $key => $val) {
// First run of the foreach loop; populate prev key/values and continue.
// We can't merge with the previous group in this case.
if (empty($prev_key)) {
$prev_key = $key;
$prev_val = $val;
continue;
}
// Array key ($prev_val + $val) is the file count of this new group if
// these 2 groups ($prev_key, $key) where to be merged together.
$map[] = array(
($prev_val + $val) => array($prev_key, $key),
);
$prev_key = $key;
$prev_val = $val;
}
// Get best merge candidate.
// We are looking for the smallest file count. $min is populated with a
// large number (15 bits) so it won't be selected in this process.
$min = 32767;
foreach ($map as $v) {
foreach ($v as $key => $values) {
$min = min($min, $key);
// If the min value (number of files in the proposed merged bundle) is
// the same as the key, then get the 2 bundle keys that generated this
// new min value.
if ($min == $key) {
list($first, $last) = $values;
}
}
}
// Create the new merged set.
$a = $groupings[$first];
$b = $groupings[$last];
$new_set = array_merge($a, $b);
// Rebuild the array with the new set in the correct place.
$new_groupings = array();
foreach ($groupings as $key => $files) {
if ($key == $first) {
$new_groupings[$first . ' ' . $last] = $new_set;
}
elseif ($key != $last) {
$new_groupings[$key] = $files;
}
}
$groupings = $new_groupings;
$group_count--;
}
}
// Make sure there isn't a group that has all files that don't exist or empty.
$bad_groups = array();
$counts = array();
foreach ($groupings as $key => $group) {
$missing_counter = 0;
$counts[$key] = count($group);
foreach ($group as $i => $file) {
advagg_clearstatcache(TRUE, $file);
if (!file_exists($file) || filesize($file) == 0) {
$missing_counter++;
}
}
// If all files are missing/empty in this group then it is a bad set.
if ($missing_counter == count($group)) {
$bad_groups[$key] = TRUE;
}
}
// Add the bad groups to the smallest grouping in this set.
if (!empty($bad_groups)) {
$merge_candidate_key = '';
$merge_candidate_count = 32767;
$bad_group = array();
foreach ($groupings as $key => $group) {
if (isset($bad_groups[$key])) {
// Merge all bad groups into one.
$bad_group = array_merge($bad_group, $group);
// Delete the bad group from the master set.
unset($groupings[$key]);
continue;
}
// Find the smallest good grouping.
$min = min($merge_candidate_count, count($group));
if ($min < $merge_candidate_count) {
$merge_candidate_key = $key;
$merge_candidate_count = $min;
}
}
// Move the bad files into the smallest good group.
$new_set = $groupings[$merge_candidate_key];
$new_set = array_merge($new_set, $bad_group);
$groupings[$merge_candidate_key] = $new_set;
}
// Prevent CSS selectors exceeding 4095 due to limits with IE9 and below.
if ($filetype == 'css') {
// Check each group to see if it exceeds the selector limit.
do {
$groupings_edited = FALSE;
foreach ($groupings as $key => $group) {
// Restart the selector limit check if the grouping was edited.
if ($groupings_edited) {
break;
}
$group_selector_counter = 0;
$selector_counts = advagg_bundler_get_css_selector_count($group);
for ($i = 0; $i < count($group) && !$groupings_edited; $i++) {
$selector_count = isset($selector_counts[$group[$i]]) ? $selector_counts[$group[$i]] : 0;
if ($group_selector_counter + $selector_count > SELECTOR_SPLIT_VALUE) {
$groupings_edited = TRUE;
// Divide the group.
$first_group = array_splice($group, 0, $i);
$second_group = array_splice($group, 0);
// Rebuild the array with the new set in the correct place.
$new_groupings = array();
foreach ($groupings as $k => $files) {
if ($k == $key) {
$new_groupings[$k . '_1'] = $first_group;
$new_groupings[$k . '_2'] = $second_group;
}
else {
$new_groupings[$k] = $files;
}
}
$groupings = $new_groupings;
}
else {
$group_selector_counter += $selector_count;
}
}
}
} while ($groupings_edited);
}
}
/**
* Gets the selector count of the provided files.
*
* @param array $files
* Array of files to use.
*
* @return array
* The selector counts of each file.
*/
function advagg_bundler_get_css_selector_count($files) {
$results = array();
$placeholders = db_placeholders($files);
$result = db_query("SELECT filename, selector_count, timestamp FROM {advagg_bundler_selector_count} WHERE filename IN ($placeholders)", $files);
while ($row = db_fetch_array($result)) {
$modified = 0;
if (is_readable($row['filename'])) {
$modified = filemtime($row['filename']);
}
if ($modified > $row['timestamp']) {
$css = advagg_build_css_bundle(array($row['filename']), TRUE);
// Get the number of selectors.
// http://stackoverflow.com/questions/12567000/regex-matching-for-counting-css-selectors/12567381#12567381
$selector_count = preg_match_all('/\{.+?\}|,/s', $css, $matched);
db_query("UPDATE {advagg_bundler_selector_count} SET timestamp = %d, selector_count = %d WHERE filename LIKE '%s'", $modified, $selector_count, $row['filename']);
$results[$row['filename']] = $selector_count;
}
else {
$results[$row['filename']] = $row['selector_count'];
}
}
foreach ($files as $file) {
if (!isset($results[$file]) && file_exists($file)) {
$css = advagg_build_css_bundle(array($file), TRUE);
$selector_count = preg_match_all('/\{.+?\}|,/s', $css, $matched);
db_query("INSERT INTO {advagg_bundler_selector_count} VALUES('%s', '%s', %d, %d)", $file, md5($file), $selector_count, filemtime($file));
$results[$file] = $selector_count;
}
}
return $results;
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Admin page callbacks for the advagg CSS compression module.
*/
/**
* Page generation function for admin/settings/css-compress
*/
function advagg_css_compress_admin_page() {
$output = '';
return $output . drupal_get_form('advagg_css_compress_admin_settings_form');
}
/**
* Form builder; Configure advagg settings.
*
* @ingroup forms
* @see system_settings_form()
*/
function advagg_css_compress_admin_settings_form() {
$form = array();
$form['advagg_css_compress_agg_files'] = array(
'#type' => 'checkbox',
'#title' => t('Compress CSS Files'),
'#default_value' => variable_get('advagg_css_compress_agg_files', ADVAGG_CSS_COMPRESS_AGG_FILES),
);
$form['advagg_css_compress_inline'] = array(
'#type' => 'checkbox',
'#title' => t('Compress Inline CSS'),
'#default_value' => variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE),
);
$advagg_css_compressor = variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR);
$form['advagg_css_compressor'] = array(
'#type' => 'radios',
'#title' => t('Select the compression library to use'),
'#default_value' => $advagg_css_compressor,
'#options' => array(
0 => 'CSSTidy',
1 => 'CSS Compressor',
2 => 'YUI CSSMin',
),
'#description' => t('<a href="@csstidy">CSSTidy</a> is a well known CSS compression library, but it has some known issues/limitations.<br \> <a href="@csscompressor">CSS Compressor</a> is a faster CSS compression alternative.<br \> <a href="@cssmin">YUI CSSMin</a> is a php port of the java library from yahoo and is fairly quick.<br \>',
array(
'@csstidy' => 'https://github.com/Cerdic/CSSTidy',
'@csscompressor' => 'http://www.codenothing.com/css-compressor/',
'@cssmin' => 'http://code.google.com/p/minify/',
)
),
);
if ($advagg_css_compressor == 0) {
$form['advagg_css_compress_preserve_css'] = array(
'#type' => 'checkbox',
'#title' => t('CSSTidy: Preserve CSS'),
'#default_value' => variable_get('advagg_css_compress_preserve_css', ADVAGG_CSS_COMPRESS_PRESERVE_CSS),
'#description' => t('If disabled CSS selectors will try to be merged together, significantly reducing the css file size. May result in broken layouts is disabled. This only applies to compression through the CSSTidy library.'),
);
}
elseif ($advagg_css_compressor == 1) {
$form['advagg_css_compress_compressor_level'] = array(
'#type' => 'radios',
'#title' => t('CSS Compressor: Select the CSS Compression to use'),
'#default_value' => variable_get('advagg_css_compress_compressor_level', ADVAGG_CSS_COMPRESS_COMPRESSOR_LEVEL),
'#options' => array(
'safe' => t('Safe mode (99% safe) does zero combinations or organizing. Its the best mode if you use a lot of CSS hacks.'),
'sane' => t('Sane mode (90% safe) does most combinations (multiple long hand notations to single shorthand), but still keeps most declarations in their place.'),
'small' => t('Small mode (65% safe) reorganizes the whole style sheet, combines as much as it can, and will break most comment hacks.'),
'full' => t('Full mode (64% safe) does everything small does, but also converts hex codes to their short color name alternatives.'),
),
'#description' => t('This only applies to compression through the CSS Compressor library.'),
);
}
return system_settings_form($form);
}

View file

@ -0,0 +1,13 @@
name = AdvAgg Compress CSS
description = Compress CSS with a 3rd party compressor, CSSTidy currently.
package = Advanced CSS/JS Aggregation
core = 6.x
dependencies[] = advagg
php = 5.0
; Information added by Drupal.org packaging script on 2017-03-18
version = "6.x-1.11"
core = "6.x"
project = "advagg"
datestamp = "1489800488"

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Handles AdvAgg CSS compress installation and upgrade tasks.
*/
/**
* Implementation of hook_enable().
*/
function advagg_css_compress_enable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
}
/**
* Implementation of hook_disable().
*/
function advagg_css_compress_disable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
}
/**
* Implementation of hook_install().
*/
function advagg_css_compress_install() {
drupal_install_schema('advagg_css_compress');
}
/**
* Implementation of hook_uninstall().
*/
function advagg_css_compress_uninstall() {
// Remove variables.
variable_del('advagg_css_compress_compressor_level');
variable_del('advagg_css_compress_preserve_css');
variable_del('advagg_css_compress_inline_cache');
variable_del('advagg_css_compress_agg_files');
variable_del('advagg_css_compress_inline');
variable_del('advagg_css_compressor');
// Remove our cache table.
cache_clear_all('*', 'cache_advagg_css_compress_inline', TRUE);
drupal_uninstall_schema('advagg_css_compress');
}
/**
* Implementation of hook_requirements().
*/
function advagg_css_compress_requirements($phase) {
$requirements = array();
// Ensure translations don't break at install time
$t = get_t();
if ($phase == 'runtime') {
}
return $requirements;
}
/**
* Implementation of hook_schema().
*/
function advagg_css_compress_schema() {
$schema = array();
// Create cache tables.
$schema['cache_advagg_css_compress_inline'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_advagg_css_compress_inline']['description'] = t('Cache table for Advanced CSS/JS Aggregations CSS Compress module. Used to keep inline versions of compressed CSS.');
return $schema;
}
/**
* Update 6100 - Create the cache_advagg_css_compress_inline cache table.
*/
function advagg_css_compress_update_6100() {
$ret = array();
// Create cache table.
$schema = advagg_css_compress_schema();
db_create_table($ret, 'cache_advagg_css_compress_inline', $schema['cache_advagg_css_compress_inline']);
return $ret;
}

View file

@ -0,0 +1,180 @@
<?php
/**
* @file
* Advanced aggregation css compression module.
*
*/
/**
* Default value to see if CSSTidy will preserve CSS.
*/
define('ADVAGG_CSS_COMPRESS_PRESERVE_CSS', TRUE);
/**
* Default value to see if this will compress aggregated files.
*/
define('ADVAGG_CSS_COMPRESS_AGG_FILES', TRUE);
/**
* Default value to see if this will compress inline css.
*/
define('ADVAGG_CSS_COMPRESS_INLINE', TRUE);
/**
* Default value to see if this will cache the compressed inline css.
*/
define('ADVAGG_CSS_COMPRESS_INLINE_CACHE', TRUE);
/**
* Default value for which css compression library to use.
*/
define('ADVAGG_CSS_COMPRESSOR', 2);
/**
* Default value for which css compression library to use.
*/
define('ADVAGG_CSS_COMPRESS_COMPRESSOR_LEVEL', 'sane');
/**
* Implementation of hook_menu
*/
function advagg_css_compress_menu() {
$items = array();
$file_path = drupal_get_path('module', 'advagg_css_compress');
$items['admin/settings/advagg/css-compress'] = array(
'title' => 'CSS Compression',
'description' => 'Adjust CSS Compression settings.',
'page callback' => 'advagg_css_compress_admin_page',
'type' => MENU_LOCAL_TASK,
'access arguments' => array('administer site configuration'),
'file path' => $file_path,
'file' => 'advagg_css_compress.admin.inc',
'weight' => 10,
);
return $items;
}
/**
* Implement hook_advagg_css_alter.
*/
function advagg_css_compress_advagg_css_alter(&$contents, $files, $bundle_md5) {
if (!variable_get('advagg_css_compress_agg_files', ADVAGG_CSS_COMPRESS_AGG_FILES)) {
return;
}
$compressor = variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR);
if ($compressor == 0) {
advagg_css_compress_css_tidy($contents);
}
elseif ($compressor == 1) {
advagg_css_compress_css_compressor($contents);
}
elseif ($compressor == 2) {
advagg_css_compress_yui_cssmin($contents);
}
}
/**
* Implement hook_advagg_css_inline_alter.
*/
function advagg_css_compress_advagg_css_inline_alter(&$contents) {
if (!variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE)) {
return;
}
$compressor = variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR);
// If using a cache, try to get the contents of it.
if (variable_get('advagg_css_compress_inline_cache', ADVAGG_CSS_COMPRESS_INLINE_CACHE)) {
$key = md5($contents) . $compressor;
$table = 'cache_advagg_css_compress_inline';
$data = cache_get($key, $table);
if (!empty($data->data)) {
$contents = $data->data;
return;
}
}
if ($compressor == 0) {
advagg_css_compress_css_tidy($contents);
}
if ($compressor == 1) {
advagg_css_compress_css_compressor($contents);
}
// If using a cache set it.
if (isset($key)) {
cache_set($key, $contents, $table, CACHE_TEMPORARY);
}
}
/**
* Use the CSS Tidy library to compress the CSS.
*
* TODO have set_cfg be fully configurable from GUI.
*/
function advagg_css_compress_css_tidy(&$contents) {
// Initialize CSSTidy.
$filename = drupal_get_path('module', 'advagg_css_compress') . '/csstidy/class.csstidy.inc';
include_once($filename);
$css = new csstidy();
// Try to allocate enough time to run CSSTidy.
if (function_exists('set_time_limit')) {
@set_time_limit(variable_get('advagg_set_time_limit', ADVAGG_SET_TIME_LIMIT));
}
// Set configuration.
$css->set_cfg('preserve_css', variable_get('advagg_css_compress_preserve_css', ADVAGG_CSS_COMPRESS_PRESERVE_CSS));
$css->set_cfg('remove_last_;', TRUE);
$css->set_cfg('merge_selectors', TRUE);
$css->set_cfg('optimise_shorthands', TRUE);
$css->set_cfg('silent', TRUE);
$css->set_cfg('compress_colors', TRUE);
$css->set_cfg('sort_selectors', FALSE);
$css->set_cfg('sort_properties', FALSE);
$css->set_cfg('discard_invalid_properties', FALSE);
$css->set_cfg('timestamp', FALSE);
$css->load_template("highest_compression");
// Compress CSS.
$css->parse($contents);
$contents = $css->print->plain();
}
/**
* Use the CSS Compressor library to compress the CSS.
*
* TODO have compression level be selectable from GUI.
*/
function advagg_css_compress_css_compressor(&$contents) {
// Initialize CSSTidy.
$filename = drupal_get_path('module', 'advagg_css_compress') . '/css-compressor-3.x/src/CSSCompression.inc';
include_once($filename);
$CSSC = new CSSCompression(variable_get('advagg_css_compress_compressor_level', ADVAGG_CSS_COMPRESS_COMPRESSOR_LEVEL));
$contents = $CSSC->compress($contents);
}
/**
* Use the CSSmin library from YUI to compress the CSS.
*/
function advagg_css_compress_yui_cssmin(&$contents) {
// Include CSSmin from YUI.
$filename = drupal_get_path('module', 'advagg_css_compress') . '/yui/CSSMin.inc';
include_once($filename);
$cssmin = new CSSmin();
// Compress the CSS splitting lines after 4k of text
$contents = $cssmin->run($contents, 4096);
}
/**
* Implementation of hook_flush_caches().
*/
function advagg_css_compress_flush_caches() {
return array('cache_advagg_css_compress_inline');
}

View file

@ -0,0 +1,34 @@
#
# CSS Compressor [VERSION]
# [DATE]
# Corey Hart @ http://www.codenothing.com
#
.PHONY: benchmark test
VERSION=temp
all:
@echo "\n\x1B[1;31mPC_LOAD_LETTER\x1B[0m\n"
test:
@php unit/start.inc
test-all:
@php unit/start.inc all
test-focus:
@php unit/focus.inc
test-file:
@php unit/file.inc
test-regression:
@php unit/benchmark/benchmark.inc $(VERSION)
test-cli:
@php cli/cli.inc -i cli/test/a.css > cli/test/result.css
benchmark:
@php unit/benchmark/benchmark.inc
clean:
@sh unit/clean.sh

View file

@ -0,0 +1,137 @@
[CSS Compressor](http://www.codenothing.com/css-compressor/)
============================================================
CSSCompression is a PHP based CSS minifier that analyzes stylesheets for various compressions.
It finds possible CSS shorthand techniques for combination of properties.
Usage
-----
require( 'src/CSSCompression.inc' );
$compressed = CSSCompression::express( $css, 'sane' );
Or, if you need to run it multiple times
$CSSC = new CSSCompression( 'sane' );
$compressed = $CSSC->compress( $css );
Modes
-----
Modes are pre-defined sets of options that can be set by passing in the mode name.
- **safe**: (99% safe) Safe mode does zero combinations or organizing. It's the best mode if you use a lot of hacks.
- **sane**: (90% safe) Sane mode does most combinations(multiple long hand notations to single shorthand), but still keeps most declarations in their place.
- **small**: (65% safe) Small mode reorganizes the whole sheet, combines as much as it can, and will break most comment hacks.
- **full**: (64% safe) Full mode does everything small does, but also converts hex codes to their short color name alternatives.
Here's a few different ways to initiate a mode.
// Express with safe mode
$compressed = CSSCompression::express( $css, 'safe' );
// Creating new instance with sane mode
$CSSC = new CSSCompression( 'sane' );
// Setting an instance with small mode
$CSSC->mode( 'small' );
// Or compressing with the current instance, and setting full mode
$compressed = $CSSC->compress( $css, 'full' );
Singleton Instances
-------------------
Yes the compressor provides singleton access(separate from express), but use it wisely.
$CSSC = CSSCompression::getInstance();
// Or, if you want to keep named instances
$rose_instance = CSSCompression::getInstance('rose');
$blue_instance = CSSCompression::getInstance('blue');
Option Handling
---------------
The compressor has an option function with multiple functionalities.
- If no arguments are passed in, the entire options array is returned.
- If a single name argument is passed, then the value of that key name in the options array is returned.
- If both a name and value are passed, then that value is set to it's corresponding key in the array.
Usage
// Returns entire options array
$options = $CSSC->option();
// Returns the readability option value
$readability = $CSSC->option( 'readability' );
// Sets the readability to non-readable
$CSSC->option( 'readability', CSSCompression::READ_NONE );
Alternatively, you can use direct access methods
// Direct access to options array
$options = $CSSC->options;
// Direct access to readability option value
$readability = $CSSC->readability;
// Direct setting of readability value
$CSSC->readability = CSSCompression::READ_NONE;
Additionally, a reset function is provided to revert back to base options (decided at runtime).
// Resets options to original values
$CSSC->reset();
Readability
-----------
The compressor class provides static integers that map to the internal readability values
CSSCompression::READ_MAX // Maximum Readability
CSSCompression::READ_MED // Medium readability of output
CSSCompression::READ_MIN // Minimal readability of output
CSSCompression::READ_NONE // No readability of output (full compression into single line)
// To set maximum readability (Assuming you have your own instance)
$CSSC->option( 'readability', CSSCompression::READ_MAX );
// Or, just pass it in as another option
$options = array(
'readability' => CSSCompression::READ_MAX,
// Other options ...
);
// Get full readability through express
$compressed = CSSCompression::express( $css, $options );
Contributors
------------
[Corey Hart](http://www.codenothing.com) - Creator
[Martin Zvarík](http://www.teplaky.net/) - Pointed out the url and empty definition bug.
[Phil DeJarnett](http://www.overzealous.com/) - Pointed out splitting(and numerous other) problems
[Stoyan Stefanov](http://www.phpied.com/) - [At rules writeup](http://www.phpied.com/css-railroad-diagrams/) and test suite help.
[Julien Deniau](http://www.jeuxvideo.fr/) - Pointed out escaped characters issue
[Olivier Gorzalka](http://clearideaz.com/) - Strict error warnings cleanup

View file

@ -0,0 +1,22 @@
#
# CSS Compression [VERSION] Change Log
# [DATE]
# Corey Hart @ http://www.codenothing.com
#
# Compressor Additions
- Configuration Modes
- pseduo-space, add space between pseduo selectors and comma/brace for ie6
- removing leading 0 of a decimal, 0.633px -> .633px
- handling all At Rules (charsets,media,imports,font-family,etc..)
- handling ms filter paths
- More css hacks support, including box-modal hack
- attr2selector, Convert id=blah and class=blah attributes to their selector counterparts
- strict-id, removes all selectors up to the last id
- add-unknown, adds unknown blocks to top of output in a comment
# Codebase Changes
- Converted to subclass architecture, more modular.
- Using JSON based sandbox specs for focused unit tests.
- Helper conversion definitions(long2hex, hex2short) are now in JSON based files.
- Added benchmark/regression testing

View file

@ -0,0 +1,263 @@
Class CSSCompression
====================
Below is a description of all public access points to the compressor.
const *bool* VERSION
--------------------
Release version
const *bool* DATE
-----------------
Release Date
const *bool* DEV
----------------
Signifies development mode. When true, back door access to all subclasses is enabled. Should always be false in production.
const *bool* TOKEN
------------------
Special marker that gets injected and removed into the stylesheet during compression. Change this if it exists in your sheet.
public static *array* defaults
------------------------------
Default settings for every instance.
const *int* READ_MAX, const *int* READ_MED, const *int* READ_MIN, const *int* READ_NONE
---------------------------------------------------------------------------------------
Readability constants. Tells what format to return the css back after compression.
Getters
=======
This is the list of readable vars on any given instance
- *string* **css**: Contains the compressed result of the last compression ran.
- *string* **mode**: Contains the current mode of the instance
- *array* **options**: Contains the complete array of currently defined options
- *array* **stats**: Contains the result stats of the last compression ran.
- *string* **(option-name)**: Contains the value of that option **name**.
Usage:
// Print out compressed css
echo $CSSC->css;
// Print out the current mode
echo $CSSC->mode;
// Print out list of options
print_r( $CSSC->options );
// Print out result stats
print_r( $CSSC->stats );
// Print out a single options value
echo $CSSC->readability;
Setters
=======
Currently, you can only directly set options
- *string* **options**, *array* **value**: Merge an array of options with the current defaults
- *string* **name**, *mixed* **value**: Set the option **name** with the **value**.
Usage:
// Merge a custom set of options into the defined set
// Remember that it doesn't set, just merges
$CSSC->options = array( 'readability' => CSSCompression::READ_MAX, 'organize' => true );
// Set a single options value
$CSSC->readability = CSSCompression::READ_MAX;
$CSSC->organize = true;
public function __construct( [ *mixed* $css = NULL, *mixed* $options = NULL ] )
-------------------------------------------------------------------------------
Builds the subclasses first, then does one of the following
- Passing a single string argument that is the name of a mode, sets the mode for this instance.
- Passing a single array of options argument will merge those with the defaults for this instance.
- Passing a single long string argument, that is not the name of a mode, will run compression with the defaults set.
- Passing a long string argument, and a mode name argument, sets the mode's options, and then runs compression on the css string.
- Passing a long string argument, and an array of options argument, merges those with default options, and runs compression on the css string.
Usage:
// Create an instance in 'sane' mode
$CSSC = new CSSCompression( 'sane' );
// Create an instance with a custom set of options
$CSSC = new CSSCompression( array( 'readability' => CSSCompression::READ_MAX ) );
// Creates a new instance, then runs compression on the css passed
$CSSC = new CSSCompression( $css );
echo $CSSC->css;
// Creates a new instance in 'sane' mode, then runs compression on the css passed
$CSSC = new CSSCompression( $css, 'sane' );
echo $CSSC->css;
// Creates a new instance with a custom set of options, then runs compression on the css passed
$CSSC = new CSSCompression( $css, array( 'readability' => CSSCompression::READ_MAX ) );
echo $CSSC->css;
*array* public function mode( *string* $mode = NULL )
-----------------------------------------------------
Sets the mode of the instance.
// Set this instance to 'sane' mode
$CSSC->mode( 'sane' );
*array* public static function modes( [ *mixed* $mode = NULL, *array* $config = NULL ] )
----------------------------------------------------------------------------------------
Mode configuration, any one of the following combination of arguments is allowed
- Passing no arguments returns the entire array of modes.
- Passing only a string mode argument returns that modes configuration.
- Passing a string mode argument, and an array config argument sets that config to the mode.
- Passing a single array argument merges a set of modes into the configured set
Usage:
// Returns the entire list of modes
$modes = CSSCompression::modes();
// Returns 'sane' mode configuration
$sane = CSSCompression::modes( 'sane' );
// Add 'rose' mode to the list of modes
CSSCompression::modes( 'rose', array( 'organize' => false, 'readability' => CSSCompression::READ_MAX ) );
// Add 'rose' and 'blue' mode configurations to set of modes
CSSCompression::modes(array(
'rose' => array( 'organize' => false, 'readability' => CSSCompression::READ_MAX ),
'blue' => array( 'rm-multi-define' => false, 'readability' => CSSCompression::READ_NONE )
));
**NOTE:** When an instance configures itself to a mode, it sets every option to true, and expects the mode configuration to tell it what is false.
*mixed* public function option( [ *mixed* $name = NULL, *mixed* $value = NULL ] )
---------------------------------------------------------------------------------
Custom option handling, any one of the following may happen
- Passing no arguments returns the entire array of options currently set.
- Passing only a string name argument returns the value for that option.
- Passing a single array argument merges those into the current options of the instance.
- Passing a string name argument, and a value argument sets the value to it's corresponding option name.
Usage:
// Get the entire options array for this instance
$options = $CSSC->option();
// Get the current readability value for this instance
$readability = $CSSC->option( 'readability' );
// Merge a set of options into the current instance
$CSSC->option( array( 'organize' => false, 'readability' => CSSCompression::READ_MAX ) );
// Set the readability of the current object to full
$CSSC->option( 'readability', CSSCompression::READ_MAX );
*string* public function compress( *string* $css = NULL, [ *mixed* $options = NULL ] )
--------------------------------------------------------------------------------------
Compresses the given string with the given options/mode. $options can be the name of a mode, or an array of options.
// Compress the css passed
$compressed = $CSSC->comrpess( $css );
// Compress the css in 'sane' mode
$compressed = $CSSC->comrpess( $css, 'sane' );
// Compress the css with a custom set of options
$compressed = $CSSC->comrpess( $css, array( 'readability' => CSSCompression::READ_MAX ) );
*string* public static function express( *string* $css = NULL, [ *mixed* $options = NULL ] )
--------------------------------------------------------------------------------------------
Use's it's own singleton instance to return compressed css sheets. $options can be the name of a mode, or an array of options.
// Compress the css passed
$compressed = CSSCompression::express( $css );
// Compress the css in 'sane' mode
$compressed = CSSCompression::express( $css, 'sane' );
// Compress the css with a custom set of options
$compressed = CSSCompression::express( $css, array( 'readability' => CSSCompression::READ_MAX ) );
*bool* public function reset()
------------------------------
Cleans out compression instance, all of it's subclasses, and resets options back to their defaults.
// Reset this instance to it's defaults
$CSSC->reset();
*bool* public function flush()
------------------------------
Cleans out class vars.
// Flush out compression variables
$CSSC->flush();
*object* public static function getInstance( [ *string* name = NULL ] )
-----------------------------------------------------------------------
Returns a singleton instance of the compressor
// Get a singleton instance
$CSSC = CSSCompression::getInstance();
// Get the stored 'rose' singleton instance
$CSSC = CSSCompression::getInstance( 'rose' );
*array* public static function getJSON( *string* $file )
--------------------------------------------------------
Pulls the contents of the $file, does some quick comment stripping, then returns a json decoded hash. Mainly for internal use.
$json = CSSCompression::getJSON( "/path/to/my/file.json" );

View file

@ -0,0 +1,63 @@
Contribute
==========
This project is always in dire need of help in any form, I only have 2 rules:
- All sandboxed tests must pass.
For nix users: From the root directory, run 'make test'
For windows users: Run 'php unit/start.inc'
- Tab character indentations
I don't care if you like 2,3,4,8,16 spaced indentation, just make sure it's a tab character and not spaces.
make test
---------
This command will run all sandboxed tests to check that most functions run the way they should.
make test-focus
---------------
This command runs a focused test on a single function for development. Open up unit/focus.inc for configuration.
make test-file
--------------
This command runs a focused test on a single file. Make sure the original resides in unit/sheets/original/ and the expected
output resides in unit/sheets/expected/. Open up unit/file.inc for configuration
make test-all
-------------
This command runs all sandboxed tests, as well as double compressions on any stylesheets in benchmark src. Doesn't always pass,
but is a helpful check to see that most compression is done the first time around.
make test-regression [ VERSION=temp ]
--------------------------------
The regression command takes an optional assignment which will do testing against the given version. Defaults is to the last run test.
make benchmark
--------------
This command runs the benchmark process. This will create a new temp directory for regression testing and comparison.
make clean
----------
This command removes all generated files used for comparisons and benchmarks.
unit/sandbox/
-------------
The sandbox directory contains json files for focused tests used in unit testing.

View file

@ -0,0 +1,234 @@
Options
=======
Here's a few different ways to set options.
// Set an array of options
$options = array( 'color-long2hex' => false, 'readability' => CSSCompression::READ_MAX );
// Pass directly into express compression
$compressed = CSSCompression::express( $css, $options );
// Create an instance based on the predefined set of options
$CSSC = new CSSCompression( $options );
// Set a batch of options on an instance
$CSSC->option( $options );
// Set a single option on an instance
$CSSC->option( 'readability', CSSCompression::READ_MAX );
// Or, if you just want to read an option
$readability = $CSSC->option( 'readability' );
// Also, you can look at the current options
$options = $CSSC->option();
color-long2hex
--------------
Converts long color names to short hex names
- *aliceblue -> #f0f8ff*
color-rgb2hex
-------------
Converts rgb colors to hex
- *rgb(159,80,98) -> #9F5062, rgb(100%) -> #FFFFFF*
color-hex2shortcolor
--------------------
Converts long hex codes to short color names, Only works on latest browsers, careful when using.
- *#f5f5dc -> beige*
color-hex2shorthex
------------------
Converts long hex codes to short hex codes
- *#44ff11 -> #4f1*
color-hex2safe
--------------------
Converts long hex codes to safe CSS Level 1 color names.
- *#f00 -> red*
fontweight2num
--------------
Converts font-weight names to numbers
- *bold -> 700*
format-units
------------
Removes zero decimals and 0 units
- *15.0px -> 15px || 0px -> 0*
lowercase-selectors
-------------------
Lowercases html tags from list
- *BODY -> body*
attr2selector
-------------
Converts class and id attributes to their shorthand counterparts
- *div[id=blah][class=blah] -> div#blah.blah*
strict-id
---------
Promotes nested id's to the front of the selector
- *body > div#elem p -> #elem p*
pseudo-space
------------
Add space after :first-letter and :first-line pseudo selectors, for ie6
- *a:first-line{ -> a:first-line {*
directional-compress
--------------------
Compresses single defined multi-directional properties
- *margin: 15px 25px 15px 25px -> margin:15px 25px*
organize
--------
Combines multiply defined selectors and details
- *p{color:blue;} p{font-size:12pt} -> p{color:blue;font-size:12pt;}*
- *p{color:blue;} a{color:blue;} -> p,a{color:blue;}*
csw-combine
-----------
Combines color/style/width properties
- *border-style:dashed;border-color:black;border-width:4px; -> border:4px dashed black*
auralcp-combine
---------------
Combines cue/pause properties
- *cue-before: url(before.au); cue-after: url(after.au) -> cue:url(before.au) url(after.au)*
mp-combine
----------
Combines margin/padding directionals
- *margin-top:10px;margin-right:5px;margin-bottom:4px;margin-left:1px; -> margin:10px 5px 4px 1px;*
border-combine
--------------
Combines border directionals
- *border-top|right|bottom|left:1px solid black -> border:1px solid black*
font-combine
------------
Combines font properties
- *font-size:12pt; font-family: arial; -> font:12pt arial*
background-combine
------------------
Combines background properties
- *background-color: black; background-image: url(bgimg.jpeg); -> background:black url(bgimg.jpeg)*
list-combine
------------
Combines list-style properties
- *list-style-type: round; list-style-position: outside -> list-style:round outside*
border-radius-combine
---------------------
Combines border-radius properties
{
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: 10px;
}
-> { border-radius: 10px; }
unnecessary-semicolons
----------------------
Removes the last semicolon of a rule set
- *{margin: 2px; color: blue;} -> {margin: 2px; color: blue}*
rm-multi-define
---------------
Removes multiple declarations within the same rule set
- *{color:black;font-size:12pt;color:red;} -> {color:red;font-size:12pt;}*
add-unknown
-----------
Adds unknown artifacts to a comment block at the top of output.
readability
-----------
Readability of Compressed Output.
CSSCompression::READ_MAX; // Maximum readability
CSSCompression::READ_MED; // Medium readability
CSSCompression::READ_MIN; // Minimum readability
CSSCompression::READ_NONE; // No readability

View file

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2011 Corey Hart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,438 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
// Static dependencies, Subclasses loaded ondemand
require( dirname(__FILE__) . '/lib/Exception.inc' );
require( dirname(__FILE__) . '/lib/Control.inc' );
class CSSCompression {
/**
* CSSCompression Info
*
* @const (string) VERSION: Release version
* @const (string) DATE: Release date
*/
const VERSION = "[VERSION]";
const DATE = "[DATE]";
/**
* WARNING: This should ALWAYS BE FALSE in production
* When DEV is true, backdoor access to private methods is opened.
* Only used for unit testing and development.
*/
const DEV = true;
/**
* TOKEN is a special string that gets used as a marker within
* the compressor, and is removed before final output. Make sure
* this token is unique to your stylsheets.
*
* NOTE: This string gets used in regular expressions, and escaping
* won't help, so don't pick a complicated token.
*/
const TOKEN = "@____CSSCOMPRESSION_TOKEN____@";
/**
* The default set of options for every instance.
*/
public static $defaults = array(
// Converts long color names to short hex names
// (aliceblue -> #f0f8ff)
'color-long2hex' => true,
// Converts rgb colors to hex
// (rgb(159,80,98) -> #9F5062, rgb(100%) -> #FFFFFF)
'color-rgb2hex' => true,
// Converts long hex codes to short color names (#f5f5dc -> beige)
// Only works on latest browsers, careful when using
'color-hex2shortcolor' => false,
// Converts long hex codes to short hex codes
// (#44ff11 -> #4f1)
'color-hex2shorthex' => true,
// Converts hex codes to safe CSS Level 1 color names
// (#F00 -> red)
'color-hex2safe' => true,
// Converts font-weight names to numbers
// (bold -> 700)
'fontweight2num' => true,
// Removes zero decimals and 0 units
// (15.0px -> 15px || 0px -> 0)
'format-units' => true,
// Lowercases html tags from list
// (BODY -> body)
'lowercase-selectors' => true,
// Converts id and class attribute selectors, to their short selector counterpart
// (div[id=blah][class=moreblah] -> div#blah.moreblah)
'attr2selector' => true,
// Promotes nested id's to the front of the selector
// (body>div#elem p -> $elem p)
'strict-id' => false,
// Add space after pseudo selectors, for ie6
// (a:first-child{ -> a:first-child {)
'pseudo-space' => false,
// Compresses single defined multi-directional properties
// (margin: 15px 25px 15px 25px -> margin:15px 25px)
'directional-compress' => true,
// Combines multiply defined selectors and details
// (p{color:blue;} p{font-size:12pt} -> p{color:blue;font-size:12pt;})
// (p{color:blue;} a{color:blue;} -> p,a{color:blue;})
'organize' => true,
// Combines color/style/width properties
// (border-style:dashed;border-color:black;border-width:4px; -> border:4px dashed black)
'csw-combine' => true,
// Combines cue/pause properties
// (cue-before: url(before.au); cue-after: url(after.au) -> cue:url(before.au) url(after.au))
'auralcp-combine' => true,
// Combines margin/padding directionals
// (margin-top:10px;margin-right:5px;margin-bottom:4px;margin-left:1px; -> margin:10px 5px 4px 1px;)
'mp-combine' => true,
// Combines border directionals
// (border-top|right|bottom|left:1px solid black -> border:1px solid black)
'border-combine' => true,
// Combines font properties
// (font-size:12pt; font-family: arial; -> font:12pt arial)
'font-combine' => true,
// Combines background properties
// (background-color: black; background-image: url(bgimg.jpeg); -> background:black url(bgimg.jpeg))
'background-combine' => true,
// Combines list-style properties
// (list-style-type: round; list-style-position: outside -> list-style:round outside)
'list-combine' => true,
// Combines border-radius properties
// {
// border-top-left-radius: 10px;
// border-top-right-radius: 10px;
// border-bottom-right-radius: 10px;
// border-bottom-left-radius: 10px;
// }
// -> { border-radius: 10px; }
'border-radius-combine' => true,
// Removes the last semicolon of a property set
// ({margin: 2px; color: blue;} -> {margin: 2px; color: blue})
'unnecessary-semicolons' => true,
// Removes multiple declarations within the same rule set
'rm-multi-define' => true,
// Adds all unknown blocks to the top of the output in a comment strip
// Purely for bug reporting, but also useful to know what isn't being handled
'add-unknown' => true,
// Readibility of Compressed Output, Defaults to none
'readability' => 0,
);
/**
* Modes are predefined sets of configuration for referencing. When creating a mode, all options are set to true,
* and the mode array defines which options are to be false
*
* @mode safe: Safe mode does zero combinations or organizing. It's the best mode if you use a lot of hacks
* @mode sane: Sane mode does most combinations(multiple long hand notations to single shorthand),
* --- but still keeps most declarations in their place
* @mode small: Small mode reorganizes the whole sheet, combines as much as it can, and will break most comment hacks
* @mode full: Full mode does everything small does, but also converts hex codes to their short color name alternatives
*/
private static $modes = array(
'safe' => array(
'color-hex2shortcolor' => false,
'attr2selector' => false,
'strict-id' => false,
'organize' => false,
'csw-combine' => false,
'auralcp-combine' => false,
'mp-combine' => false,
'border-combine' => false,
'font-combine' => false,
'background-combine' => false,
'list-combine' => false,
'border-radius-combine' => false,
'rm-multi-define' => false,
),
'sane' => array(
'color-hex2shortcolor' => false,
'strict-id' => false,
'organize' => false,
'font-combine' => false,
'background-combine' => false,
'list-combine' => false,
'rm-multi-define' => false,
),
'small' => array(
'color-hex2shortcolor' => false,
'pseudo-space' => false,
),
'full' => array(
'pseudo-space' => false,
),
);
/**
* Readability Constants
*
* @param (int) READ_MAX: Maximum readability of output
* @param (int) READ_MED: Medium readability of output
* @param (int) READ_MIN: Minimal readability of output
* @param (int) READ_NONE: No readability of output (full compression into single line)
*/
const READ_MAX = 3;
const READ_MED = 2;
const READ_MIN = 1;
const READ_NONE = 0;
/**
* Static Helpers
*
* @instance express: Use a separate instance from singleton access
* @instance instance: Saved instance of CSSCompression
* @param (array) instances: Array of stored instances
* @param (array) rjson: Comment removal before json decoding
*/
private static $express;
private static $instance;
private static $instances = array();
private static $rjson = array(
'patterns' => array(
"/^(.*?){/s",
"/(\t|\s)+\/\/.*/",
),
'replacements' => array(
'{',
'',
),
);
/**
* Controller Instance
*/
private $Control;
/**
* Builds the subclasses, runs the compression if css passed, and merges options
*
* @param (string) css: CSS to compress on initialization if needed
* @param (array) options: Array of preferences to override the defaults
*/
public function __construct( $css = NULL, $options = NULL ) {
$this->Control = new CSSCompression_Control( $this );
// Autorun against css passed
if ( $css ) {
// Allow passing options/mode only
if ( is_array( $css ) || array_key_exists( $css, self::$modes ) ) {
$this->Control->Option->merge( $css );
}
else {
$this->Control->compress( $css, $options );
}
}
// Merge passed options
else if ( $options ) {
$this->Control->Option->merge( $options );
}
}
/**
* (Proxy function) Control access to properties
*
* - Getting stats/_mode/css returns the current value of that property
* - Getting options will return the current full options array
* - Getting anything else returns that current value in the options array or NULL
*
* @param (string) name: Name of property that you want to access
*/
public function __get( $name ) {
return $this->Control->get( $name );
}
/**
* (Proxy function) The setter method only allows
* access to setting values in the options array
*
* @param (string) name: Key name of the option you want to set
* @param (mixed) value: Value of the option you want to set
*/
public function __set( $name, $value ) {
return $this->Control->set( $name, $value );
}
/**
* (Proxy function) Merges a predefined set options
*
* @param (string) mode: Name of mode to use.
*/
public function mode( $mode = NULL ) {
return $this->Control->Option->merge( $mode );
}
/**
* Creates a new mode, or overwrites existing mode
*
* @param (mixed) mode: Name of the mode, or array of modes
* @param (array) config: Configuration of the mode
*/
public static function modes( $mode = NULL, $config = NULL ) {
if ( $mode === NULL ) {
return self::$modes;
}
else if ( is_array( $mode ) ) {
return array_merge( self::$modes, $mode );
}
else if ( $config === NULL ) {
return isset( self::$modes[$mode] ) ? self::$modes[$mode] : NULL;
}
else {
return self::$modes[$mode] = $config;
}
}
/**
* (Proxy function) Maintainable access to the options array
*
* - Passing no arguments returns the entire options array
* - Passing a single name argument returns the value for the option
* - Passing an array will merge the options with the array passed, for object like extension
* - Passing both a name and value, sets the value to the name key, and returns the value
*
* @param (mixed) name: The key name of the option
* @param (mixed) value: Value to set the option
*/
public function option( $name = NULL, $value = NULL ) {
return $this->Control->Option->option( $name, $value );
}
/**
* (Proxy function) Run compression on the sheet passed.
*
* @param (string) css: Stylesheet to be compressed
* @param (mixed) options: Array of options or mode to use.
*/
public function compress( $css = NULL, $options = NULL ) {
return $this->Control->compress( $css, $options );
}
/**
* Static access for direct compression
*
* @param (string) css: Stylesheet to be compressed
* @param (mixed) options: Array of options or mode to use.
*/
public static function express( $css = NULL, $options = NULL ) {
if ( ! self::$express ) {
self::$express = new CSSCompression();
}
self::$express->reset();
return self::$express->compress( $css, $options );
}
/**
* (Proxy Function) Cleans out compressor and it's subclasses to defaults
*
* @params none
*/
public function reset() {
return $this->Control->reset();
}
/**
* (Proxy Function) Cleans out class variables for next run
*
* @params none
*/
public function flush() {
return $this->Control->flush();
}
/**
* The Singleton access method (for those that want it)
*
* @param (string) name: Name of the stored instance
*/
public static function getInstance( $name = NULL ) {
if ( $name !== NULL ) {
if ( ! isset( self::$instances[$name] ) ) {
self::$instances[$name] = new self;
}
return self::$instances[$name];
}
else if ( ! self::$instance ) {
self::$instance = new self;
}
return self::$instance;
}
/**
* Reads JOSN based files, strips comments and converts to array
*
* @param (string) file: Filename
*/
public static function getJSON( $file ) {
// Assume helper file if full path not given
$file = $file[0] == '/' ? $file : dirname(__FILE__) . '/helpers/' . $file;
// Strip comments
$json = preg_replace( self::$rjson['patterns'], self::$rjson['replacements'], file_get_contents( $file ) );
// Decode json
$json = json_decode( $json, true );
// Check for errors
if ( $json === NULL ) {
$e = '';
// JSON Errors, taken directly from http://php.net/manual/en/function.json-last-error.php
switch ( json_last_error() ) {
case JSON_ERROR_NONE:
$e = 'No error has occurred';
break;
case JSON_ERROR_DEPTH:
$e = 'The maximum stack depth has been exceeded';
break;
case JSON_ERROR_CTRL_CHAR:
$e = 'Control character error, possibly incorrectly encoded';
break;
case JSON_ERROR_STATE_MISMATCH:
$e = 'Invalid or malformed JSON';
break;
case JSON_ERROR_SYNTAX:
$e = 'Syntax error';
break;
case JSON_ERROR_UTF8:
$e = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$e = 'Unknown JSON Error';
break;
}
throw new CSSCompression_Exception( "JSON Error in $file: $e" );
}
// Good to go
return $json;
}
/**
* Backdoor access to subclasses
* ONLY FOR DEVELOPMENT/TESTING.
*
* @param (string) class: Name of the focus class
* @param (string) method: Method function to call
* @param (array) args: Array of arguments to pass in
*/
public function access( $class = NULL, $method = NULL, $args = NULL ) {
if ( ! self::DEV ) {
throw new CSSCompression_Exception( "CSSCompression is not in development mode." );
}
else if ( $class === NULL || $method === NULL || $args === NULL ) {
throw new CSSCompression_Exception( "Invalid Access Call." );
}
else if ( ! is_array( $args ) ) {
throw new CSSCompression_Exception( "Expecting array of arguments." );
}
return $this->Control->access( $class, $method, $args );
}
}

View file

@ -0,0 +1,39 @@
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
{
"#f0ffff":"azure",
"#f5f5dc":"beige",
"#ffe4c4":"bisque",
"#a52a2a":"brown",
"#ff7f50":"coral",
"#ffd700":"gold",
"#808080":"gray",
"#008000":"green",
"#4b0082":"indigo",
"#fffff0":"ivory",
"#f0e68c":"khaki",
"#faf0e6":"linen",
"#800000":"maroon",
"#000080":"navy",
"#808000":"olive",
"#ffa500":"orange",
"#da70d6":"orchid",
"#cd853f":"peru",
"#ffc0cb":"pink",
"#dda0dd":"plum",
"#800080":"purple",
"#ff0000":"red",
"#fa8072":"salmon",
"#a0522d":"sienna",
"#c0c0c0":"silver",
"#fffafa":"snow",
"#d2b48c":"tan",
"#008080":"teal",
"#ff6347":"tomato",
"#ee82ee":"violet",
"#f5deb3":"wheat",
"#f00":"red"
}

View file

@ -0,0 +1,16 @@
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
{
"#808080":"gray",
"#008000":"green",
"#800000":"maroon",
"#000080":"navy",
"#808000":"olive",
"#ff0000":"red",
"#c0c0c0":"silver",
"#008080":"teal",
"#f00":"red"
}

View file

@ -0,0 +1,126 @@
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
{
"aliceblue":"#f0f8ff",
"antiquewhite":"#faebd7",
"aquamarine":"#7fffd4",
"bisque":"#ffe4c4",
"black":"#000000",
"blanchedalmond":"#ffebcd",
"blueviolet":"#8a2be2",
"burlywood":"#deb887",
"cadetblue":"#5f9ea0",
"chartreuse":"#7fff00",
"chocolate":"#d2691e",
"coral":"#ff7f50",
"cornflowerblue":"#6495ed",
"cornsilk":"#fff8dc",
"crimson":"#dc143c",
"cyan":"#00ffff",
"darkblue":"#00008b",
"darkcyan":"#008b8b",
"darkgoldenrod":"#b8860b",
"darkgray":"#a9a9a9",
"darkgreen":"#006400",
"darkkhaki":"#bdb76b",
"darkmagenta":"#8b008b",
"darkolivegreen":"#556b2f",
"darkorange":"#ff8c00",
"darkorchid":"#9932cc",
"darkred":"#8b0000",
"darksalmon":"#e9967a",
"darkseagreen":"#8fbc8f",
"darkslateblue":"#483d8b",
"darkslategray":"#2f4f4f",
"darkturquoise":"#00ced1",
"darkviolet":"#9400d3",
"deeppink":"#ff1493",
"deepskyblue":"#00bfff",
"dimgray":"#696969",
"dodgerblue":"#1e90ff",
"firebrick":"#b22222",
"floralwhite":"#fffaf0",
"forestgreen":"#228b22",
"fuchsia":"#ff00ff",
"gainsboro":"#dcdcdc",
"ghostwhite":"#f8f8ff",
"goldenrod":"#daa520",
"greenyellow":"#adff2f",
"honeydew":"#f0fff0",
"hotpink":"#ff69b4",
"indianred ":"#cd5c5c",
"indigo ":"#4b0082",
"lavender":"#e6e6fa",
"lavenderblush":"#fff0f5",
"lawngreen":"#7cfc00",
"lemonchiffon":"#fffacd",
"lightblue":"#add8e6",
"lightcoral":"#f08080",
"lightcyan":"#e0ffff",
"lightgoldenrodyellow":"#fafad2",
"lightgrey":"#d3d3d3",
"lightgreen":"#90ee90",
"lightpink":"#ffb6c1",
"lightsalmon":"#ffa07a",
"lightseagreen":"#20b2aa",
"lightskyblue":"#87cefa",
"lightslategray":"#778899",
"lightsteelblue":"#b0c4de",
"lightyellow":"#ffffe0",
"lime":"#00ff00",
"limegreen":"#32cd32",
"magenta":"#ff00ff",
"maroon":"#800000",
"mediumaquamarine":"#66cdaa",
"mediumblue":"#0000cd",
"mediumorchid":"#ba55d3",
"mediumpurple":"#9370d8",
"mediumseagreen":"#3cb371",
"mediumslateblue":"#7b68ee",
"mediumspringgreen":"#00fa9a",
"mediumturquoise":"#48d1cc",
"mediumvioletred":"#c71585",
"midnightblue":"#191970",
"mintcream":"#f5fffa",
"mistyrose":"#ffe4e1",
"moccasin":"#ffe4b5",
"navajowhite":"#ffdead",
"oldlace":"#fdf5e6",
"olivedrab":"#6b8e23",
"orange":"#ffa500",
"orangered":"#ff4500",
"orchid":"#da70d6",
"palegoldenrod":"#eee8aa",
"palegreen":"#98fb98",
"paleturquoise":"#afeeee",
"palevioletred":"#d87093",
"papayawhip":"#ffefd5",
"peachpuff":"#ffdab9",
"powderblue":"#b0e0e6",
"purple":"#800080",
"rosybrown":"#bc8f8f",
"royalblue":"#4169e1",
"saddlebrown":"#8b4513",
"salmon":"#fa8072",
"sandybrown":"#f4a460",
"seagreen":"#2e8b57",
"seashell":"#fff5ee",
"sienna":"#a0522d",
"silver":"#c0c0c0",
"skyblue":"#87ceeb",
"slateblue":"#6a5acd",
"slategray":"#708090",
"springgreen":"#00ff7f",
"steelblue":"#4682b4",
"thistle":"#d8bfd8",
"tomato":"#ff6347",
"turquoise":"#40e0d0",
"violet":"#ee82ee",
"white":"#ffffff",
"whitesmoke":"#f5f5f5",
"yellow":"#ffff00",
"yellowgreen":"#9acd32"
}

View file

@ -0,0 +1,218 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Cleanup {
/**
* Cleanup patterns
*
* @class Control: Compression Controller
* @param (string) token: Copy of the injection token
* @param (regex) rtoken: Token regex built upon instantiation
* @param (array) options: Reference to options
* @param (regex) rsemi: Checks for last semit colon in details
* @param (regex) rsemicolon: Checks for semicolon without an escape '\' character before it
* @param (regex) rspace: Checks for space without an escape '\' character before it
* @param (regex) rcolon: Checks for colon without an escape '\' character before it
* @param (regex) rquote: Checks for quote (') without an escape '\' character before it
* @param (array) rescape: Array of patterns for groupings that should be escaped
* @param (array) escaped: Contains patterns and replacements for espaced characters
*/
private $Control;
private $token = '';
private $rtoken = '';
private $options = array();
private $rsemi = "/;$/";
private $rsemicolon = "/(?<!\\\);/";
private $rspace = "/(?<!\\\)\s/";
private $rcolon = "/(?<!\\\):/";
private $rquote = "/(?<!\\\)'/";
private $rescape = array(
"/((?<!\\\)\")(.*?)((?<!\\\)\")/",
"/((?<!\\\)')(.*?)((?<!\\\)')/",
"/(url\()(.*?)(\))/",
);
private $escaped = array(
'search' => array(
"\\:",
"\\;",
"\\}",
"\\{",
"\\@",
"\\!",
"\\,",
"\\>",
"\\+",
"\\~",
"\\/",
"\\*",
"\\.",
"\\=",
"\\#",
"\\r",
"\\n",
"\\t",
"\\ ",
),
'replace' => array(
":",
";",
"}",
"{",
"@",
"!",
",",
">",
"+",
"~",
"/",
"*",
".",
"=",
"#",
"\r",
"\n",
"\t",
" ",
),
);
/**
* Build the token regex based on defined token
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->token = CSSCompression::TOKEN;
$this->options = &$control->Option->options;
// Have to build the token regexs after initialization
$this->rtoken = "/($this->token)(.*?)($this->token)/";
array_push( $this->rescape, $this->rtoken );
}
/**
* Central cleanup process, removes all injections
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of details
*/
public function cleanup( &$selectors, &$details ) {
foreach ( $details as $i => &$value ) {
// Auto skip sections
if ( isset( $selectors[$i] ) && strpos( $selectors[$i], $this->token ) === 0 ) {
continue;
}
// Removing dupes
if ( $this->options['rm-multi-define'] ) {
$value = $this->removeMultipleDefinitions( $value );
}
$value = $this->removeUnnecessarySemicolon( $value );
}
return array($selectors, $details);
}
/**
* Removes '\' from possible splitter characters in URLs
*
* @param (string) css: Full css sheet
*/
public function removeInjections( $css ) {
// Remove escaped characters
foreach ( $this->rescape as $regex ) {
$pos = 0;
while ( preg_match( $regex, $css, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$value = $match[1][0]
. str_replace( $this->escaped['search'], $this->escaped['replace'], $match[2][0] )
. $match[3][0];
$css = substr_replace( $css, $value, $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $value ) + 1;
}
}
// Remove token injections
$pos = 0;
while ( preg_match( $this->rtoken, $css, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$value = $match[2][0];
$id = substr( $css, $match[0][1] - 4, 4 ) == '[id=' ? true : false;
$class = substr( $css, $match[0][1] - 7, 7 ) == '[class=' ? true : false;
if ( preg_match( $this->rspace, $value ) || ( ! $id && ! $class ) ) {
$quote = preg_match( $this->rquote, $value ) ? "\"" : "'";
$value = "$quote$value$quote";
$css = substr_replace( $css, $value, $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $value ) + 1;
}
else {
$css = substr_replace( $css, $value, $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $value ) + 1;
}
}
return $css;
}
/**
* Removes multiple definitions that were created during compression
*
* @param (string) val: CSS Selector Properties
*/
private function removeMultipleDefinitions( $val = '' ) {
$storage = array();
$arr = preg_split( $this->rsemicolon, $val );
foreach ( $arr as $x ) {
if ( $x ) {
list( $a, $b ) = preg_split( $this->rcolon, $x, 2 );
$storage[$a] = $b;
}
}
if ( $storage ) {
$val = '';
foreach ( $storage as $x => $y ) {
$val .= "$x:$y;";
}
}
// Return converted val
return $val;
}
/**
* Removes last semicolons on the final property of a set
*
* @param (string) value: rule set
*/
private function removeUnnecessarySemicolon( $value ) {
return preg_replace( $this->rsemi, '', $value );
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
if ( $method == 'cleanup' ) {
return $this->cleanup( $args[0], $args[1] );
}
else {
return call_user_func_array( array($this, $method), $args );
}
}
else {
throw new CSSCompression_Exception( "Unknown method in Cleanup Class - " . $method );
}
}
}

View file

@ -0,0 +1,196 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Color {
/**
* Color Patterns
*
* @class Control: Compression Controller
* @param (array) options: Reference to options array
* @param (regex) rrgb: Checks for rgb notation
* @param (regex) rhex: Checks for hex code
* @param (regex) rfullhex: Checks for full 6 character hex code
* @static (array) color2hex: Long color name to hex code conversions
* @static (array) hex2short: Hex code to short color name conversions
* @static (array) hex2short_safe: CSS Level 1 safe color names that are shorter than hex codes
* @static (array) files: List of static helpers with their class vars
*/
private $Control;
private $options = array();
private $rrgb = "/^rgb\((\d{1,3}\%?(,\d{1,3}\%?,\d{1,3}\%?)?)\)$/i";
private $rhex = "/^#([0-9a-f]{3}|[0-9a-f]{6})$/i";
private $rfullhex = "/^#([0-9a-f]{6})$/i";
private static $color2hex = array();
private static $hex2short = array();
private static $hex2short_safe = array();
private static $files = array(
'color2hex' => 'long2hex-colors.json',
'hex2short' => 'hex2short-colors.json',
'hex2short_safe' => 'hex2short-safe.json',
);
/**
* Stash a reference to the controller on each instantiation
* and install conversion helpers
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = &$control->Option->options;
if ( ! self::$color2hex ) {
foreach ( self::$files as $v => $file ) {
self::$$v = CSSCompression::getJSON( $file );
}
}
}
/**
* Central handler for all color conversions.
*
* @param (string) val: Color to be parsed
*/
public function color( $val ) {
// Converts rgb values to hex codes
if ( $this->options['color-rgb2hex'] ) {
$val = $this->rgb2hex( $val );
}
// Convert long color names to hex codes
if ( $this->options['color-long2hex'] ) {
$val = $this->color2hex( $val );
}
// Ensure all hex codes are lowercase
if ( preg_match( $this->rhex, $val ) ) {
$val = strtolower( $val );
}
// Convert large hex codes to small codes
if ( $this->options['color-hex2shorthex'] ) {
$val = $this->hex2short( $val );
}
// Convert 6 digit hex codes to short color names
if ( $this->options['color-hex2shortcolor'] ) {
$val = $this->hex2color( $val );
}
// Convert safe css level1 color names
if ( $this->options['color-hex2safe'] ) {
$val = $this->hex2safe( $val );
}
return $val;
}
/**
* Converts rgb values to hex codes
*
* @param (string) val: Color to be converted
*/
private function rgb2hex( $val ) {
if ( ! preg_match( $this->rrgb, $val, $match ) ) {
return $val;
}
// locals
$hex = '0123456789abcdef';
$str = explode( ',', $match[1] );
$new = '';
// Incase rgb was defined with single val
if ( ! $str ) {
$str = array($match[1]);
}
foreach ( $str as $x ) {
$x = strpos( $x, '%' ) !== false ? intval( ( intval( $x ) / 100 ) * 255 ) : intval( $x );
if ( $x > 255 ) {
$x = 255;
}
if ( $x < 0 ) {
$x = 0;
}
$new .= $hex[( $x - $x % 16 ) / 16];
$new .= $hex[$x % 16];
}
// Repeat hex code to complete 6 digit hex requirement for single definitions
if ( count( $str ) == 1 ) {
$new .= $new . $new;
}
// Replace with hex value
return "#$new";
}
/**
* Convert long color names to hex codes
*
* @param (string) val: Color to be converted
*/
private function color2hex( $val ) {
return isset( self::$color2hex[$val] ) ? self::$color2hex[$val] : $val;
}
/**
* Convert large hex codes to small codes
*
* @param (string) val: Hex to be shortened
*/
private function hex2short( $val ) {
if ( ! preg_match( $this->rfullhex, $val, $match ) ) {
return $val;
}
// See if we can convert to 3 char hex
$hex = $match[1];
if ( $hex[0] == $hex[1] && $hex[2] == $hex[3] && $hex[4] == $hex[5] ) {
$val = '#' . $hex[0] . $hex[2] . $hex[4];
}
return $val;
}
/**
* Convert large hex codes to small codes
*
* @param (string) val: Color to be converted
*/
private function hex2color( $val ) {
return isset( self::$hex2short[$val] ) ? self::$hex2short[$val] : $val;
}
/**
* Convert large hex codes to small codes
*
* @param (string) val: Color to be converted
*/
private function hex2safe( $val ) {
return isset( self::$hex2short_safe[$val] ) ? self::$hex2short_safe[$val] : $val;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Color Class - " . $method );
}
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @param (string) token: Copy of the injection token
* @param (array) options: Reference to options
* @param (regex) rspace: Checks for space without an escape '\' character before it
* @param (regex) rslash: Checks for unescaped slash character
* @param (regex) rimportant: Checking props for uncombinables
* @param (array) methods: List of options with their corresponding class
*/
private $Control;
private $token = '';
private $options = array();
private $rspace = "/(?<!\\\)\s/";
private $rslash = "/(?<!\\\)\//";
private $rimportant = "/inherit|\!important|\!ie|(?<!\\\)\s/i";
private $methods = array(
'csw-combine' => 'BorderOutline',
'border-radius-combine' => 'BorderRadius',
'border-combine' => 'Border',
'mp-combine' => 'MarginPadding',
'background-combine' => 'Background',
'auralcp-combine' => 'Aural',
'font-combine' => 'Font',
'list-combine' => 'List',
);
/**
* Sub Comination Classes
*
* @class BorderOutline: Handles Color/Style/With combinations of border/outline properties
* @class BorderRadius: Handles border-radius combinations
* @class Border: Handles normal border combinations
* @class MarginPadding: Handles margin/padding combinations
* @class Background: Handles background combinations
* @class Aural: Handles aural combinations
* @class Font: Handles font combinations
* @class List: Handles list combinations
* @param (array) subcombines: Array holding all subcombination classes
*/
public $BorderOutline;
public $BorderRadius;
public $Border;
public $MarginPadding;
public $Background;
public $Aural;
public $Font;
public $List;
private $subcombines = array(
'BorderOutline',
'BorderRadius',
'Border',
'MarginPadding',
'Background',
'Aural',
'Font',
'List',
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->token = CSSCompression::TOKEN;
$this->options = &$control->Option->options;
// Include classes if not already done so
if ( ! class_exists( "CSSCompression_Combine_Border", false ) ) {
$path = dirname(__FILE__) . '/Combine/';
foreach ( $this->subcombines as $class ) {
require( $path . $class . '.inc' );
}
}
// Instantiate each sub combine
foreach ( $this->subcombines as $class ) {
$full = "CSSCompression_Combine_$class";
$this->$class = new $full( $control, $this );
}
}
/**
* Reads through each detailed package and checks for cross defn combinations
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of details
*/
public function combine( &$selectors = array(), &$details = array() ) {
foreach ( $details as $i => &$value ) {
if ( isset( $selectors[$i] ) && strpos( $selectors[$i], $this->token ) === 0 ) {
continue;
}
foreach ( $this->methods as $option => $class ) {
if ( $this->options[$option] ) {
$value = $this->$class->combine( $value );
}
}
}
return array($selectors, $details);
}
/**
* Helper function to ensure flagged words don't get
* overridden
*
* @param (mixed) obj: Array/String of definitions to be checked
*/
public function checkUncombinables( $obj ) {
if ( is_array( $obj ) ) {
foreach ( $obj as $item ) {
if ( preg_match( $this->rimportant, $item ) ) {
return true;
}
}
return false;
}
else {
return preg_match( $this->rimportant, $obj );
}
}
/**
* Helper function to ensure all values of search array
* exist within the storage array
*
* @param (string) prop: CSS Property
* @param (array) storage: Array of definitions found
* @param (array) search: Array of definitions requred
*/
public function searchDefinitions( $prop, $storage, $search ) {
// Return if storage & search don't match
if ( count( $storage ) != count( $search ) ) {
return false;
}
$str = "$prop:";
foreach ( $search as $value ) {
if ( ! isset( $storage[$value] ) || $this->checkUncombinables( $storage[$value] ) ) {
return false;
}
$str .= $storage[$value] . ' ';
}
return trim( $str ) . ';';
}
/**
* Access to private methods for testing
*
* @param (string) subclass: Name of subclass to focus on
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $subclass, $method, $args ) {
if ( $subclass == 'Combine' ) {
if ( method_exists( $this, $method ) ) {
if ( $method == 'combine' ) {
return $this->combine( $args[0], $args[1] );
}
else {
return call_user_func_array( array($this, $method), $args );
}
}
else {
throw new CSSCompression_Exception( "Unknown method in Combine Class - " . $method );
}
}
else if ( in_array( $subclass, $this->subcombines ) ) {
return $this->$subclass->access( $method, $args );
}
else {
throw new CSSCompression_Exception( "Unknown Sub Combine Class - " . $subclass );
}
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_Aural {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) raural: Aurual matching
*/
private $Control;
private $Combine;
private $raural = "/(^|(?<!\\\);)(cue|pause)-(before|after):(.*?)((?<!\\\);|$)/";
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines Aural properties (currently being depreciated in W3C Standards)
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
$storage = $this->storage( $val );
$pos = 0;
// Replace first occurance with it's prop, and remove all following occurances
while ( preg_match( $this->raural, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$prop = $match[2][0];
if ( isset( $storage[$prop] ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $storage[$prop], $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $storage[$prop] ) - $colon - 1;
$storage[$prop] = '';
}
else {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
}
// Return converted val
return $val;
}
/**
* Builds a storage object for iteration
*
* @param (string) val: Rule Set
*/
private function storage( $val ) {
$storage = array();
// Find all possible occurences and build the replacement
$pos = 0;
while ( preg_match( $this->raural, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
if ( ! isset( $storage[$match[2][0]] ) ) {
$storage[$match[2][0]] = array($match[3][0] => $match[4][0]);
}
// Override double written properties
$storage[$match[2][0]][$match[3][0]] = $match[4][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Go through each tag for possible combination
foreach ( $storage as $tag => $arr ) {
// All three have to be defined
if ( count( $arr ) == 2 && ! $this->Combine->checkUncombinables( $arr ) ) {
$storage[$tag] = "$tag:" . $arr['before'] . ' ' . $arr['after'] . ';';
}
else {
unset( $storage[$tag] );
}
}
return $storage;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Aural Class - " . $method );
}
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_Background {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rbackground: Background matching
* @param (array) groupings: List of background combinations
*/
private $Control;
private $Combine;
private $rbackground = "/(^|(?<!\\\);)background-(color|image|repeat|attachment|position):(.*?)((?<!\\\);|$)/";
private $groupings = array(
// With color
array('color', 'image', 'repeat', 'attachment', 'position'),
array('color', 'image', 'attachment', 'position'),
array('color', 'image', 'repeat', 'position'),
array('color', 'image', 'repeat', 'attachment'),
array('color', 'image', 'repeat'),
array('color', 'image', 'attachment'),
array('color', 'image', 'position'),
array('color', 'image'),
// Without Color
array('image', 'attachment', 'position'),
array('image', 'repeat', 'position'),
array('image', 'repeat', 'attachment'),
array('image', 'repeat'),
array('image', 'attachment'),
array('image', 'position'),
// Just Color/Image
array('image'),
array('color'),
);
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines multiple background props into single definition
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
$storage = array();
// Find all possible occurences and build the replacement
$pos = 0;
while ( preg_match( $this->rbackground, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$storage[$match[2][0]] = $match[3][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Run background checks and get replacement str
foreach ( $this->groupings as $props ) {
if ( $replace = $this->Combine->searchDefinitions( 'background', $storage, $props ) ) {
break;
}
}
// If replacement string found, run it on all declarations
if ( $replace ) {
$pos = 0;
while ( preg_match( $this->rbackground, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
$replace = '';
}
}
// Return converted val
return $val;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Background Class - " . $method );
}
}
}

View file

@ -0,0 +1,95 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_Border {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rborder: Border matching
*/
private $Control;
private $Combine;
private $rborder = "/(^|(?<!\\\);)border-(top|right|bottom|left):(.*?)((?<!\\\);|$)/";
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines multiple border properties into single definition
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
if ( ( $replace = $this->replace( $val ) ) === false ) {
return $val;
}
// Rebuild the rule set with the combinations found
$pos = 0;
while ( preg_match( $this->rborder, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
$replace = '';
}
// Return converted val
return $val;
}
/**
* Builds a replacement string
*
* @param (string) val: Rule Set
*/
private function replace( $val ) {
$storage = array();
// Find all possible occurences and build the replacement
$pos = 0;
while ( preg_match( $this->rborder, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
// Override double written properties
$storage[$match[2][0]] = $match[3][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// All 4 have to be defined
if ( count( $storage ) == 4 &&
$storage['top'] == $storage['bottom'] &&
$storage['left'] == $storage['right'] &&
$storage['top'] == $storage['right'] ) {
return "border:" . $storage['top'] . ';';
}
return false;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Border Class - " . $method );
}
}
}

View file

@ -0,0 +1,106 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_BorderOutline {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rcsw: Border/Outline matching
*/
private $Control;
private $Combine;
private $rcsw = "/(^|(?<!\\\);)(border|border-top|border-bottom|border-left|border-right|outline)-(color|style|width):(.*?)((?<!\\\);|$)/";
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines color/style/width of border/outline properties
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
$storage = $this->storage( $val );
$pos = 0;
// Now rebuild the string replacing all instances
while ( preg_match( $this->rcsw, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$prop = $match[2][0];
if ( isset( $storage[$prop] ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $storage[$prop], $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $storage[$prop] ) - $colon - 1;
$storage[$prop] = '';
}
else {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
}
// Return converted val
return $val;
}
/**
* Builds a storage object for iteration
*
* @param (string) val: Rule Set
*/
private function storage( $val ) {
$storage = array();
$pos = 0;
// Find all possible occurences and build the replacement
while ( preg_match( $this->rcsw, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
if ( ! isset( $storage[$match[2][0]] ) ) {
$storage[$match[2][0]] = array($match[3][0] => $match[4][0]);
}
// Override double written properties
$storage[$match[2][0]][$match[3][0]] = $match[4][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Go through each tag for possible combination
foreach ( $storage as $tag => $arr ) {
// All three have to be defined
if ( count( $arr ) == 3 && ! $this->Combine->checkUncombinables( $arr ) ) {
$storage[$tag] = "$tag:" . $arr['width'] . ' ' . $arr['style'] . ' ' . $arr['color'] . ';';
}
else {
unset( $storage[$tag] );
}
}
return $storage;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in BorderOutline Class - " . $method );
}
}
}

View file

@ -0,0 +1,263 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_BorderRadius {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rspace: Checks for space without an escape '\' character before it
* @param (regex) rslash: Checks for unescaped slash character
* @param (array) borderRadius: Various border radii components
*/
private $Control;
private $Combine;
private $rspace = "/(?<!\\\)\s/";
private $rslash = "/(?<!\\\)\//";
private $borderRadius = array(
'css3' => array(
'mod' => '',
'base' => "/(^|(?<!\\\);)border-radius:(.*?)((?<!\\\);|$)/",
'full' => "/(^|(?<!\\\);)border-(top|bottom)-(left|right)-radius:(.*?)((?<!\\\);|$)/",
),
'moz' => array(
'mod' => '-moz-',
'base' => "/(^|(?<!\\\);)-moz-border-radius:(.*?)((?<!\\\);|$)/",
'full' => "/(^|(?<!\\\);)-moz-border-radius-(top|bottom)(left|right):(.*?)((?<!\\\);|$)/",
),
'webkit' => array(
'mod' => '-webkit-',
'base' => "/(^|(?<!\\\);)-webkit-border-radius:(.*?)((?<!\\\);|$)/",
'full' => "/(^|(?<!\\\);)-webkit-border-(top|bottom)-(left|right)-radius:(.*?)((?<!\\\);|$)/",
),
);
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Main handler to combine border-radii into a single rule
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
foreach ( $this->borderRadius as $regex ) {
$val = $this->fix( $val, $regex );
}
return $val;
}
/**
* Does the actual combining
*
* @param (string) val: Rule Set
*/
private function fix( $val, $regex ) {
$val = $this->base( $val, $regex );
$replace = $regex['mod'];
// Storage builder
if ( ( $storage = $this->storage( $val, $regex ) ) === false ) {
return $val;
}
// Setup horizontal/vertical radii
foreach ( $storage as $dir => &$config ) {
// Verticals are optional
if ( $dir == 'vertical' && ! $config['keep'] ) {
break;
}
// All 4 are the same
else if ( $config['top-left'] == $config['top-right'] &&
$config['top-right'] == $config['bottom-right'] &&
$config['bottom-right'] == $config['bottom-left'] ) {
$config['replace'] .= $config['top-left'];
}
// Opposites are the same
else if ( $config['top-left'] == $config['bottom-right'] && $config['top-right'] == $config['bottom-left'] ) {
$config['replace'] .= $config['top-left'] . ' ' . $config['top-right'];
}
// 3-point directional
else if ( $config['top-right'] == $config['bottom-left'] ) {
$config['replace'] .= $config['top-left'] . ' ' . $config['top-right'] . ' ' . $config['bottom-right'];
}
// none are the same, but can still use shorthand notation
else {
$config['replace'] .= $config['top-left'] . ' ' . $config['top-right'] . ' '
. $config['bottom-right'] . ' ' . $config['bottom-left'];
}
}
// Now rebuild the string replacing all instances of margin/padding if shorthand exists
$pos = 0;
$replace = $regex['mod'] . "border-radius:" . $storage['horizontal']['replace'] . $storage['vertical']['replace'] . ';';
while ( preg_match( $regex['full'], $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
$replace = '';
}
// Return converted val
return $val;
}
/**
* Expands short handed border radius props for combination
*
* @param (string) val: Rule Set
*/
private function base( $val, $regex ) {
$pos = 0;
while ( preg_match( $regex['base'], $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$replace = '';
$colon = strlen( $match[1][0] );
$parts = preg_split( $this->rslash, trim( $match[2][0] ), 2 );
$positions = array(
'top-left' => 0,
'top-right' => 0,
'bottom-right' => 0,
'bottom-left' => 0,
);
$base = array(
'horizontal' => array(
'parts' => preg_split( $this->rspace, trim( $parts[0] ) ),
'pos' => $positions,
),
'vertical' => array(
'parts' => isset( $parts[1] ) ? preg_split( $this->rspace, trim( $parts[1] ) ) : '',
'pos' => $positions,
),
);
foreach ( $base as &$config ) {
// Skip uncombinables
if ( $this->Combine->checkUncombinables( $config['parts'] ) ) {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
continue 2;
}
// Might not have verticals
else if ( $config['parts'] === '' ) {
continue;
}
// Each position needs a value
switch ( count( $config['parts'] ) ) {
case 1:
$config['pos']['top-left'] = $config['pos']['top-right'] = $config['parts'][0];
$config['pos']['bottom-left'] = $config['pos']['bottom-right'] = $config['parts'][0];
break;
case 2:
$config['pos']['top-left'] = $config['pos']['bottom-right'] = $config['parts'][0];
$config['pos']['bottom-left'] = $config['pos']['top-right'] = $config['parts'][1];
break;
case 3:
$config['pos']['top-left'] = $config['parts'][0];
$config['pos']['bottom-left'] = $config['pos']['top-right'] = $config['parts'][1];
$config['pos']['bottom-right'] = $config['parts'][2];
break;
case 4:
$config['pos']['top-left'] = $config['parts'][0];
$config['pos']['top-right'] = $config['parts'][1];
$config['pos']['bottom-right'] = $config['parts'][2];
$config['pos']['bottom-left'] = $config['parts'][3];
break;
default:
continue 2;
}
}
// Build the replacement
foreach ( $positions as $p => $v ) {
if ( $regex['mod'] == '-moz-' ) {
$replace .= "-moz-border-radius-" . preg_replace( "/-/", '', $p ) . ":"
. $base['horizontal']['pos'][$p]
. ( $base['vertical']['parts'] === '' ? '' : ' ' . $base['vertical']['pos'][$p] )
. ';';
}
else {
$replace .= $regex['mod'] . "border-$p-radius:"
. $base['horizontal']['pos'][$p]
. ( $base['vertical']['parts'] === '' ? '' : ' ' . $base['vertical']['pos'][$p] )
. ';';
}
}
$pos += strlen( $replace );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
}
return $val;
}
/**
* Builds the storage object for border radius props
*
* @param (string) val: Rule Set
* @param (array) regex: Current border radius type checking props
*/
private function storage( $val, $regex ) {
$storage = array(
'horizontal' => array('replace' => ''),
'vertical' => array(
'replace' => '',
'keep' => false,
),
);
// Find all possible occurences of this border-radius type and mark their directional value
$pos = 0;
while ( preg_match( $regex['full'], $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
$parts = preg_split( $this->rspace, $match[4][0], 2 );
$storage['horizontal'][$match[2][0] . '-' . $match[3][0]] = trim( $parts[0] );
if ( isset( $parts[1] ) ) {
$storage['vertical'][$match[2][0] . '-' . $match[3][0]] = trim( $parts[1] );
$storage['vertical']['keep'] = true;
$storage['vertical']['replace'] = '/';
}
else {
$storage['vertical'][$match[2][0] . '-' . $match[3][0]] = '0';
}
}
// Only combine if all 4 definitions are found (5 including replace)
if ( count( $storage['horizontal'] ) != 5 ||
$this->Combine->checkUncombinables( $storage['horizontal'] ) ||
$this->Combine->checkUncombinables( $storage['vertical'] ) ) {
return false;
}
return $storage;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in BorderRadius Class - " . $method );
}
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_Font {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rfont: Font matching
* @param (array) groupings: Set of font combinationals
*/
private $Control;
private $Combine;
private $rfont = "/(^|(?<!\\\);)(font|line)-(style|variant|weight|size|height|family):(.*?)((?<!\\\);|$)/";
private $groupings = array(
array('font-style', 'font-variant', 'font-weight', 'size/height', 'font-family'),
array('font-style', 'font-variant', 'font-weight', 'font-size', 'font-family'),
array('font-style', 'font-variant', 'size/height', 'font-family'),
array('font-style', 'font-variant', 'font-size', 'font-family'),
array('font-style', 'font-weight', 'size/height', 'font-family'),
array('font-style', 'font-weight', 'font-size', 'font-family'),
array('font-variant', 'font-weight', 'size/height', 'font-family'),
array('font-variant', 'font-weight', 'font-size', 'font-family'),
array('font-weight', 'size/height', 'font-family'),
array('font-weight', 'font-size', 'font-family'),
array('font-variant', 'size/height', 'font-family'),
array('font-variant', 'font-size', 'font-family'),
array('font-style', 'size/height', 'font-family'),
array('font-style', 'font-size', 'font-family'),
array('size/height', 'font-family'),
array('font-size', 'font-family'),
);
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines multiple font-definitions into single definition
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
$storage = $this->storage( $val );
// Loop through each property check and see if they can be replaced
foreach ( $this->groupings as $props ) {
if ( $replace = $this->Combine->searchDefinitions( 'font', $storage, $props ) ) {
break;
}
}
// If replacement string found, run it on all declarations
if ( $replace ) {
$pos = 0;
while ( preg_match( $this->rfont, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
if ( ! isset( $storage['line-height'] ) && stripos( $match[0][0], 'line-height') === 0 ) {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
continue;
}
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
$replace = '';
}
}
// Return converted val
return $val;
}
/**
* Builds a storage object for iteration
*
* @param (string) val: Rule Set
*/
private function storage( $val ) {
$storage = array();
// Find all possible occurences and build the replacement
$pos = 0;
while ( preg_match( $this->rfont, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$storage[$match[2][0] . '-' . $match[3][0]] = $match[4][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Combine font-size & line-height if possible
if ( isset( $storage['font-size'] ) && isset( $storage['line-height'] ) ) {
$storage['size/height'] = $storage['font-size'] . '/' . $storage['line-height'];
unset( $storage['font-size'], $storage['line-height'] );
}
return $storage;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Font Class - " . $method );
}
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_List {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rlist: List style matching
* @param (array) groupings: Group of list combinationals
*/
private $Control;
private $Combine;
private $rlist = "/(^|(?<!\\\);)list-style-(type|position|image):(.*?)((?<!\\\);|$)/";
private $groupings = array(
array('type', 'position', 'image'),
array('type', 'position'),
array('type', 'image'),
array('position', 'image'),
array('type'),
array('position'),
array('image'),
);
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines multiple list style props into single definition
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
// If replacement string found, run it on all declarations
if ( ( $replace = $this->replace( $val ) ) !== false ) {
$pos = 0;
while ( preg_match( $this->rlist, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
$replace = '';
}
}
// Return converted val
return $val;
}
/**
* Build the replacement string for list props
*
* @param (string) val: Rule Set
*/
private function replace( $val ) {
$storage = array();
$pos = 0;
// Find all possible occurences and build the replacement
while ( preg_match( $this->rlist, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$storage[$match[2][0]] = $match[3][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Run background checks and get replacement str
foreach ( $this->groupings as $props ) {
if ( $replace = $this->Combine->searchDefinitions( 'list-style', $storage, $props ) ) {
return $replace;
}
}
return false;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in List Class - " . $method );
}
}
}

View file

@ -0,0 +1,187 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Combine_MarginPadding {
/**
* Combine Patterns
*
* @class Control: Compression Controller
* @class Combine: Combine Controller
* @param (regex) rspace: Checks for space without an escape '\' character before it
* @param (regex) rmp: Margin/Padding matching
* @param (regex) rmpbase: Margin/Padding base match
*/
private $Control;
private $Combine;
private $rspace = "/(?<!\\\)\s/";
private $rmp = "/(^|(?<!\\\);)(margin|padding)-(top|right|bottom|left):(.*?)((?<!\\\);|$)/";
private $rmpbase = "/(^|(?<!\\\);)(margin|padding):(.*?)((?<!\\\);|$)/";
/**
* Stash a reference to the controller & combiner
*
* @param (class) control: CSSCompression Controller
* @param (class) combine: CSSCompression Combiner
*/
public function __construct( CSSCompression_Control $control, CSSCompression_Combine $combine ) {
$this->Control = $control;
$this->Combine = $combine;
}
/**
* Combines multiple directional properties of
* margin/padding into single definition.
*
* @param (string) val: Rule Set
*/
public function combine( $val ) {
$val = $this->expand( $val );
$storage = $this->storage( $val );
$pos = 0;
// Now rebuild the string replacing all instances of margin/padding if shorthand exists
while ( preg_match( $this->rmp, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$prop = $match[2][0];
if ( isset( $storage[$prop] ) ) {
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $storage[$prop], $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $storage[$prop] ) - $colon - 1;
$storage[$prop] = '';
}
else {
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
}
// Return converted val
return $val;
}
/**
* Build the storage object for iteration
*
* @param (string) val: Rule Set
*/
private function storage( $val ) {
$storage = array();
$pos = 0;
// Find all possible occurences of margin/padding and mark their directional value
while ( preg_match( $this->rmp, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
if ( ! isset( $storage[$match[2][0]] ) ) {
$storage[$match[2][0]] = array($match[3][0] => $match[4][0]);
}
// Override double written properties
$storage[$match[2][0]][$match[3][0]] = $match[4][0];
$pos = $match[0][1] + strlen( $match[0][0] ) - 1;
}
// Go through each tag for possible combination
foreach ( $storage as $tag => $arr ) {
// Only combine if all 4 definitions are found
if ( count( $arr ) == 4 && ! $this->Combine->checkUncombinables( $arr ) ) {
// All 4 are the same
if ( $arr['top'] == $arr['bottom'] && $arr['left'] == $arr['right'] && $arr['top'] == $arr['left'] ) {
$storage[$tag] = "$tag:" . $arr['top'] . ';';
}
// Opposites are the same
else if ( $arr['top'] == $arr['bottom'] && $arr['left'] == $arr['right'] ) {
$storage[$tag] = "$tag:" . $arr['top'] . ' ' . $arr['left'] . ';';
}
// 3-point directional
else if ( $arr['right'] == $arr['left'] ) {
$storage[$tag] = "$tag:" . $arr['top'] . ' ' . $arr['right'] . ' ' . $arr['bottom'] . ';';
}
// none are the same, but can still use shorthand notation
else {
$storage[$tag] = "$tag:" . $arr['top'] . ' ' . $arr['right'] . ' ' . $arr['bottom'] . ' ' . $arr['left'] . ';';
}
}
else {
unset( $storage[$tag] );
}
}
return $storage;
}
/**
* Explodes shorthanded margin/padding properties for later combination
*
* @param (string) val: Rule set
*/
private function expand( $val ) {
$pos = 0;
while ( preg_match( $this->rmpbase, $val, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$replace = '';
$prop = $match[2][0];
$value = preg_split( $this->rspace, trim( $match[3][0] ) );
$positions = array(
'top' => 0,
'right' => 0,
'bottom' => 0,
'left' => 0,
);
// Skip uncombinables
if ( $this->Combine->checkUncombinables( $value ) ) {
$pos = $match[0][1] + strlen( $match[0][0] );
continue;
}
// Each position needs a value
switch ( count( $value ) ) {
case 1:
$positions['top'] = $positions['right'] = $positions['bottom'] = $positions['left'] = $value[0];
break;
case 2:
$positions['top'] = $positions['bottom'] = $value[0];
$positions['right'] = $positions['left'] = $value[1];
break;
case 3:
$positions['top'] = $value[0];
$positions['right'] = $positions['left'] = $value[1];
$positions['bottom'] = $value[2];
break;
case 4:
$positions['top'] = $value[0];
$positions['right'] = $value[1];
$positions['bottom'] = $value[2];
$positions['left'] = $value[3];
break;
default:
continue;
}
// Build the replacement
foreach ( $positions as $p => $v ) {
$replace .= "$prop-$p:$v;";
}
$colon = strlen( $match[1][0] );
$val = substr_replace( $val, $replace, $match[0][1] + $colon, strlen( $match[0][0] ) - $colon );
$pos = $match[0][1] + strlen( $replace ) - $colon - 1;
}
return $val;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in MarginPadding Class - " . $method );
}
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Compress {
/**
* Trim Patterns
*
* @param (array) options: Reference to options
* @param (array) stats: Reference to stats
* @param (regex) rsemicolon: Checks for semicolon without an escape '\' character before it
* @param (regex) rcolon: Checks for colon without an escape '\' character before it
* @param (regex) rspace: Checks for space without an escape '\' character before it
*/
private $options = array();
private $stats = array();
private $rsemicolon = "/(?<!\\\);/";
private $rcolon = "/(?<!\\\):/";
private $rspace = "/(?<!\\\)\s/";
/**
* Other inner classes that get used within compression
*
* @class Control: Compression Controller
* @class Trim: Trim Instance
* @class Setup: Setup Instance
* @class Format: Formatting Instance
* @class Combine: Combine Instance
* @class Cleanup: Cleanup Instance
* @class Organize: Organize Instance
* @class Selectors: Selectors Instance
* @param (array) others: List of above classes for copying
*/
private $Control;
private $Trim;
private $Setup;
private $Format;
private $Combine;
private $Cleanup;
private $Organize;
private $Selectors;
private $others = array(
'Trim',
'Setup',
'Format',
'Combine',
'Cleanup',
'Organize',
'Selectors',
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = &$control->Option->options;
$this->stats = &$control->stats;
foreach ( $this->others as $class ) {
$this->$class = $control->$class;
}
}
/**
* Centralized function to run css compression.
*
* @param (string) css: Stylesheet to compresss
*/
public function compress( $css ) {
$setup = $this->setup( $css );
$setup = $this->rulesets( $setup );
$css = $this->readability( $setup );
// Attach plea to top of page with unknown blocks
if ( $this->options['add-unknown'] && count( $setup['unknown'] ) ) {
$css = "/*\nThere are unknown blocks in the sheet, please please please open an issue with your sheet attached to it:\n"
. "https://github.com/codenothing/css-compressor/issues\n"
. "Thank You --\n\n"
. implode( "\n", $setup['unknown'] )
. "\n*/\n"
. $css;
}
// Mark final file size
$this->stats['after']['size'] = strlen( $css = trim( $css ) );
// Return compressed css
return $css;
}
/**
* Runs css through initial setup handlers
*
* @param (string) css: Sheet to compress
*/
private function setup( $css ) {
// Initial stats
$this->stats['before']['time'] = microtime( true );
$this->stats['before']['size'] = strlen( $css );
// Initial trimming
$css = $this->Trim->trim( $css );
// Do a little tokenizing, compress each property individually
$setup = $this->Setup->setup( $css );
// Mark number of selectors pre-combine
$this->stats['before']['selectors'] = count( $setup['selectors'] );
return $setup;
}
/**
* Focus compressions on each rule set
*
* @param (array) setup: Array containing selectors and rule sets
*/
private function rulesets( $setup ) {
// Do selector specific compressions
$this->Selectors->selectors( $setup['selectors'] );
// Look at each group of properties as a whole, and compress/combine similiar definitions
$this->Combine->combine( $setup['selectors'], $setup['details'] );
// If order isn't important, run comination functions before and after compressions to catch all instances
// Be sure to prune before hand for higher chance of matching
if ( $this->options['organize'] ) {
$this->Cleanup->cleanup( $setup['selectors'], $setup['details'] );
$this->Organize->organize( $setup['selectors'], $setup['details'] );
$this->Combine->combine( $setup['selectors'], $setup['details'] );
}
// Do final maintenace work, remove injected property/values
$this->Cleanup->cleanup( $setup['selectors'], $setup['details'] );
// Run final counters before full cleanup
$this->finalCount( $setup['selectors'], $setup['details'] );
return $setup;
}
/**
* Runs final counts on selectors and props
*
* @param (array) selectors: Selector rules
* @param (array) details: Rule sets
*/
private function finalCount( $selectors, $details ) {
// Selectors and props
$this->stats['after']['selectors'] = count( $selectors );
foreach ( $details as $item ) {
$props = preg_split( $this->rsemicolon, $item );
// Make sure count is true
foreach ( $props as $k => $v ) {
if ( ! isset( $v ) || $v == '' ) {
unset( $props[$k] );
}
}
$this->stats['after']['props'] += count( $props );
}
// Final count for stats
$this->stats['after']['time'] = microtime( true );
}
/**
* Formats the compressed rule sets into a stylesheet
*
* @param (array) setup: Array containing selectors and rule sets
*/
private function readability( $setup ) {
// Format css to users preference
$css = $this->Format->readability( $this->options['readability'], $setup['selectors'], $setup['details'] );
// Intros
foreach ( $setup as $value ) {
if ( $value && is_string( $value ) ) {
$css = $value . $css;
}
}
// Remove escapables
$css = $this->Cleanup->removeInjections( $css );
return $css;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Compress Class - " . $method );
}
}
}

View file

@ -0,0 +1,233 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Control {
/**
* Control Patterns
*
* @param (string) css: Holds compressed css string
* @param (string) mode: Current compression mode state
* @param (array) stats: Holds compression stats
* @param (array) getters: Array of accessible getters
*/
public $css = '';
public $mode = '';
public $stats = array();
private $getters = array(
'css',
'mode',
'stats',
);
/**
* Subclasses that do the ground work for this compressor
*
* @class CSSCompression: Public facing compression class
* @class Option: Option handling
* @class Trim: Does the initial trimming for the css
* @class Format: Formats the output
* @class Numeric: Handles numeric compression
* @class Color: Handles color compression
* @class Individuals: Runs compression algorithms on individual properties and values
* @class Selectors: Runs selector specific compressions
* @class Combine: Handles combining of various properties
* @class Organize: Reorganizes the sheet for futher compression
* @class Cleanup: Cleans out all injected characters during compression
* @class Compress: Central compression unit.
* @param (array) subclasses: Array holding all the subclasses for inlusion
*/
public $CSSCompression;
public $Option;
public $Trim;
public $Format;
public $Numeric;
public $Color;
public $Individuals;
public $Selectors;
public $Combine;
public $Organize;
public $Cleanup;
public $Setup;
public $Compress;
private $subclasses = array(
'Option',
'Trim',
'Format',
'Numeric',
'Color',
'Individuals',
'Selectors',
'Combine',
'Organize',
'Cleanup',
'Setup',
'Compress',
);
/**
* Pull in the Compression instance and build the subclasses
*
* @param (class) CSSCompression: CSSCompression Instance
*/
public function __construct( CSSCompression $CSSCompression ) {
$this->CSSCompression = $CSSCompression;
// Load all subclasses on demand
if ( ! class_exists( "CSSCompression_Option", false ) ) {
$path = dirname(__FILE__) . '/';
foreach ( $this->subclasses as $class ) {
require( $path . $class . '.inc' );
}
}
// Initialize each subclass
foreach ( $this->subclasses as $class ) {
$full = "CSSCompression_$class";
$this->$class = new $full( $this );
}
}
/**
* Control access to properties
*
* - Getting stats/mode/css returns the current value of that property
* - Getting options will return the current full options array
* - Getting anything else returns that current value in the options array or NULL
*
* @param (string) name: Name of property that you want to access
*/
public function get( $name ) {
if ( in_array( $name, $this->getters ) ) {
return $this->$name;
}
else if ( $name == 'options' ) {
return $this->Option->options;
}
else {
return $this->Option->option( $name );
}
}
/**
* The setter method only allows access to setting values in the options array
*
* @param (string) name: Key name of the option you want to set
* @param (mixed) value: Value of the option you want to set
*/
public function set( $name, $value ) {
// Allow for passing array of options to merge into current ones
if ( $name === 'options' && is_array( $value ) ) {
return $this->Option->merge( $value );
}
else if ( isset( CSSCompression::$defaults[$name] ) ) {
return $this->Option->option( $name, $value );
}
else {
throw new CSSCompression_Exception( "Invalid Private Access to $name in CSSCompression." );
}
}
/**
* Merges a predefined set options
*
* @param (string) mode: Name of mode to use.
*/
public function mode( $mode = NULL ) {
return $this->Options->merge( $mode );
}
/**
* Resets options to their defaults, and flushes out variables
*
* @params none
*/
public function reset() {
$this->Option->reset();
return $this->flush();
}
/**
* Cleans out class variables for next run
*
* @params none
*/
public function flush() {
$this->css = '';
$this->stats = array(
'before' => array(
'props' => 0,
'selectors' => 0,
'size' => 0,
'time' => 0,
),
'after' => array(
'props' => 0,
'selectors' => 0,
'size' => 0,
'time' => 0,
),
);
return true;
}
/**
* Proxy to run Compression on the sheet passed. Handle options here.
*
* @param (string) css: Stylesheet to be compressed
* @param (mixed) options: Array of options or mode to use.
*/
public function compress( $css = NULL, $options = NULL ) {
// Flush out old stats and variables
$this->flush();
// If no additional options, just run compression
if ( $options === NULL ) {
return $this->css = $this->Compress->compress( $css );
}
// Store old params
$old = $this->mode == '__custom' ? $this->Option->option() : $this->mode;
$readability = $this->Option->option( 'readability' );
// Compress with new set of options
$this->Option->merge( $options );
$css = $this->Compress->compress( $css );
// Reset original options
$this->reset();
$this->Option->merge( $old );
// Return the compressed css
return $this->css = $css;
}
/**
* Backdoor access to subclasses
* ONLY FOR DEVELOPMENT/TESTING.
*
* @param (string) class: Name of the focus class
* @param (array) config: Contains name reference and test arguments
*/
public function access( $class, $method, $args ) {
if ( $class == 'Control' ) {
return call_user_func_array( array($class, $method), $args );
}
else if ( strpos( $class, '.' ) !== false ) {
$parts = explode( '.', $class );
$class = $parts[0];
$subclass = $parts[1];
return $this->$class->access( $subclass, $method, $args );
}
else if ( in_array( $class, $this->subclasses ) ) {
return $this->$class->access( $method, $args );
}
else {
throw new CSSCompression_Exception( "Unknown Class Access - " . $class );
}
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Exception extends Exception {
/**
* Custom exception handler
*
* @param (string) message: Error message
* @param (int) code: Error code
* @instance (Exception Instance) previous: Previous exception
*/
public function __construct( $message = 'Unknown Exception', $code = 0, Exception $previous = NULL ) {
parent::__construct( $message, $code, $previous );
}
/**
* String version of this custom exception
*
* @params none
*/
public function __toString() {
return "CSSCompression Exception: [" . $this->code . "] " . $this->message . "\n";
}
}

View file

@ -0,0 +1,184 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Format {
/**
* Format Patterns
*
* @class Control: Compression Controller
* @param (string) token: Copy of the injection token
* @param (array) options: Reference to options
* @param (regex) rsemicolon: Checks for semicolon without an escape '\' character before it
* @param (regex) rcolon: Checks for colon without an escape '\' character before it
* @param (array) readability: Mapping to readability functions
*/
private $Control;
private $token = '';
private $options = array();
private $rsemicolon = "/(?<!\\\);/";
private $rcolon = "/(?<!\\\):/";
private $readability = array(
CSSCompression::READ_MAX => 'maximum',
CSSCompression::READ_MED => 'medium',
CSSCompression::READ_MIN => 'minimum',
CSSCompression::READ_NONE => 'none',
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->token = CSSCompression::TOKEN;
$this->options = &$control->Option->options;
}
/**
* Reformats compressed CSS into specified format
*
* @param (int) readability: Readability level of compressed output
* @param (array) selectors: Array of selectors
* @param (array) details: Array of declarations
*/
public function readability( $readability = CSSCompression::READ_NONE, $selectors = array(), $details = array() ) {
if ( isset( $this->readability[$readability] ) ) {
$fn = $this->readability[$readability];
return trim( $this->$fn( $selectors, $details ) );
}
else {
return 'Invalid Readability Value';
}
}
/**
* Returns maxium readability, breaking on every selector, brace, and property
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of declarations
*/
private function maximum( $selectors, $details ) {
$css = '';
foreach ( $selectors as $k => $v ) {
if ( strpos( $v, $this->token ) === 0 ) {
$css .= substr( $v, strlen( $this->token ) );
$css .= $details[$k];
continue;
}
else if ( ! $details[$k] || trim( $details[$k] ) == '' ) {
continue;
}
$v = str_replace( '>', ' > ', $v );
$v = str_replace( '+', ' + ', $v );
$v = str_replace( ',', ', ', $v );
$css .= "$v {\n";
$arr = preg_split( $this->rsemicolon, $details[$k] );
foreach ( $arr as $item ) {
if ( ! $item ) {
continue;
}
list( $prop, $val ) = preg_split( $this->rcolon, $item, 2 );
$css .= "\t$prop: $val;\n";
}
// Kill that last semicolon at users request
if ( $this->options['unnecessary-semicolons'] ) {
$css = preg_replace( "/;\n$/", "\n", $css );
}
$css .= "}\n\n";
}
return $css;
}
/**
* Returns medium readability, putting selectors and rule sets on new lines
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of declarations
*/
private function medium( $selectors, $details ) {
$css = '';
foreach ( $selectors as $k => $v ) {
if ( strpos( $v, $this->token ) === 0 ) {
$css .= substr( $v, strlen( $this->token ) );
$css .= $details[$k];
continue;
}
else if ( $details[$k] && $details[$k] != '' ) {
$css .= "$v {\n\t" . $details[$k] . "\n}\n";
}
}
return $css;
}
/**
* Returns minimum readability, breaking after every selector and it's rule set
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of declarations
*/
private function minimum( $selectors, $details ) {
$css = '';
foreach ( $selectors as $k => $v ) {
if ( strpos( $v, $this->token ) === 0 ) {
$css .= substr( $v, strlen( $this->token ) );
$css .= $details[$k];
continue;
}
else if ( $details[$k] && $details[$k] != '' ) {
$css .= "$v{" . $details[$k] . "}\n";
}
}
return $css;
}
/**
* Returns an unreadable, but fully compressed script
*
* @param (array) selectors: Array of selectors
* @param (array) details: Array of declarations
*/
private function none( $selectors, $details ) {
$css = '';
foreach ( $selectors as $k => $v ) {
if ( strpos( $v, $this->token ) === 0 ) {
$css .= substr( $v, strlen( $this->token ) );
$css .= $details[$k];
continue;
}
else if ( $details[$k] && $details[$k] != '' ) {
$css .= trim( "$v{" . $details[$k] . "}" );
}
}
return $css;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Format Class - " . $method );
}
}
}

View file

@ -0,0 +1,304 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Individuals {
/**
* Individual patterns
*
* @class Control: Compression Controller
* @class Numeric: Numeric handler
* @class Color: Color Handler
* @param (array) options: Reference to options
* @param (regex) rdirectional: Properties that may have multiple directions
* @param (regex) rborderradius: Checks property for border-radius declaration
* @param (regex) rnoneprop: Properties that can have none as their value(will be converted to 0)
* @param (regex) rclip: Looks for rect grouping in clip declaration
* @param (regex) rsplitter: Checks font properties for font-size/line-height split
* @param (regex) rfilter: Special alpha filter for msie
* @param (regex) rspace: Checks for unescaped space
* @param (regex) rspace: Checks for unescaped slash
* @param (array) weights: Array of font-weight name conversions to their numeric counterpart
*/
private $Control;
private $Numeric;
private $Color;
private $options = array();
private $rdirectional = "/^(margin|padding|border-spacing)$/";
private $rborderradius = "/border[a-z-]*radius/";
private $rradiusfull = "/^(-moz-|-webkit-)?border-radius$/";
private $rnoneprop = "/^(border|background|border-(top|right|bottom|left))$/";
private $rclip = "/^rect\(\s*(\-?\d*\.?\d*?\w*)(,|\s)(\-?\d*\.?\d*?\w*)(,|\s)(\-?\d*\.?\d*?\w*)(,|\s)(\-?\d*\.?\d*?\w*)\s*\)$/";
private $rsplitter = "/(^|(?<!\\\)\s)([^\/ ]+)\/([^\/ ]+)((?<!\\\)\s|$)/";
private $rfilter = "/[\"']?PROGID\\\?:DXImageTransform\\\?.Microsoft\\\?.Alpha\(Opacity\\\?=(\d+\\\?\.?\d*)\)[\"']?/i";
private $rspace = "/(?<!\\\)\s/";
private $rslash = "/(?<!\\\)\//";
private $weights = array(
"normal" => 400,
"bold" => 700,
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->Numeric = $control->Numeric;
$this->Color = $control->Color;
$this->options = &$control->Option->options;
}
/**
* Runs special unit/directional compressions
*
* @param (string) prop: CSS Property
* @param (string) val: Value of CSS Property
*/
public function individuals( $prop, $val ) {
// Properties should always be lowercase
$prop = strtolower( $prop );
// Split up each definiton for color and numeric compressions
$parts = preg_split( $this->rspace, $val );
foreach ( $parts as &$v ) {
if ( ! $v || $v == '' ) {
continue;
}
// Remove uneeded decimals/units
if ( $this->options['format-units'] ) {
$v = $this->Numeric->numeric( $v );
}
// Color compression
$v = $this->Color->color( $v );
}
$val = trim( implode( ' ', $parts ) );
// Special border radius handling
if ( preg_match( $this->rborderradius, $prop ) ) {
$val = $this->borderRadius( $prop, $val );
}
// Remove uneeded side definitions if possible
else if ( $this->options['directional-compress'] && count( $parts ) > 1 && preg_match( $this->rdirectional, $prop ) ) {
$val = $this->directionals( strtolower( $val ) );
}
// Font-weight converter
if ( $this->options['fontweight2num'] && ( $prop == 'font-weight' || $prop == 'font' ) ) {
$val = $this->fontweight( $val );
}
// Special font value conversions
if ( $prop == 'font' ) {
$val = $this->font( $val );
}
// Special clip value compressions
if ( $prop == 'clip' ) {
$val = $this->clip( $val );
}
// None to 0 converter
$val = $this->none( $prop, $val );
// MSIE Filters
$val = $this->filter( $prop, $val );
// Return for list retrival
return array($prop, $val);
}
/**
* Preps border radius for directional compression
*
* @param (string) prop: Property Declaration
* @param (string) val: Declaration Value
*/
private function borderRadius( $prop, $val ) {
if ( preg_match( $this->rslash, $val ) ) {
$parts = preg_split( $this->rslash, $val, 2 );
// We have to redo numeric compression because the slash may hav intruded
foreach ( $parts as &$row ) {
$p = preg_split( $this->rspace, $row );
foreach ( $p as &$v ) {
if ( ! $v || $v == '' ) {
continue;
}
// Remove uneeded decimals/units
if ( $this->options['format-units'] ) {
$v = $this->Numeric->numeric( $v );
}
}
$row = implode( ' ', $p );
if ( $this->options['directional-compress'] ) {
$row = $this->directionals( strtolower( $row ) );
}
}
$val = implode( '/', $parts );
}
else if ( $this->options['directional-compress'] && preg_match( $this->rradiusfull, $prop ) ) {
$val = $this->directionals( strtolower( $val ) );
}
return $val;
}
/**
* Finds directional compression on methods like margin/padding
*
* @param (string) val: Value of CSS Property
*/
private function directionals( $val ) {
// Split up each definiton
$direction = preg_split( $this->rspace, $val );
// 4 Direction reduction
$count = count( $direction );
if ( $count == 4 ) {
// All 4 sides are the same, combine into 1 definition
if ( $direction[0] == $direction[1] && $direction[2] == $direction[3] && $direction[0] == $direction[3] ) {
$direction = array($direction[0]);
}
// top-bottom/left-right are the same, reduce definition
else if ( $direction[0] == $direction[2] && $direction[1] == $direction[3] ) {
$direction = array($direction[0], $direction[1]);
}
// Only left-right are the same
else if ( $direction[1] == $direction[3] ) {
$direction = array($direction[0], $direction[1], $direction[2]);
}
}
// 3 Direction reduction
else if ( $count == 3 ) {
// All directions are the same
if ( $direction[0] == $direction[1] && $direction[1] == $direction[2] ) {
$direction = array($direction[0]);
}
// Only top(first) and bottom(last) are the same
else if ( $direction[0] == $direction[2] ) {
$direction = array($direction[0], $direction[1]);
}
}
// 2 Direction reduction
// Both directions are the same, combine into single definition
else if ( $count == 2 && $direction[0] == $direction[1] ) {
$direction = array($direction[0]);
}
// Return the combined version of the directions
// Single entries will just return
return implode( ' ', $direction );
}
/**
* Converts font-weight names to numbers
*
* @param (string) val: font-weight prop value
*/
private function fontweight( $val ) {
if ( preg_match( $this->rspace, $val ) ) {
$parts = preg_split( $this->rspace, $val );
foreach ( $parts as &$item ) {
$lower = strtolower( $item );
if ( isset( $this->weights[$lower] ) && $lower != 'normal' ) {
$item = $this->weights[$lower];
}
}
$val = implode( ' ', $parts );
}
else if ( isset( $this->weights[strtolower( $val )] ) ) {
$val = $this->weights[strtolower( $val )];
}
return $val;
}
/**
* Special font conversions
*
* @param (string) val: property value
*/
private function font( $val ) {
// Split out the font-size/line-height split and run through numerical handlers
if ( preg_match( $this->rsplitter, $val, $match, PREG_OFFSET_CAPTURE ) ) {
$size = $this->Numeric->numeric( $match[2][0] );
$height = $this->Numeric->numeric( $match[3][0] );
$concat = $match[1][0] . $size . '/' . $height . $match[4][0];
$val = substr_replace( $val, $concat, $match[0][1], strlen( $match[0][0] ) );
}
return $val;
}
/**
* Special clip conversions
*
* @param (string) val: property value
*/
private function clip( $val ) {
if ( preg_match( $this->rclip, $val, $match ) ) {
$positions = array(1, 3, 5, 7);
$clean = 'rect(';
foreach ( $positions as $pos ) {
if ( ! isset( $match[$pos] ) ) {
return $val;
}
$clean .= $this->Numeric->numeric( $match[$pos] ) . ( isset( $match[$pos + 1] ) ? $match[$pos + 1] : '' );
}
$val = $clean . ')';
}
return $val;
}
/**
* Convert none vals to 0
*
* @param (string) prop: Current Property
* @param (string) val: property value
*/
private function none( $prop, $val ) {
if ( preg_match( $this->rnoneprop, $prop ) && $val == 'none' ) {
$val = '0';
}
return $val;
}
/**
* MSIE Filter Conversion
*
* @param (string) prop: Current Property
* @param (string) val: property value
*/
private function filter( $prop, $val ) {
if ( preg_match( "/filter/", $prop ) ) {
$val = preg_replace( $this->rfilter, "alpha(opacity=$1)", $val );
}
return $val;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Individuals Class - " . $method );
}
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Numeric {
/**
* Numeric Patterns
*
* @class Control: Compression Controller
* @param (array) options: Reference to options
* @param (regex) rdecimal: Checks for zero decimal
* @param (regex) rzero: Checks for preceding 0 to decimal unit
* @param (regex) runit: Checks for suffix on 0 unit
*/
private $Control;
private $options = array();
private $rdecimal = "/^(\+|\-)?(\d*\.[1-9]*0*)(\%|[a-z]{2})$/i";
private $rzero = "/^(\+|\-)?0(\.\d+)(\%|[a-z]{2})?$/i";
private $runit = "/^0(\%|[a-z]{2})$/i";
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = &$control->Option->options;
}
/**
* Runs all numeric operations
*
* @param (string) str: Unit string
*/
public function numeric( $str ) {
$str = $this->decimal( $str );
$str = $this->zeroes( $str );
$str = $this->units( $str );
return $str;
}
/**
* Remove's unecessary decimal, ie 13.0px => 13px
*
* @param (string) str: Unit string
*/
private function decimal( $str ) {
if ( preg_match( $this->rdecimal, $str, $match ) ) {
$str = ( $match[1] == '-' ? '-' : '' ) . floatval( $match[2] ) . $match[3];
}
return $str;
}
/**
* Removes suffix from 0 unit, ie 0px; => 0;
*
* @param (string) str: Unit string
*/
private function units( $str ) {
if ( preg_match( $this->runit, $str, $match ) ) {
$str = '0';
}
return $str;
}
/**
* Removes leading zero in decimal, ie 0.33px => .33px
*
* @param (string) str: Unit string
*/
private function zeroes( $str ) {
if ( preg_match( $this->rzero, $str, $match ) ) {
$str = ( isset( $match[1] ) && $match[1] == '-' ? '-' : '' ) . $match[2] . ( isset( $match[3] ) ? $match[3] : '' );
}
return $str;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Numeric Class - " . $method );
}
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Option {
/**
* Option Patterns
*
* @class Control: Compression Controller
* @param (string) custom: Name of the custom mode
* @param (array) options: Instance settings
*/
private $Control;
private $custom = '__custom';
public $options = array();
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = CSSCompression::$defaults;
$control->mode = $this->custom;
}
/**
* Maintainable access to the options array
*
* - Passing no arguments returns the entire options array
* - Passing a single name argument returns the value for the option
* - Passing both a name and value, sets the value to the name key, and returns the value
* - Passing an array will merge the options with the array passed, for object like extension
*
* @param (mixed) name: The key name of the option
* @param (mixed) value: Value to set the option
*/
public function option( $name = NULL, $value = NULL ) {
if ( $name === NULL ) {
return $this->options;
}
else if ( is_array( $name ) ) {
return $this->merge( $name );
}
else if ( $value === NULL ) {
return isset( $this->options[$name] ) ? $this->options[$name] : NULL;
}
else {
// Readability doesn't signify custom settings
if ( $name != 'readability' ) {
$this->Control->mode = $this->custom;
}
return ( $this->options[$name] = $value );
}
}
/**
* Reset's the default options
*
* @params none;
*/
public function reset() {
// Reset and return the new options
return $this->options = CSSCompression::$defaults;
}
/**
* Extend like function to merge an array of preferences into
* the options array.
*
* @param (mixed) options: Array of preferences to merge into options
*/
public function merge( $options = array() ) {
$modes = CSSCompression::modes();
if ( $options && is_array( $options ) && count( $options ) ) {
$this->Control->mode = $this->custom;
foreach ( $this->options as $key => $value ) {
if ( ! isset( $options[$key] ) ) {
continue;
}
else if ( strtolower( $options[$key] ) == 'on' ) {
$this->options[$key] = true;
}
else if ( strtolower( $options[$key] ) == 'off' ) {
$this->options[$key] = false;
}
else {
$this->options[$key] = intval( $options[$key] );
}
}
}
else if ( $options && is_string( $options ) && array_key_exists( $options, $modes ) ) {
$this->Control->mode = $options;
// Default all to true, the mode has to force false
foreach ( $this->options as $key => $value ) {
if ( $key != 'readability' ) {
$this->options[$key] = true;
}
}
// Merge mode into options
foreach ( $modes[$options] as $key => $value ) {
$this->options[$key] = $value;
}
}
return $this->options;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Option Class - " . $method );
}
}
}

View file

@ -0,0 +1,146 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Organize {
/**
* Organize Patterns
*
* @class Control: Compression Controller
* @param (array) options: Reference to options
* @param (regex) rsemicolon: Checks for semicolon without an escape '\' character before it
* @param (regex) rlastsemi: Checks for semicolon at the end of the string
*/
private $Control;
private $options = array();
private $rsemicolon = "/(?<!\\\);/";
private $rlastsemi = "/(?<!\\\);$/";
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = &$control->Option->options;
}
/**
* Look to see if we can combine selectors to reduce the number
* of definitions.
*
* @param (array) selectors: Array of selectors, map directly to details
* @param (array) details: Array of rule sets, map directly to selectors
*/
public function organize( &$selectors = array(), &$details = array() ) {
// Combining defns based on similar selectors
list( $selectors, $details ) = $this->reduceSelectors( $selectors, $details );
// Combining defns based on similar details
list( $selectors, $details ) = $this->reduceDetails( $selectors, $details );
// Return in package form
return array($selectors, $details);
}
/**
* Combines multiply defined selectors by merging the rule sets,
* latter declarations overide declaratins at top of file
*
* @param (array) selectors: Array of selectors broken down by setup
* @param (array) details: Array of rule sets broken down by setup
*/
private function reduceSelectors( $selectors, $details ) {
$keys = array_keys( $selectors );
$max = array_pop( $keys ) + 1;
for ( $i = 0; $i < $max; $i++ ) {
if ( ! isset( $selectors[$i] ) ) {
continue;
}
for ( $k = $i + 1; $k < $max; $k++ ) {
if ( ! isset( $selectors[$k] ) ) {
continue;
}
if ( $selectors[$i] == $selectors[$k] ) {
// Prevent noticies
if ( ! isset( $details[$i] ) ) {
$details[$i] = '';
}
if ( ! isset( $details[$k] ) ) {
$details[$k] = '';
}
// We kill the last semicolon before organization, so account for that.
if ( $details[$i] != '' && $details[$k] != '' && ! preg_match( $this->rlastsemi, $details[$i] ) ) {
$details[$i] .= ';' . $details[$k];
}
else {
$details[$i] .= $details[$k];
}
// Remove the second part
unset( $selectors[$k], $details[$k] );
}
}
}
return array($selectors, $details);
}
/**
* Combines multiply defined rule sets by merging the selectors
* in comma seperated format
*
* @param (array) selectors: Array of selectors broken down by setup
* @param (array) details: Array of rule sets broken down by setup
*/
private function reduceDetails( $selectors, $details ) {
$keys = array_keys( $selectors );
$max = array_pop( $keys ) + 1;
for ( $i = 0; $i < $max; $i++ ) {
if ( ! isset( $selectors[$i] ) ) {
continue;
}
$arr = preg_split( $this->rsemicolon, isset( $details[$i] ) ? $details[$i] : '' );
for ( $k = $i + 1; $k < $max; $k++ ) {
if ( ! isset( $selectors[$k] ) ) {
continue;
}
$match = preg_split( $this->rsemicolon, isset( $details[$k] ) ? $details[$k] : '' );
$x = array_diff( $arr, $match );
$y = array_diff( $match, $arr );
if ( count( $x ) < 1 && count( $y ) < 1 ) {
$selectors[$i] .= ',' . $selectors[$k];
unset( $details[$k], $selectors[$k] );
}
}
}
return array($selectors, $details);
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Organize Class - " . $method );
}
}
}

View file

@ -0,0 +1,242 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Selectors {
/**
* Selector patterns
*
* @class Control: Compression Controller
* @param (string) token: Copy of the injection token
* @param (regex) ridattr: ID Attribute matcher (combined with token)
* @param (regex) rclassattr: class Attribute matcher (combined with token)
* @param (array) options: Reference to options
* @param (regex) rmark: Stop points during selector parsing
* @param (regex) ridclassend: End of a id/class string
* @param (regex) rescapedspace: for replacement in class attributes
* @param (regex) rquote: Checks for the next quote character
* @param (regex) rcomma: looks for an unescaped comma character
* @param (regex) rspace: looks for an unescaped space character
* @param (regex) rid: looks for an unescaped hash character
* @param (regex) rpseudo: Add space after first-letter|line pseudo selector
* --- when it occurs before comma or rule set
*/
private $Control;
private $token = '';
private $ridattr = "";
private $rclassattr = "";
private $options = array();
private $rmark = "/(?<!\\\)(#|\.|=)/";
private $ridclassend = "/(?<!\\\)[:#>~\[\+\*\. ]/";
private $rquote = "/(?<!\\\)(\"|')?\]/";
private $rescapedspace = "/\\\ /";
private $rcomma = "/(?<!\\\),/";
private $rspace = "/(?<!\\\)\s/";
private $rid = "/(?<!\\\)#/";
private $rpseudo = "/:first-(letter|line)(,|$)/i";
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->token = CSSCompression::TOKEN;
$this->ridattr = "/\[id=$this->token(.*?)$this->token\]/";
$this->rclassattr = "/\[class=$this->token(.*?)$this->token\]/";
$this->options = &$control->Option->options;
}
/**
* Selector specific optimizations
*
* @param (array) selectors: Array of selectors
*/
public function selectors( &$selectors = array() ) {
foreach ( $selectors as &$selector ) {
// Auto ignore sections
if ( strpos( $selector, $this->token ) === 0 ) {
continue;
}
// Smart casing and token injection
$selector = $this->parse( $selector );
// Converting attr to shorthanded selectors
if ( $this->options['attr2selector'] ) {
// Use id hash instead of id attr
$selector = $this->idAttribute( $selector );
// Use class notation instead of class attr
$selector = $this->classAttribute( $selector );
}
// Remove everything before final id in a selector
if ( $this->options['strict-id'] ) {
$selector = $this->strictid( $selector );
}
// Get rid of possible repeated selectors
$selector = $this->repeats( $selector );
// Add space after pseudo selectors (so ie6 doesn't complain)
if ( $this->options['pseudo-space'] ) {
$selector = $this->pseudoSpace( $selector );
}
}
return $selectors;
}
/**
* Converts selectors like BODY => body, DIV => div
* and injects tokens wrappers for attribute values
*
* @param (string) selector: CSS Selector
*/
private function parse( $selector ) {
$clean = '';
$substr = '';
$pos = 0;
while ( preg_match( $this->rmark, $selector, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$substr = substr( $selector, $pos, $match[0][1] + 1 - $pos );
$clean .= $this->options['lowercase-selectors'] ? strtolower( $substr ) : $substr;
$pos = $match[0][1] + strlen( $match[1][0] );
// Class or id match
if ( $match[1][0] == '#' || $match[1][0] == '.' ) {
if ( preg_match( $this->ridclassend, $selector, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
$clean .= substr( $selector, $pos, $m[0][1] - $pos );
$pos = $m[0][1];
}
else {
$clean .= substr( $selector, $pos );
$pos = strlen( $selector );
break;
}
}
// Start of a string
else if ( preg_match( $this->rquote, $selector, $m, PREG_OFFSET_CAPTURE, $pos ) ) {
if ( $selector[$pos] == "\"" || $selector[$pos] == "'" ) {
$pos++;
}
$clean .= $this->token . substr( $selector, $pos, $m[0][1] - $pos ) . $this->token . ']';
$pos = $m[0][1] + strlen( $m[0][0] );
}
// No break points left
else {
$clean .= substr( $selector, $pos );
$pos = strlen( $selector );
break;
}
}
return $clean . ( $this->options['lowercase-selectors'] ? strtolower( substr( $selector, $pos ) ) : substr( $selector, $pos ) );
}
/**
* Convert [id=blah] attribute selectors into id form selector (#blah)
*
* @param (string) selector: CSS Selector
*/
private function idAttribute( $selector ) {
$pos = 0;
while ( preg_match( $this->ridattr, $selector, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
// Don't convert if space found (not valid hash selector)
if ( strpos( $match[1][0], ' ' ) !== false ) {
$pos = $match[0][1] + strlen( $match[0][0] );
continue;
}
$selector = substr_replace( $selector, '#' . $match[1][0], $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $match[1][0] ) + 1;
}
return $selector;
}
/**
* Convert [class=blah] attribute selectors into class form selector (.blah)
*
* @param (string) selector: CSS Selector
*/
private function classAttribute( $selector ) {
$pos = 0;
while ( preg_match( $this->rclassattr, $selector, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
// Don't convert if prescense of dot separator found
if ( strpos( $match[1][0], '.' ) !== false ) {
$pos = $match[0][1] + strlen( $match[0][0] );
continue;
}
$replace = '.' . preg_replace( $this->rescapedspace, ".", $match[1][0] );
$selector = substr_replace( $selector, $replace, $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $match[1][0] ) + 1;
}
return $selector;
}
/**
* Promotes nested id's to the front of the selector
*
* @param (string) selector: CSS Selector
*/
private function strictid( $selector ) {
$parts = preg_split( $this->rcomma, $selector );
foreach ( $parts as &$s ) {
if ( preg_match( $this->rid, $s ) ) {
$p = preg_split( $this->rid, $s );
$s = '#' . array_pop( $p );
}
}
return implode( ',', $parts );
}
/**
* Removes repeated selectors that have been comma separated
*
* @param (string) selector: CSS Selector
*/
private function repeats( $selector ) {
$parts = preg_split( $this->rcomma, $selector );
$parts = array_flip( $parts );
$parts = array_flip( $parts );
return implode( ',', $parts );
}
/**
* Adds space after pseudo selector for ie6 like a:first-child{ => a:first-child {
*
* @param (string) selector: CSS Selector
*/
private function pseudoSpace( $selector ) {
return preg_replace( $this->rpseudo, ":first-$1 $2", $selector );
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
if ( $method == 'selectors' ) {
return $this->selectors( $args[0] );
}
else {
return call_user_func_array( array($this, $method), $args );
}
}
else {
throw new CSSCompression_Exception( "Unknown method in Selectors Class - " . $method );
}
}
}

View file

@ -0,0 +1,289 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Setup {
/**
* Trim Patterns
*
* @class Control: Compression Controller
* @class Individuals: Individuals Instance
* @instance instance: CSSCompression Instance
* @param (string) token: Copy of the injection token
* @param (array) options: Reference to options
* @param (array) stats: Reference to stats
* @param (regex) rsemicolon: Checks for semicolon without an escape '\' character before it
* @param (regex) rcolon: Checks for colon without an escape '\' character before it
* @param (regex) rbang: Checks for '!' without an escape '\' character before it
* @param (regex) rspacebank: Checks for an unescaped space before a bang character
* @param (regex) rliner: Matching known 1-line intros
* @param (regex) rnested: Matching known subsection handlers
* @param (regex) rurl: url wrapper matching
* @param (regex) rsinglequote: Checks for unescaped escaped single quote (mouthfull)
* @param (array) rsetup: Expanding stylesheet for semi-tokenizing
*/
private $Control;
private $Individuals;
private $instance;
private $token = '';
private $options = array();
private $stats = array();
private $rsemicolon = "/(?<!\\\);/";
private $rcolon = "/(?<!\\\):/";
private $rbang = "/(?<!\\\)\!/";
private $rspacebang = "/(?<!\\\)\s\!/";
private $rliner = "/^@(import|charset|namespace)/i";
private $rmedia = "/^@media/i";
private $rurl = "/url\((.*?)\)/";
private $rsinglequote = "/(?<!\\\)\\\'/";
private $rsetup = array(
'patterns' => array(
"/(?<!\\\){/",
"/(?<!\\\)}/",
"/(?<!\\\)@/",
"/(@(charset|import)[^;]*(?<!\\\);)/",
),
'replacements' => array(
"\n{\n",
"\n}\n",
"\n@",
"$1\n",
),
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->Individuals = $control->Individuals;
$this->token = CSSCompression::TOKEN;
$this->options = &$control->Option->options;
$this->stats = &$control->stats;
}
/**
* Setup selector and details arrays for compression methods
*
* @param (string) css: Trimed stylesheet
*/
public function setup( $css ) {
// Seperate the element from the elements details
$css = explode( "\n", preg_replace( $this->rsetup['patterns'], $this->rsetup['replacements'], $css ) );
$newline = $this->options['readability'] > CSSCompression::READ_NONE ? "\n" : '';
$setup = array(
'selectors' => array(),
'details' => array(),
'unknown' => array(),
'introliner' => '',
'namespace' => '',
'import' => '',
'charset' => '',
);
while ( count( $css ) ) {
$row = trim( array_shift( $css ) );
if ( $row == '' ) {
continue;
}
// Single block At-Rule set
else if ( $row[0] == '@' && $css[0] == '{' && trim( $css[1] ) != '' && $css[2] == '}' ) {
// Stash selector
array_push( $setup['selectors'], $row );
// Stash details (after the opening brace)
array_push( $setup['details'], $this->details( trim( $css[1] ) ) );
// drop the details from the stack
$css = array_slice( $css, 3 );
}
// Single line At-Rules (import/charset/namespace)
else if ( preg_match( $this->rliner, $row, $match ) ) {
$setup[$match[1]] .= $this->liner( $row ) . $newline;
}
// Nested At-Rule declaration blocks
else if ( $row[0] == '@' && $css[0] == '{' ) {
// Stash atrule as selector
array_push( $setup['selectors'], $this->token . $row );
// Stash details (after the opening brace)
array_push( $setup['details'], $this->nested( $css, preg_match( $this->rmedia, $row ) ) . $newline );
}
// Unknown single line At-Rules
else if ( $row[0] == '@' && substr( $row, -1 ) == ';' ) {
$setup['introliner'] .= $row . $newline;
}
// Declaration Block
else if ( count( $css ) >= 3 && $css[0] == '{' && $css[2] == '}' ) {
// Stash selector
array_push( $setup['selectors'], $row );
// Stash details (after the opening brace)
array_push( $setup['details'], $this->details( trim( $css[1] ) ) );
// drop the details from the stack
$css = array_slice( $css, 3 );
}
// Last catch, store unknown artifacts as selectors with a token
// and give it an empty rule set
else {
// Still add to unknown stack, for notification
array_push( $setup['unknown'], $row );
// Stash unknown artifacts as selectors with a token
array_push( $setup['selectors'], $this->token . $row );
// Give it an empty rule set
array_push( $setup['details'], '' );
}
}
return $setup;
}
/**
* Run nested elements through a separate instance of compression
*
* @param (array) css: Reference to the original css array
* @param (bool) organize: Whether or not to organize the subsection (only true for media sections)
*/
private function nested( &$css = array(), $organize = false ) {
$options = $this->options;
$left = 0;
$right = 0;
$row = '';
$independent = '';
$content = '';
$spacing = '';
$newline = $this->options['readability'] > CSSCompression::READ_NONE ? "\n" : '';
// Find the end of the nested section
while ( count( $css ) && ( $left < 1 || $left > $right ) ) {
$row = trim( array_shift( $css ) );
if ( $row == '' ) {
continue;
}
else if ( $row == '{' ) {
$left++;
}
else if ( $row == '}' ) {
$right++;
}
else if ( count( $css ) && substr( $row, 0, 1 ) != '@' && substr( $css[0], 0, 1 ) == '@' && substr( $row, -1 ) == ';' ) {
$independent .= $row;
continue;
}
$content .= $row;
}
// Ensure copy of instance exists
if ( ! $this->instance ) {
$this->instance = new CSSCompression();
}
// Fresh start
$this->instance->reset();
// Compress the nested section independently after removing the wrapping braces
// Also make sure to only organize media sections
if ( $options['organize'] == true && $organize == false ) {
$options['organize'] = false;
}
// Independent sections should be prepended to the next compressed section
$content = ( $independent == '' ? '' : $independent . $newline )
. $this->instance->compress( substr( $content, 1, -1 ), $options );
// Formatting for anything higher then 0 readability
if ( $newline == "\n" ) {
$content = "\n\t" . str_replace( "\n", "\n\t", $content ) . "\n";
$spacing = $this->options['readability'] > CSSCompression::READ_MIN ? ' ' : '';
}
// Stash the compressed nested script
return "$spacing{" . $content . "}$newline";
}
/**
* Converts import/namespace urls into strings
*
* @param (string) row: At-rule
*/
private function liner( $row ) {
$pos = 0;
while ( preg_match( $this->rurl, $row, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
$quote = preg_match( $this->rsinglequote, $match[1][0] ) ? '"' : "'";
$replace = $quote . $match[1][0] . $quote;
$row = substr_replace( $row, $replace, $match[0][1], strlen( $match[0][0] ) );
$pos = $match[0][1] + strlen( $replace );
}
return $row;
}
/**
* Run individual compression techniques on each property of a selector
*
* @param (string) row: Selector properties
*/
private function details( $row ) {
$row = preg_split( $this->rsemicolon, $row );
$parts = array();
$details = '';
foreach ( $row as $line ) {
// Set loopers
$parts = preg_split( $this->rcolon, $line, 2 );
$prop = '';
$value = '';
// Property
if ( isset( $parts[0] ) && ( $parts[0] = trim( $parts[0] ) ) != '' ) {
$prop = $parts[0];
}
// Value
if ( isset( $parts[1] ) && ( $parts[1] = trim( $parts[1] ) ) != '' ) {
$value = preg_replace( $this->rbang, ' !', $parts[1] );
}
// Fail safe, remove unspecified property/values
if ( $prop == '' || $value == '' ) {
continue;
}
// Run the tag/element through each compression
list( $prop, $value ) = $this->Individuals->individuals( $prop, $value );
// Add counter to before stats
$this->stats['before']['props']++;
// Store the compressed element
$details .= "$prop:" . preg_replace( $this->rspacebang, '!', $value ) . ";";
}
return $details;
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Setup Class - " . $method );
}
}
}

View file

@ -0,0 +1,210 @@
<?php
/**
* CSS Compressor [VERSION]
* [DATE]
* Corey Hart @ http://www.codenothing.com
*/
class CSSCompression_Trim {
/**
* Trim Patterns
*
* @class Control: Compression Controller
* @param (array) options: Reference to options
* @param (regex) rcmark: Marking point when traversing through sheet for comments
* @param (regex) rendcomment: Finds the ending comment point
* @param (regex) rendquote: Finds the ending quote point
* @param (regex) rendsinglequote: Finds the ending single quote point
* @param (array) rescape: Array of patterns of groupings that should be escaped
* @param (array) trimmings: Stylesheet trimming patterns/replacements
* @param (array) escaped: Array of characters that need to be escaped
*/
private $Control;
private $options = array();
private $rcmark = "/((?<!\\\)\/\*|(?<!\\\)\"|(?<!\\\)')/";
private $rendcomment = "/\*\//";
private $rendquote = "/(?<!\\\)\"/";
private $rendsinglequote = "/(?<!\\\)'/";
private $rescape = array(
"/(url\()([^'\"].*?)(\))/is",
"/((?<!\\\)\")(.*?)((?<!\\\)\")/s",
"/((?<!\\\)')(.*?)((?<!\\\)')/s",
);
private $trimmings = array(
'patterns' => array(
"/(?<!\\\)(\s+)?(?<!\\\)([!,{};>\~\+\/])(?<!\\\)(\s+)?/s", // Remove un-needed spaces around special characters
"/url\((?<!\\\)\"(.*?)(?<!\\\)\"\)/is", // Remove quotes from urls
"/url\((?<!\\\)'(.*?)(?<!\\\)'\)/is", // Remove single quotes from urls
"/url\((.*?)\)/is", // Lowercase url wrapper
"/(?<!\\\);{2,}/", // Remove unecessary semi-colons
"/(?<!\\\)\s+/s", // Compress all spaces into single space
),
'replacements' => array(
'$2',
'url($1)',
'url($1)',
'url($1)',
';',
' ',
),
);
private $escaped = array(
'search' => array(
":",
";",
"}",
"{",
"@",
"!",
",",
">",
"+",
"~",
"/",
"*",
".",
"=",
"#",
"\r",
"\n",
"\t",
" ",
),
'replace' => array(
"\\:",
"\\;",
"\\}",
"\\{",
"\\@",
"\\!",
"\\,",
"\\>",
"\\+",
"\\~",
"\\/",
"\\*",
"\\.",
"\\=",
"\\#",
"\\r",
"\\n",
"\\t",
"\\ ",
),
);
/**
* Stash a reference to the controller on each instantiation
*
* @param (class) control: CSSCompression Controller
*/
public function __construct( CSSCompression_Control $control ) {
$this->Control = $control;
$this->options = &$control->Option->options;
}
/**
* Central trim handler
*
* @param (string) css: Stylesheet to trim
*/
public function trim( $css ) {
$css = $this->comments( $css );
$css = $this->escape( $css );
$css = $this->strip( $css );
return $css;
}
/**
* Does a quick run through the script to remove all comments from the sheet,
*
* @param (string) css: Stylesheet to trim
*/
private function comments( $css ) {
$pos = 0;
while ( preg_match( $this->rcmark, $css, $match, PREG_OFFSET_CAPTURE, $pos ) ) {
switch ( $match[1][0] ) {
// Start of comment block
case "/*":
if ( preg_match( $this->rendcomment, $css, $m, PREG_OFFSET_CAPTURE, $match[1][1] + 1 ) ) {
$end = $m[0][1] - $match[1][1] + strlen( $m[0][0] );
$css = substr_replace( $css, '', $match[1][1], $end );
$pos = $match[0][1];
}
else {
$css = substr( $css, 0, $match[1][1] );
break 2;
}
break;
// Start of string
case "\"":
if ( preg_match( $this->rendquote, $css, $m, PREG_OFFSET_CAPTURE, $match[1][1] + 1 ) ) {
$pos = $m[0][1] + strlen( $m[0][0] );
}
else {
break 2;
}
break;
// Start of string
case "'":
if ( preg_match( $this->rendsinglequote, $css, $m, PREG_OFFSET_CAPTURE, $match[1][1] + 1 ) ) {
$pos = $m[0][1] + strlen( $m[0][0] );
}
else {
break 2;
}
break;
// Should have never gotten here
default:
break 2;
}
}
return $css;
}
/**
* Escape out possible splitter characters within urls
*
* @param (string) css: Full stylesheet
*/
private function escape( $css ) {
foreach ( $this->rescape as $regex ) {
$start = 0;
while ( preg_match( $regex, $css, $match, PREG_OFFSET_CAPTURE, $start ) ) {
$value = $match[1][0]
. str_replace( $this->escaped['search'], $this->escaped['replace'], $match[2][0] )
. $match[3][0];
$css = substr_replace( $css, $value, $match[0][1], strlen( $match[0][0] ) );
$start = $match[0][1] + strlen( $value ) + 1;
}
}
return $css;
}
/**
* Runs initial formatting to setup for compression
*
* @param (string) css: CSS Contents
*/
private function strip( $css ) {
// Run replacements
return trim( preg_replace( $this->trimmings['patterns'], $this->trimmings['replacements'], $css ) );
}
/**
* Access to private methods for testing
*
* @param (string) method: Method to be called
* @param (array) args: Array of paramters to be passed in
*/
public function access( $method, $args ) {
if ( method_exists( $this, $method ) ) {
return call_user_func_array( array($this, $method), $args );
}
else {
throw new CSSCompression_Exception( "Unknown method in Trim Class - " . $method );
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,46 @@
<?php
/**
* CSSTidy - CSS Parser and Optimiser
*
* CSS ctype functions
* Defines some functions that can be not defined.
*
* This file is part of CSSTidy.
*
* CSSTidy 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 2 of the License, or
* (at your option) any later version.
*
* CSSTidy 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 CSSTidy; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package csstidy
* @author Nikolay Matsievsky (speed at webo dot name) 2009-2010
* @version 1.0
*/
/* ctype_space Check for whitespace character(s) */
if (!function_exists('ctype_space')) {
function ctype_space($text) {
return;
preg_match("/[^\s\r\n\t\f]/", $text);
}
}
/* ctype_alpha Check for alphabetic character(s) */
if (!function_exists('ctype_alpha')) {
function ctype_alpha($text) {
return preg_match("/[a-zA-Z]/", $text);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,416 @@
<?php
/**
* CSSTidy - CSS Parser and Optimiser
*
* CSS Printing class
* This class prints CSS data generated by csstidy.
*
* Copyright 2005, 2006, 2007 Florian Schmitz
*
* This file is part of CSSTidy.
*
* CSSTidy is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CSSTidy 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @license http://opensource.org/licenses/lgpl-license.php GNU Lesser General Public License
* @package csstidy
* @author Florian Schmitz (floele at gmail dot com) 2005-2007
* @author Brett Zamir (brettz9 at yahoo dot com) 2007
* @author Cedric Morin (cedric at yterium dot com) 2010
*/
/**
* CSS Printing class
*
* This class prints CSS data generated by csstidy.
*
* @package csstidy
* @author Florian Schmitz (floele at gmail dot com) 2005-2006
* @version 1.0.1
*/
class csstidy_print {
/**
* Saves the input CSS string
* @var string
* @access private
*/
var $input_css = '';
/**
* Saves the formatted CSS string
* @var string
* @access public
*/
var $output_css = '';
/**
* Saves the formatted CSS string (plain text)
* @var string
* @access public
*/
var $output_css_plain = '';
/**
* Constructor
* @param array $css contains the class csstidy
* @access private
* @version 1.0
*/
function csstidy_print(&$css) {
$this->parser = & $css;
$this->css = & $css->css;
$this->template = & $css->template;
$this->tokens = & $css->tokens;
$this->charset = & $css->charset;
$this->import = & $css->import;
$this->namespace = & $css->namespace;
}
/**
* Resets output_css and output_css_plain (new css code)
* @access private
* @version 1.0
*/
function _reset() {
$this->output_css = '';
$this->output_css_plain = '';
}
/**
* Returns the CSS code as plain text
* @param string $default_media default @media to add to selectors without any @media
* @return string
* @access public
* @version 1.0
*/
function plain($default_media = '') {
$this->_print(true, $default_media);
return $this->output_css_plain;
}
/**
* Returns the formatted CSS code
* @param string $default_media default @media to add to selectors without any @media
* @return string
* @access public
* @version 1.0
*/
function formatted($default_media = '') {
$this->_print(false, $default_media);
return $this->output_css;
}
/**
* Returns the formatted CSS code to make a complete webpage
* @param string $doctype shorthand for the document type
* @param bool $externalcss indicates whether styles to be attached internally or as an external stylesheet
* @param string $title title to be added in the head of the document
* @param string $lang two-letter language code to be added to the output
* @return string
* @access public
* @version 1.4
*/
function formatted_page($doctype = 'xhtml1.1', $externalcss = true, $title = '', $lang = 'en') {
switch ($doctype) {
case 'xhtml1.0strict':
$doctype_output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
break;
case 'xhtml1.1':
default:
$doctype_output = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">';
break;
}
$output = $cssparsed = '';
$this->output_css_plain = & $output;
$output .= $doctype_output . "\n" . '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="' . $lang . '"';
$output .= ( $doctype === 'xhtml1.1') ? '>' : ' lang="' . $lang . '">';
$output .= "\n<head>\n <title>$title</title>";
if ($externalcss) {
$output .= "\n <style type=\"text/css\">\n";
$cssparsed = file_get_contents('cssparsed.css');
$output .= $cssparsed; // Adds an invisible BOM or something, but not in css_optimised.php
$output .= "\n</style>";
}
else {
$output .= "\n" . ' <link rel="stylesheet" type="text/css" href="cssparsed.css" />';
// }
}
$output .= "\n</head>\n<body><code id=\"copytext\">";
$output .= $this->formatted();
$output .= '</code>' . "\n" . '</body></html>';
return $this->output_css_plain;
}
/**
* Returns the formatted CSS Code and saves it into $this->output_css and $this->output_css_plain
* @param bool $plain plain text or not
* @param string $default_media default @media to add to selectors without any @media
* @access private
* @version 2.0
*/
function _print($plain = false, $default_media = '') {
if ($this->output_css && $this->output_css_plain) {
return;
}
$output = '';
if (!$this->parser->get_cfg('preserve_css')) {
$this->_convert_raw_css($default_media);
}
$template = & $this->template;
if ($plain) {
$template = array_map('strip_tags', $template);
}
if ($this->parser->get_cfg('timestamp')) {
array_unshift($this->tokens, array(COMMENT, ' CSSTidy ' . $this->parser->version . ': ' . date('r') . ' '));
}
if (!empty($this->charset)) {
$output .= $template[0] . '@charset ' . $template[5] . $this->charset . $template[6];
}
if (!empty($this->import)) {
for ($i = 0, $size = count($this->import); $i < $size; $i++) {
if (substr($this->import[$i], 0, 4) === 'url(' && substr($this->import[$i], -1, 1) === ')') {
$this->import[$i] = '\'' . substr($this->import[$i], 4, -1) . '\'';
$this->parser->log('Optimised @import : Removed "url("', 'Information');
}
$output .= $template[0] . '@import ' . $template[5] . $this->import[$i] . $template[6];
}
}
if (!empty($this->namespace)) {
if (substr($this->namespace, 0, 4) === 'url(' && substr($this->namespace, -1, 1) === ')') {
$this->namespace = '\'' . substr($this->namespace, 4, -1) . '\'';
$this->parser->log('Optimised @namespace : Removed "url("', 'Information');
}
$output .= $template[0] . '@namespace ' . $template[5] . $this->namespace . $template[6];
}
$output .= $template[13];
$in_at_out = '';
$out = & $output;
foreach ($this->tokens as $key => $token) {
switch ($token[0]) {
case AT_START:
$out .= $template[0] . $this->_htmlsp($token[1], $plain) . $template[1];
$out = & $in_at_out;
break;
case SEL_START:
if ($this->parser->get_cfg('lowercase_s')) {
$token[1] = strtolower($token[1]);
}
$out .= ( $token[1]{0} !== '@') ? $template[2] . $this->_htmlsp($token[1], $plain) : $template[0] . $this->_htmlsp($token[1], $plain);
$out .= $template[3];
break;
case PROPERTY:
if ($this->parser->get_cfg('case_properties') === 2) {
$token[1] = strtoupper($token[1]);
}
elseif ($this->parser->get_cfg('case_properties') === 1) {
$token[1] = strtolower($token[1]);
}
$out .= $template[4] . $this->_htmlsp($token[1], $plain) . ':' . $template[5];
break;
case VALUE:
$out .= $this->_htmlsp($token[1], $plain);
if ($this->_seeknocomment($key, 1) == SEL_END && $this->parser->get_cfg('remove_last_;')) {
$out .= str_replace(';', '', $template[6]);
}
else {
$out .= $template[6];
}
break;
case SEL_END:
$out .= $template[7];
if ($this->_seeknocomment($key, 1) != AT_END) {
$out .= $template[8];
}
break;
case AT_END:
$out = & $output;
$out .= $template[10] . str_replace("\n", "\n" . $template[10], $in_at_out);
$in_at_out = '';
$out .= $template[9];
break;
case COMMENT:
$out .= $template[11] . '/*' . $this->_htmlsp($token[1], $plain) . '*/' . $template[12];
break;
}
}
$output = trim($output);
if (!$plain) {
$this->output_css = $output;
$this->_print(true);
}
else {
// If using spaces in the template, don't want these to appear in the plain output
$this->output_css_plain = str_replace('&#160;', '', $output);
}
}
/**
* Gets the next token type which is $move away from $key, excluding comments
* @param integer $key current position
* @param integer $move move this far
* @return mixed a token type
* @access private
* @version 1.0
*/
function _seeknocomment($key, $move) {
$go = ($move > 0) ? 1 : -1;
for ($i = $key + 1; abs($key - $i) - 1 < abs($move); $i += $go) {
if (!isset($this->tokens[$i])) {
return;
}
if ($this->tokens[$i][0] == COMMENT) {
$move += 1;
continue;
}
return $this->tokens[$i][0];
}
}
/**
* Converts $this->css array to a raw array ($this->tokens)
* @param string $default_media default @media to add to selectors without any @media
* @access private
* @version 1.0
*/
function _convert_raw_css($default_media = '') {
$this->tokens = array();
foreach ($this->css as $medium => $val) {
if ($this->parser->get_cfg('sort_selectors')) {
ksort($val);
}
if (intval($medium) < DEFAULT_AT) {
$this->parser->_add_token(AT_START, $medium, true);
}
elseif ($default_media) {
$this->parser->_add_token(AT_START, $default_media, true);
}
foreach ($val as $selector => $vali) {
if ($this->parser->get_cfg('sort_properties')) {
ksort($vali);
}
$this->parser->_add_token(SEL_START, $selector, true);
foreach ($vali as $property => $valj) {
$this->parser->_add_token(PROPERTY, $property, true);
$this->parser->_add_token(VALUE, $valj, true);
}
$this->parser->_add_token(SEL_END, $selector, true);
}
if (intval($medium) < DEFAULT_AT) {
$this->parser->_add_token(AT_END, $medium, true);
}
elseif ($default_media) {
$this->parser->_add_token(AT_END, $default_media, true);
}
}
}
/**
* Same as htmlspecialchars, only that chars are not replaced if $plain !== true. This makes print_code() cleaner.
* @param string $string
* @param bool $plain
* @return string
* @see csstidy_print::_print()
* @access private
* @version 1.0
*/
function _htmlsp($string, $plain) {
if (!$plain) {
return htmlspecialchars($string, ENT_QUOTES, 'utf-8');
}
return $string;
}
/**
* Get compression ratio
* @access public
* @return float
* @version 1.2
*/
function get_ratio() {
if (!$this->output_css_plain) {
$this->formatted();
}
return round((strlen($this->input_css) - strlen($this->output_css_plain)) / strlen($this->input_css), 3) * 100;
}
/**
* Get difference between the old and new code in bytes and prints the code if necessary.
* @access public
* @return string
* @version 1.1
*/
function get_diff() {
if (!$this->output_css_plain) {
$this->formatted();
}
$diff = strlen($this->output_css_plain) - strlen($this->input_css);
if ($diff > 0) {
return '+' . $diff;
}
elseif ($diff == 0) {
return '+-' . $diff;
}
return $diff;
}
/**
* Get the size of either input or output CSS in KB
* @param string $loc default is "output"
* @access public
* @return integer
* @version 1.0
*/
function size($loc = 'output') {
if ($loc === 'output' && !$this->output_css) {
$this->formatted();
}
if ($loc === 'input') {
return (strlen($this->input_css) / 1000);
}
else {
return (strlen($this->output_css_plain) / 1000);
}
}
}

View file

@ -0,0 +1,528 @@
<?php
/**
* Various CSS Data for CSSTidy
*
* This file is part of CSSTidy.
*
* CSSTidy 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 2 of the License, or
* (at your option) any later version.
*
* CSSTidy 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 CSSTidy; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package csstidy
* @author Florian Schmitz (floele at gmail dot com) 2005
* @author Nikolay Matsievsky (speed at webo dot name) 2010
*/
define('AT_START', 1);
define('AT_END', 2);
define('SEL_START', 3);
define('SEL_END', 4);
define('PROPERTY', 5);
define('VALUE', 6);
define('COMMENT', 7);
define('DEFAULT_AT', 41);
/**
* All whitespace allowed in CSS
*
* @global array $GLOBALS['csstidy']['whitespace']
* @version 1.0
*/
$GLOBALS['csstidy']['whitespace'] = array(' ', "\n", "\t", "\r", "\x0B");
/**
* All CSS tokens used by csstidy
*
* @global string $GLOBALS['csstidy']['tokens']
* @version 1.0
*/
$GLOBALS['csstidy']['tokens'] = '/@}{;:=\'"(,\\!$%&)*+.<>?[]^`|~';
/**
* All CSS units (CSS 3 units included)
*
* @see compress_numbers()
* @global array $GLOBALS['csstidy']['units']
* @version 1.0
*/
$GLOBALS['csstidy']['units'] = array('in', 'cm', 'mm', 'pt', 'pc', 'px', 'rem', 'em', '%', 'ex', 'gd', 'vw', 'vh', 'vm', 'deg', 'grad', 'rad', 'ms', 's', 'khz', 'hz');
/**
* Available at-rules
*
* @global array $GLOBALS['csstidy']['at_rules']
* @version 1.0
*/
$GLOBALS['csstidy']['at_rules'] = array(
'page' => 'is',
'font-face' => 'is',
'charset' => 'iv',
'import' => 'iv',
'namespace' => 'iv',
'media' => 'at',
);
/**
* Properties that need a value with unit
*
* @todo CSS3 properties
* @see compress_numbers();
* @global array $GLOBALS['csstidy']['unit_values']
* @version 1.2
*/
$GLOBALS['csstidy']['unit_values'] = array(
'background',
'background-position',
'border',
'border-top',
'border-right',
'border-bottom',
'border-left',
'border-width',
'border-top-width',
'border-right-width',
'border-left-width',
'border-bottom-width',
'bottom',
'border-spacing',
'font-size',
'height',
'left',
'margin',
'margin-top',
'margin-right',
'margin-bottom',
'margin-left',
'max-height',
'max-width',
'min-height',
'min-width',
'outline',
'outline-width',
'padding',
'padding-top',
'padding-right',
'padding-bottom',
'padding-left',
'right',
'top',
'text-indent',
'letter-spacing',
'word-spacing',
'width',
);
/**
* Properties that allow <color> as value
*
* @todo CSS3 properties
* @see compress_numbers();
* @global array $GLOBALS['csstidy']['color_values']
* @version 1.0
*/
$GLOBALS['csstidy']['color_values'] = array();
$GLOBALS['csstidy']['color_values'][] = 'background-color';
$GLOBALS['csstidy']['color_values'][] = 'border-color';
$GLOBALS['csstidy']['color_values'][] = 'border-top-color';
$GLOBALS['csstidy']['color_values'][] = 'border-right-color';
$GLOBALS['csstidy']['color_values'][] = 'border-bottom-color';
$GLOBALS['csstidy']['color_values'][] = 'border-left-color';
$GLOBALS['csstidy']['color_values'][] = 'color';
$GLOBALS['csstidy']['color_values'][] = 'outline-color';
/**
* Default values for the background properties
*
* @todo Possibly property names will change during CSS3 development
* @global array $GLOBALS['csstidy']['background_prop_default']
* @see dissolve_short_bg()
* @see merge_bg()
* @version 1.0
*/
$GLOBALS['csstidy']['background_prop_default'] = array();
$GLOBALS['csstidy']['background_prop_default']['background-image'] = 'none';
$GLOBALS['csstidy']['background_prop_default']['background-size'] = 'auto';
$GLOBALS['csstidy']['background_prop_default']['background-repeat'] = 'repeat';
$GLOBALS['csstidy']['background_prop_default']['background-position'] = '0 0';
$GLOBALS['csstidy']['background_prop_default']['background-attachment'] = 'scroll';
$GLOBALS['csstidy']['background_prop_default']['background-clip'] = 'border';
$GLOBALS['csstidy']['background_prop_default']['background-origin'] = 'padding';
$GLOBALS['csstidy']['background_prop_default']['background-color'] = 'transparent';
/**
* Default values for the font properties
*
* @global array $GLOBALS['csstidy']['font_prop_default']
* @see merge_fonts()
* @version 1.3
*/
$GLOBALS['csstidy']['font_prop_default'] = array();
$GLOBALS['csstidy']['font_prop_default']['font-style'] = 'normal';
$GLOBALS['csstidy']['font_prop_default']['font-variant'] = 'normal';
$GLOBALS['csstidy']['font_prop_default']['font-weight'] = 'normal';
$GLOBALS['csstidy']['font_prop_default']['font-size'] = '';
$GLOBALS['csstidy']['font_prop_default']['line-height'] = '';
$GLOBALS['csstidy']['font_prop_default']['font-family'] = '';
/**
* A list of non-W3C color names which get replaced by their hex-codes
*
* @global array $GLOBALS['csstidy']['replace_colors']
* @see cut_color()
* @version 1.0
*/
$GLOBALS['csstidy']['replace_colors'] = array();
$GLOBALS['csstidy']['replace_colors']['aliceblue'] = '#f0f8ff';
$GLOBALS['csstidy']['replace_colors']['antiquewhite'] = '#faebd7';
$GLOBALS['csstidy']['replace_colors']['aquamarine'] = '#7fffd4';
$GLOBALS['csstidy']['replace_colors']['azure'] = '#f0ffff';
$GLOBALS['csstidy']['replace_colors']['beige'] = '#f5f5dc';
$GLOBALS['csstidy']['replace_colors']['bisque'] = '#ffe4c4';
$GLOBALS['csstidy']['replace_colors']['blanchedalmond'] = '#ffebcd';
$GLOBALS['csstidy']['replace_colors']['blueviolet'] = '#8a2be2';
$GLOBALS['csstidy']['replace_colors']['brown'] = '#a52a2a';
$GLOBALS['csstidy']['replace_colors']['burlywood'] = '#deb887';
$GLOBALS['csstidy']['replace_colors']['cadetblue'] = '#5f9ea0';
$GLOBALS['csstidy']['replace_colors']['chartreuse'] = '#7fff00';
$GLOBALS['csstidy']['replace_colors']['chocolate'] = '#d2691e';
$GLOBALS['csstidy']['replace_colors']['coral'] = '#ff7f50';
$GLOBALS['csstidy']['replace_colors']['cornflowerblue'] = '#6495ed';
$GLOBALS['csstidy']['replace_colors']['cornsilk'] = '#fff8dc';
$GLOBALS['csstidy']['replace_colors']['crimson'] = '#dc143c';
$GLOBALS['csstidy']['replace_colors']['cyan'] = '#00ffff';
$GLOBALS['csstidy']['replace_colors']['darkblue'] = '#00008b';
$GLOBALS['csstidy']['replace_colors']['darkcyan'] = '#008b8b';
$GLOBALS['csstidy']['replace_colors']['darkgoldenrod'] = '#b8860b';
$GLOBALS['csstidy']['replace_colors']['darkgray'] = '#a9a9a9';
$GLOBALS['csstidy']['replace_colors']['darkgreen'] = '#006400';
$GLOBALS['csstidy']['replace_colors']['darkkhaki'] = '#bdb76b';
$GLOBALS['csstidy']['replace_colors']['darkmagenta'] = '#8b008b';
$GLOBALS['csstidy']['replace_colors']['darkolivegreen'] = '#556b2f';
$GLOBALS['csstidy']['replace_colors']['darkorange'] = '#ff8c00';
$GLOBALS['csstidy']['replace_colors']['darkorchid'] = '#9932cc';
$GLOBALS['csstidy']['replace_colors']['darkred'] = '#8b0000';
$GLOBALS['csstidy']['replace_colors']['darksalmon'] = '#e9967a';
$GLOBALS['csstidy']['replace_colors']['darkseagreen'] = '#8fbc8f';
$GLOBALS['csstidy']['replace_colors']['darkslateblue'] = '#483d8b';
$GLOBALS['csstidy']['replace_colors']['darkslategray'] = '#2f4f4f';
$GLOBALS['csstidy']['replace_colors']['darkturquoise'] = '#00ced1';
$GLOBALS['csstidy']['replace_colors']['darkviolet'] = '#9400d3';
$GLOBALS['csstidy']['replace_colors']['deeppink'] = '#ff1493';
$GLOBALS['csstidy']['replace_colors']['deepskyblue'] = '#00bfff';
$GLOBALS['csstidy']['replace_colors']['dimgray'] = '#696969';
$GLOBALS['csstidy']['replace_colors']['dodgerblue'] = '#1e90ff';
$GLOBALS['csstidy']['replace_colors']['feldspar'] = '#d19275';
$GLOBALS['csstidy']['replace_colors']['firebrick'] = '#b22222';
$GLOBALS['csstidy']['replace_colors']['floralwhite'] = '#fffaf0';
$GLOBALS['csstidy']['replace_colors']['forestgreen'] = '#228b22';
$GLOBALS['csstidy']['replace_colors']['gainsboro'] = '#dcdcdc';
$GLOBALS['csstidy']['replace_colors']['ghostwhite'] = '#f8f8ff';
$GLOBALS['csstidy']['replace_colors']['gold'] = '#ffd700';
$GLOBALS['csstidy']['replace_colors']['goldenrod'] = '#daa520';
$GLOBALS['csstidy']['replace_colors']['greenyellow'] = '#adff2f';
$GLOBALS['csstidy']['replace_colors']['honeydew'] = '#f0fff0';
$GLOBALS['csstidy']['replace_colors']['hotpink'] = '#ff69b4';
$GLOBALS['csstidy']['replace_colors']['indianred'] = '#cd5c5c';
$GLOBALS['csstidy']['replace_colors']['indigo'] = '#4b0082';
$GLOBALS['csstidy']['replace_colors']['ivory'] = '#fffff0';
$GLOBALS['csstidy']['replace_colors']['khaki'] = '#f0e68c';
$GLOBALS['csstidy']['replace_colors']['lavender'] = '#e6e6fa';
$GLOBALS['csstidy']['replace_colors']['lavenderblush'] = '#fff0f5';
$GLOBALS['csstidy']['replace_colors']['lawngreen'] = '#7cfc00';
$GLOBALS['csstidy']['replace_colors']['lemonchiffon'] = '#fffacd';
$GLOBALS['csstidy']['replace_colors']['lightblue'] = '#add8e6';
$GLOBALS['csstidy']['replace_colors']['lightcoral'] = '#f08080';
$GLOBALS['csstidy']['replace_colors']['lightcyan'] = '#e0ffff';
$GLOBALS['csstidy']['replace_colors']['lightgoldenrodyellow'] = '#fafad2';
$GLOBALS['csstidy']['replace_colors']['lightgrey'] = '#d3d3d3';
$GLOBALS['csstidy']['replace_colors']['lightgreen'] = '#90ee90';
$GLOBALS['csstidy']['replace_colors']['lightpink'] = '#ffb6c1';
$GLOBALS['csstidy']['replace_colors']['lightsalmon'] = '#ffa07a';
$GLOBALS['csstidy']['replace_colors']['lightseagreen'] = '#20b2aa';
$GLOBALS['csstidy']['replace_colors']['lightskyblue'] = '#87cefa';
$GLOBALS['csstidy']['replace_colors']['lightslateblue'] = '#8470ff';
$GLOBALS['csstidy']['replace_colors']['lightslategray'] = '#778899';
$GLOBALS['csstidy']['replace_colors']['lightsteelblue'] = '#b0c4de';
$GLOBALS['csstidy']['replace_colors']['lightyellow'] = '#ffffe0';
$GLOBALS['csstidy']['replace_colors']['limegreen'] = '#32cd32';
$GLOBALS['csstidy']['replace_colors']['linen'] = '#faf0e6';
$GLOBALS['csstidy']['replace_colors']['magenta'] = '#ff00ff';
$GLOBALS['csstidy']['replace_colors']['mediumaquamarine'] = '#66cdaa';
$GLOBALS['csstidy']['replace_colors']['mediumblue'] = '#0000cd';
$GLOBALS['csstidy']['replace_colors']['mediumorchid'] = '#ba55d3';
$GLOBALS['csstidy']['replace_colors']['mediumpurple'] = '#9370d8';
$GLOBALS['csstidy']['replace_colors']['mediumseagreen'] = '#3cb371';
$GLOBALS['csstidy']['replace_colors']['mediumslateblue'] = '#7b68ee';
$GLOBALS['csstidy']['replace_colors']['mediumspringgreen'] = '#00fa9a';
$GLOBALS['csstidy']['replace_colors']['mediumturquoise'] = '#48d1cc';
$GLOBALS['csstidy']['replace_colors']['mediumvioletred'] = '#c71585';
$GLOBALS['csstidy']['replace_colors']['midnightblue'] = '#191970';
$GLOBALS['csstidy']['replace_colors']['mintcream'] = '#f5fffa';
$GLOBALS['csstidy']['replace_colors']['mistyrose'] = '#ffe4e1';
$GLOBALS['csstidy']['replace_colors']['moccasin'] = '#ffe4b5';
$GLOBALS['csstidy']['replace_colors']['navajowhite'] = '#ffdead';
$GLOBALS['csstidy']['replace_colors']['oldlace'] = '#fdf5e6';
$GLOBALS['csstidy']['replace_colors']['olivedrab'] = '#6b8e23';
$GLOBALS['csstidy']['replace_colors']['orangered'] = '#ff4500';
$GLOBALS['csstidy']['replace_colors']['orchid'] = '#da70d6';
$GLOBALS['csstidy']['replace_colors']['palegoldenrod'] = '#eee8aa';
$GLOBALS['csstidy']['replace_colors']['palegreen'] = '#98fb98';
$GLOBALS['csstidy']['replace_colors']['paleturquoise'] = '#afeeee';
$GLOBALS['csstidy']['replace_colors']['palevioletred'] = '#d87093';
$GLOBALS['csstidy']['replace_colors']['papayawhip'] = '#ffefd5';
$GLOBALS['csstidy']['replace_colors']['peachpuff'] = '#ffdab9';
$GLOBALS['csstidy']['replace_colors']['peru'] = '#cd853f';
$GLOBALS['csstidy']['replace_colors']['pink'] = '#ffc0cb';
$GLOBALS['csstidy']['replace_colors']['plum'] = '#dda0dd';
$GLOBALS['csstidy']['replace_colors']['powderblue'] = '#b0e0e6';
$GLOBALS['csstidy']['replace_colors']['rosybrown'] = '#bc8f8f';
$GLOBALS['csstidy']['replace_colors']['royalblue'] = '#4169e1';
$GLOBALS['csstidy']['replace_colors']['saddlebrown'] = '#8b4513';
$GLOBALS['csstidy']['replace_colors']['salmon'] = '#fa8072';
$GLOBALS['csstidy']['replace_colors']['sandybrown'] = '#f4a460';
$GLOBALS['csstidy']['replace_colors']['seagreen'] = '#2e8b57';
$GLOBALS['csstidy']['replace_colors']['seashell'] = '#fff5ee';
$GLOBALS['csstidy']['replace_colors']['sienna'] = '#a0522d';
$GLOBALS['csstidy']['replace_colors']['skyblue'] = '#87ceeb';
$GLOBALS['csstidy']['replace_colors']['slateblue'] = '#6a5acd';
$GLOBALS['csstidy']['replace_colors']['slategray'] = '#708090';
$GLOBALS['csstidy']['replace_colors']['snow'] = '#fffafa';
$GLOBALS['csstidy']['replace_colors']['springgreen'] = '#00ff7f';
$GLOBALS['csstidy']['replace_colors']['steelblue'] = '#4682b4';
$GLOBALS['csstidy']['replace_colors']['tan'] = '#d2b48c';
$GLOBALS['csstidy']['replace_colors']['thistle'] = '#d8bfd8';
$GLOBALS['csstidy']['replace_colors']['tomato'] = '#ff6347';
$GLOBALS['csstidy']['replace_colors']['turquoise'] = '#40e0d0';
$GLOBALS['csstidy']['replace_colors']['violet'] = '#ee82ee';
$GLOBALS['csstidy']['replace_colors']['violetred'] = '#d02090';
$GLOBALS['csstidy']['replace_colors']['wheat'] = '#f5deb3';
$GLOBALS['csstidy']['replace_colors']['whitesmoke'] = '#f5f5f5';
$GLOBALS['csstidy']['replace_colors']['yellowgreen'] = '#9acd32';
/**
* A list of all shorthand properties that are devided into four properties and/or have four subvalues
*
* @global array $GLOBALS['csstidy']['shorthands']
* @todo Are there new ones in CSS3?
* @see dissolve_4value_shorthands()
* @see merge_4value_shorthands()
* @version 1.0
*/
$GLOBALS['csstidy']['shorthands'] = array();
$GLOBALS['csstidy']['shorthands']['border-color'] = array('border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color');
$GLOBALS['csstidy']['shorthands']['border-style'] = array('border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style');
$GLOBALS['csstidy']['shorthands']['border-width'] = array('border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width');
$GLOBALS['csstidy']['shorthands']['margin'] = array('margin-top', 'margin-right', 'margin-bottom', 'margin-left');
$GLOBALS['csstidy']['shorthands']['padding'] = array('padding-top', 'padding-right', 'padding-bottom', 'padding-left');
$GLOBALS['csstidy']['shorthands']['-moz-border-radius'] = 0;
/**
* All CSS Properties. Needed for csstidy::property_is_next()
*
* @global array $GLOBALS['csstidy']['all_properties']
* @todo Add CSS3 properties
* @version 1.0
* @see csstidy::property_is_next()
*/
$GLOBALS['csstidy']['all_properties'] = array();
$GLOBALS['csstidy']['all_properties']['background'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['background-color'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['background-image'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['background-repeat'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['background-attachment'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['background-position'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-top'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-right'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-bottom'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-left'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-color'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-top-color'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-bottom-color'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-left-color'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-right-color'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-style'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-top-style'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-right-style'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-left-style'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-bottom-style'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-top-width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-right-width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-left-width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-bottom-width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-collapse'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['border-spacing'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['bottom'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['caption-side'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['content'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['clear'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['clip'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['color'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['counter-reset'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['counter-increment'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['cursor'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['empty-cells'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['display'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['direction'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['float'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font-family'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font-style'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['font-stretch'] = 'CSS2.0';
$GLOBALS['csstidy']['all_properties']['font-size-adjust'] = 'CSS2.0';
$GLOBALS['csstidy']['all_properties']['font-size'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['height'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['left'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['line-height'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['list-style'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['list-style-type'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['list-style-image'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['list-style-position'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['margin'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['margin-top'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['margin-right'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['margin-bottom'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['margin-left'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['marks'] = 'CSS1.0,CSS2.0';
$GLOBALS['csstidy']['all_properties']['marker-offset'] = 'CSS2.0';
$GLOBALS['csstidy']['all_properties']['max-height'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['max-width'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['min-height'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['min-width'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['overflow'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['orphans'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['outline'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['outline-width'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['outline-style'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['outline-color'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['padding'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['padding-top'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['padding-right'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['padding-bottom'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['padding-left'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['page-break-before'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['page-break-after'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['page-break-inside'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['page'] = 'CSS2.0';
$GLOBALS['csstidy']['all_properties']['position'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['quotes'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['right'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['size'] = 'CSS1.0,CSS2.0';
$GLOBALS['csstidy']['all_properties']['speak-header'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['table-layout'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['top'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['text-indent'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['text-align'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['text-decoration'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['text-shadow'] = 'CSS2.0';
$GLOBALS['csstidy']['all_properties']['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['word-spacing'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['text-transform'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['white-space'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['unicode-bidi'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['vertical-align'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['visibility'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['width'] = 'CSS1.0,CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['widows'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['z-index'] = 'CSS1.0,CSS2.0,CSS2.1';
/* Speech */
$GLOBALS['csstidy']['all_properties']['volume'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['speak'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['pause'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['pause-before'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['pause-after'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['cue'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['cue-before'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['cue-after'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['play-during'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['azimuth'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['elevation'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['speech-rate'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['voice-family'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['pitch'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['pitch-range'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['stress'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['richness'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['speak-punctuation'] = 'CSS2.0,CSS2.1';
$GLOBALS['csstidy']['all_properties']['speak-numeral'] = 'CSS2.0,CSS2.1';
/**
* An array containing all predefined templates.
*
* @global array $GLOBALS['csstidy']['predefined_templates']
* @version 1.0
* @see csstidy::load_template()
*/
$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="at">'; //string before @rule
$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span> <span class="format">{</span>' . "\n"; //bracket after @-rule
$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="selector">'; //string before selector
$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span> <span class="format">{</span>' . "\n"; //bracket after selector
$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="property">'; //string before property
$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span><span class="value">'; //string after property+before value
$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span><span class="format">;</span>' . "\n"; //string after value
$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="format">}</span>'; //closing bracket - selector
$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n\n"; //space between blocks {...}
$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n" . '<span class="format">}</span>' . "\n\n"; //closing bracket @-rule
$GLOBALS['csstidy']['predefined_templates']['default'][] = ''; //indent in @-rule
$GLOBALS['csstidy']['predefined_templates']['default'][] = '<span class="comment">'; // before comment
$GLOBALS['csstidy']['predefined_templates']['default'][] = '</span>' . "\n"; // after comment
$GLOBALS['csstidy']['predefined_templates']['default'][] = "\n"; // after last line @-rule
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="at">';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span> <span class="format">{</span>' . "\n";
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="selector">';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="format">{</span>';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="property">';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="value">';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span><span class="format">;</span>';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="format">}</span>';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n";
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n" . '<span class="format">}' . "\n" . '</span>';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '';
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '<span class="comment">'; // before comment
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = '</span>'; // after comment
$GLOBALS['csstidy']['predefined_templates']['high_compression'][] = "\n";
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="at">';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">{</span>';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="selector">';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">{</span>';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="property">';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="value">';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span><span class="format">;</span>';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="format">}</span>';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="format">}</span>';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '<span class="comment">'; // before comment
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '</span>'; // after comment
$GLOBALS['csstidy']['predefined_templates']['highest_compression'][] = '';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="at">';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span> <span class="format">{</span>' . "\n";
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="selector">';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span>' . "\n" . '<span class="format">{</span>' . "\n";
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = ' <span class="property">';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span><span class="value">';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span><span class="format">;</span>' . "\n";
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="format">}</span>';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n\n";
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n" . '<span class="format">}</span>' . "\n\n";
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = ' ';
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '<span class="comment">'; // before comment
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = '</span>' . "\n"; // after comment
$GLOBALS['csstidy']['predefined_templates']['low_compression'][] = "\n";

View file

@ -0,0 +1,728 @@
<?php
/*!
* cssmin.php rev 91c5ea5
* Author: Tubal Martin - http://blog.margenn.com/
* Repo: https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port
*
* This is a PHP port of the Javascript port of the CSS minification tool
* distributed with YUICompressor, itself a port of the cssmin utility by
* Isaac Schlueter - http://foohack.com/
* Permission is hereby granted to use the PHP version under the same
* conditions as the YUICompressor.
*/
/*!
* YUI Compressor
* http://developer.yahoo.com/yui/compressor/
* Author: Julien Lecomte - http://www.julienlecomte.net/
* Copyright (c) 2011 Yahoo! Inc. All rights reserved.
* The copyrights embodied in the content of this file are licensed
* by Yahoo! Inc. under the BSD (revised) open source license.
*/
class CSSmin
{
const NL = '___YUICSSMIN_PRESERVED_NL___';
const TOKEN = '___YUICSSMIN_PRESERVED_TOKEN_';
const COMMENT = '___YUICSSMIN_PRESERVE_CANDIDATE_COMMENT_';
const CLASSCOLON = '___YUICSSMIN_PSEUDOCLASSCOLON___';
private $comments;
private $preserved_tokens;
private $memory_limit;
private $max_execution_time;
private $pcre_backtrack_limit;
private $pcre_recursion_limit;
private $raise_php_limits;
/**
* @param bool|int $raise_php_limits
* If true, PHP settings will be raised if needed
*/
public function __construct($raise_php_limits = TRUE)
{
// Set suggested PHP limits
$this->memory_limit = 128 * 1048576; // 128MB in bytes
$this->max_execution_time = 60; // 1 min
$this->pcre_backtrack_limit = 1000 * 1000;
$this->pcre_recursion_limit = 500 * 1000;
$this->raise_php_limits = (bool) $raise_php_limits;
}
/**
* Minify a string of CSS
* @param string $css
* @param int|bool $linebreak_pos
* @return string
*/
public function run($css = '', $linebreak_pos = FALSE)
{
if (empty($css)) {
return '';
}
if ($this->raise_php_limits) {
$this->do_raise_php_limits();
}
$this->comments = array();
$this->preserved_tokens = array();
$start_index = 0;
$length = strlen($css);
$css = $this->extract_data_urls($css);
// collect all comment blocks...
while (($start_index = $this->index_of($css, '/*', $start_index)) >= 0) {
$end_index = $this->index_of($css, '*/', $start_index + 2);
if ($end_index < 0) {
$end_index = $length;
}
$comment_found = $this->str_slice($css, $start_index + 2, $end_index);
$this->comments[] = $comment_found;
$comment_preserve_string = self::COMMENT . (count($this->comments) - 1) . '___';
$css = $this->str_slice($css, 0, $start_index + 2) . $comment_preserve_string . $this->str_slice($css, $end_index);
// Set correct start_index: Fixes issue #2528130
$start_index = $end_index + 2 + strlen($comment_preserve_string) - strlen($comment_found);
}
// preserve strings so their content doesn't get accidentally minified
$css = preg_replace_callback('/(?:"(?:[^\\\\"]|\\\\.|\\\\)*")|'."(?:'(?:[^\\\\']|\\\\.|\\\\)*')/S", array($this, 'replace_string'), $css);
// Let's divide css code in chunks of 25.000 chars aprox.
// Reason: PHP's PCRE functions like preg_replace have a "backtrack limit"
// of 100.000 chars by default (php < 5.3.7) so if we're dealing with really
// long strings and a (sub)pattern matches a number of chars greater than
// the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently
// returning NULL and $css would be empty.
$charset = '';
$charset_regexp = '/@charset [^;]+;/i';
$css_chunks = array();
$css_chunk_length = 25000; // aprox size, not exact
$start_index = 0;
$i = $css_chunk_length; // save initial iterations
$l = strlen($css);
// if the number of characters is 25000 or less, do not chunk
if ($l <= $css_chunk_length) {
$css_chunks[] = $css;
} else {
// chunk css code securely
while ($i < $l) {
$i += 50; // save iterations. 500 checks for a closing curly brace }
if ($l - $start_index <= $css_chunk_length || $i >= $l) {
$css_chunks[] = $this->str_slice($css, $start_index);
break;
}
if ($css[$i - 1] === '}' && $i - $start_index > $css_chunk_length) {
// If there are two ending curly braces }} separated or not by spaces,
// join them in the same chunk (i.e. @media blocks)
$next_chunk = substr($css, $i);
if (preg_match('/^\s*\}/', $next_chunk)) {
$i = $i + $this->index_of($next_chunk, '}') + 1;
}
$css_chunks[] = $this->str_slice($css, $start_index, $i);
$start_index = $i;
}
}
}
// Minify each chunk
for ($i = 0, $n = count($css_chunks); $i < $n; $i++) {
$css_chunks[$i] = $this->minify($css_chunks[$i], $linebreak_pos);
// Keep the first @charset at-rule found
if (empty($charset) && preg_match($charset_regexp, $css_chunks[$i], $matches)) {
$charset = $matches[0];
}
// Delete all @charset at-rules
$css_chunks[$i] = preg_replace($charset_regexp, '', $css_chunks[$i]);
}
// Update the first chunk and push the charset to the top of the file.
$css_chunks[0] = $charset . $css_chunks[0];
return implode('', $css_chunks);
}
/**
* Sets the memory limit for this script
* @param int|string $limit
*/
public function set_memory_limit($limit)
{
$this->memory_limit = $this->normalize_int($limit);
}
/**
* Sets the maximum execution time for this script
* @param int|string $seconds
*/
public function set_max_execution_time($seconds)
{
$this->max_execution_time = (int) $seconds;
}
/**
* Sets the PCRE backtrack limit for this script
* @param int $limit
*/
public function set_pcre_backtrack_limit($limit)
{
$this->pcre_backtrack_limit = (int) $limit;
}
/**
* Sets the PCRE recursion limit for this script
* @param int $limit
*/
public function set_pcre_recursion_limit($limit)
{
$this->pcre_recursion_limit = (int) $limit;
}
/**
* Try to configure PHP to use at least the suggested minimum settings
*/
private function do_raise_php_limits()
{
$php_limits = array(
'memory_limit' => $this->memory_limit,
'max_execution_time' => $this->max_execution_time,
'pcre.backtrack_limit' => $this->pcre_backtrack_limit,
'pcre.recursion_limit' => $this->pcre_recursion_limit
);
// If current settings are higher respect them.
foreach ($php_limits as $name => $suggested) {
$current = $this->normalize_int(ini_get($name));
// memory_limit exception: allow -1 for "no memory limit".
if ($current > -1 && ($suggested == -1 || $current < $suggested)) {
ini_set($name, $suggested);
}
}
}
/**
* Does bulk of the minification
* @param string $css
* @param int|bool $linebreak_pos
* @return string
*/
private function minify($css, $linebreak_pos)
{
// strings are safe, now wrestle the comments
for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
$token = $this->comments[$i];
$placeholder = '/' . self::COMMENT . $i . '___/';
// ! in the first position of the comment means preserve
// so push to the preserved tokens keeping the !
if (substr($token, 0, 1) === '!') {
$this->preserved_tokens[] = $token;
$token_tring = self::TOKEN . (count($this->preserved_tokens) - 1) . '___';
$css = preg_replace($placeholder, $token_tring, $css, 1);
// Preserve new lines for /*! important comments
$css = preg_replace('/\s*[\n\r\f]+\s*(\/\*'. $token_tring .')/S', self::NL.'$1', $css);
$css = preg_replace('/('. $token_tring .'\*\/)\s*[\n\r\f]+\s*/S', '$1'.self::NL, $css);
continue;
}
// \ in the last position looks like hack for Mac/IE5
// shorten that to /*\*/ and the next one to /**/
if (substr($token, (strlen($token) - 1), 1) === '\\') {
$this->preserved_tokens[] = '\\';
$css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
$i = $i + 1; // attn: advancing the loop
$this->preserved_tokens[] = '';
$css = preg_replace('/' . self::COMMENT . $i . '___/', self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
continue;
}
// keep empty comments after child selectors (IE7 hack)
// e.g. html >/**/ body
if (strlen($token) === 0) {
$start_index = $this->index_of($css, $this->str_slice($placeholder, 1, -1));
if ($start_index > 2) {
if (substr($css, $start_index - 3, 1) === '>') {
$this->preserved_tokens[] = '';
$css = preg_replace($placeholder, self::TOKEN . (count($this->preserved_tokens) - 1) . '___', $css, 1);
}
}
}
// in all other cases kill the comment
$css = preg_replace('/\/\*' . $this->str_slice($placeholder, 1, -1) . '\*\//', '', $css, 1);
}
// Normalize all whitespace strings to single spaces. Easier to work with that way.
$css = preg_replace('/\s+/', ' ', $css);
// Shorten & preserve calculations calc(...) since spaces are important
$css = preg_replace_callback('/calc(\((?:[^\(\)]+|(?1))*\))/i', array($this, 'replace_calc'), $css);
// Replace positive sign from numbers preceded by : or a white-space before the leading space is removed
// +1.2em to 1.2em, +.8px to .8px, +2% to 2%
$css = preg_replace('/((?<!\\\\)\:|\s)\+(\.?\d+)/S', '$1$2', $css);
// Remove leading zeros from integer and float numbers preceded by : or a white-space
// 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05
$css = preg_replace('/((?<!\\\\)\:|\s)(\-?)0+(\.?\d+)/S', '$1$2$3', $css);
// Remove trailing zeros from float numbers preceded by : or a white-space
// -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px
$css = preg_replace('/((?<!\\\\)\:|\s)(\-?)(\d?\.\d+?)0+([^\d])/S', '$1$2$3$4', $css);
// Remove trailing .0 -> -9.0 to -9
$css = preg_replace('/((?<!\\\\)\:|\s)(\-?\d+)\.0([^\d])/S', '$1$2$3', $css);
// Replace 0 length numbers with 0
$css = preg_replace('/((?<!\\\\)\:|\s)\-?\.?0+([^\d])/S', '${1}0$2', $css);
// Remove the spaces before the things that should not have spaces before them.
// But, be careful not to turn "p :link {...}" into "p:link{...}"
// Swap out any pseudo-class colons with the token, and then swap back.
$css = preg_replace_callback('/(?:^|\})(?:(?:[^\{\:])+\:)+(?:[^\{]*\{)/', array($this, 'replace_colon'), $css);
$css = preg_replace('/\s+([\!\{\}\;\:\>\+\(\)\]\~\=,])/', '$1', $css);
$css = preg_replace('/' . self::CLASSCOLON . '/', ':', $css);
// retain space for special IE6 cases
$css = preg_replace('/\:first\-(line|letter)(\{|,)/i', ':first-$1 $2', $css);
// no space after the end of a preserved comment
$css = preg_replace('/\*\/ /', '*/', $css);
// Put the space back in some cases, to support stuff like
// @media screen and (-webkit-min-device-pixel-ratio:0){
$css = preg_replace('/\band\(/i', 'and (', $css);
// Remove the spaces after the things that should not have spaces after them.
$css = preg_replace('/([\!\{\}\:;\>\+\(\[\~\=,])\s+/S', '$1', $css);
// remove unnecessary semicolons
$css = preg_replace('/;+\}/', '}', $css);
// Fix for issue: #2528146
// Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack)
// to avoid issues on Symbian S60 3.x browsers.
$css = preg_replace('/(\*[a-z0-9\-]+\s*\:[^;\}]+)(\})/', '$1;$2', $css);
// Replace 0 length units 0(px,em,%) with 0.
$css = preg_replace('/((?<!\\\\)\:|\s)\-?0(?:em|ex|ch|rem|vw|vh|vm|vmin|cm|mm|in|px|pt|pc|%)/iS', '${1}0', $css);
// Replace 0 0; or 0 0 0; or 0 0 0 0; with 0.
$css = preg_replace('/\:0(?: 0){1,3}(;|\})/', ':0$1', $css);
// Fix for issue: #2528142
// Replace text-shadow:0; with text-shadow:0 0 0;
$css = preg_replace('/(text-shadow\:0)(;|\})/ie', "strtolower('$1 0 0$2')", $css);
// Replace background-position:0; with background-position:0 0;
// same for transform-origin
$css = preg_replace('/(background\-position|(?:webkit|moz|o|ms|)\-?transform\-origin)\:0(;|\})/ieS', "strtolower('$1:0 0$2')", $css);
// Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space)
// Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space)
// This makes it more likely that it'll get further compressed in the next step.
$css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'rgb_to_hex'), $css);
$css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-\.\%]+)\s*\)(.{1})/i', array($this, 'hsl_to_hex'), $css);
// Shorten colors from #AABBCC to #ABC or short color name.
$css = $this->compress_hex_colors($css);
// border: none to border:0, outline: none to outline:0
$css = preg_replace('/(border\-?(?:top|right|bottom|left|)|outline)\:none(;|\})/ieS', "strtolower('$1:0$2')", $css);
// shorter opacity IE filter
$css = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $css);
// Remove empty rules.
$css = preg_replace('/[^\};\{\/]+\{\}/S', '', $css);
// Some source control tools don't like it when files containing lines longer
// than, say 8000 characters, are checked in. The linebreak option is used in
// that case to split long lines after a specific column.
if ($linebreak_pos !== FALSE && (int) $linebreak_pos >= 0) {
$linebreak_pos = (int) $linebreak_pos;
$start_index = $i = 0;
while ($i < strlen($css)) {
$i++;
if ($css[$i - 1] === '}' && $i - $start_index > $linebreak_pos) {
$css = $this->str_slice($css, 0, $i) . "\n" . $this->str_slice($css, $i);
$start_index = $i;
}
}
}
// Replace multiple semi-colons in a row by a single one
// See SF bug #1980989
$css = preg_replace('/;;+/', ';', $css);
// Restore new lines for /*! important comments
$css = preg_replace('/'. self::NL .'/', "\n", $css);
// restore preserved comments and strings
for ($i = 0, $max = count($this->preserved_tokens); $i < $max; $i++) {
$css = preg_replace('/' . self::TOKEN . $i . '___/', $this->preserved_tokens[$i], $css, 1);
}
// Trim the final string (for any leading or trailing white spaces)
return trim($css);
}
/**
* Utility method to replace all data urls with tokens before we start
* compressing, to avoid performance issues running some of the subsequent
* regexes against large strings chunks.
*
* @param string $css
* @return string
*/
private function extract_data_urls($css)
{
// Leave data urls alone to increase parse performance.
$max_index = strlen($css) - 1;
$append_index = $index = $last_index = $offset = 0;
$sb = array();
$pattern = '/url\(\s*(["\']?)data\:/i';
// Since we need to account for non-base64 data urls, we need to handle
// ' and ) being part of the data string. Hence switching to indexOf,
// to determine whether or not we have matching string terminators and
// handling sb appends directly, instead of using matcher.append* methods.
while (preg_match($pattern, $css, $m, 0, $offset)) {
$index = $this->index_of($css, $m[0], $offset);
$last_index = $index + strlen($m[0]);
$start_index = $index + 4; // "url(".length()
$end_index = $last_index - 1;
$terminator = $m[1]; // ', " or empty (not quoted)
$found_terminator = FALSE;
if (strlen($terminator) === 0) {
$terminator = ')';
}
while ($found_terminator === FALSE && $end_index+1 <= $max_index) {
$end_index = $this->index_of($css, $terminator, $end_index + 1);
// endIndex == 0 doesn't really apply here
if ($end_index > 0 && substr($css, $end_index - 1, 1) !== '\\') {
$found_terminator = TRUE;
if (')' != $terminator) {
$end_index = $this->index_of($css, ')', $end_index);
}
}
}
// Enough searching, start moving stuff over to the buffer
$sb[] = $this->substring($css, $append_index, $index);
if ($found_terminator) {
$token = $this->substring($css, $start_index, $end_index);
$token = preg_replace('/\s+/', '', $token);
$this->preserved_tokens[] = $token;
$preserver = 'url(' . self::TOKEN . (count($this->preserved_tokens) - 1) . '___)';
$sb[] = $preserver;
$append_index = $end_index + 1;
} else {
// No end terminator found, re-add the whole match. Should we throw/warn here?
$sb[] = $this->substring($css, $index, $last_index);
$append_index = $last_index;
}
$offset = $last_index;
}
$sb[] = $this->substring($css, $append_index);
return implode('', $sb);
}
/**
* Utility method to compress hex color values of the form #AABBCC to #ABC or short color name.
*
* DOES NOT compress CSS ID selectors which match the above pattern (which would break things).
* e.g. #AddressForm { ... }
*
* DOES NOT compress IE filters, which have hex color values (which would break things).
* e.g. filter: chroma(color="#FFFFFF");
*
* DOES NOT compress invalid hex values.
* e.g. background-color: #aabbccdd
*
* @param string $css
* @return string
*/
private function compress_hex_colors($css)
{
// Look for hex colors inside { ... } (to avoid IDs) and which don't have a =, or a " in front of them (to avoid filters)
$pattern = '/(\=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS';
$_index = $index = $last_index = $offset = 0;
$sb = array();
// See: http://ajaxmin.codeplex.com/wikipage?title=CSS%20Colors
$short_safe = array(
'#808080' => 'gray',
'#008000' => 'green',
'#800000' => 'maroon',
'#000080' => 'navy',
'#808000' => 'olive',
'#800080' => 'purple',
'#c0c0c0' => 'silver',
'#008080' => 'teal',
'#f00' => 'red'
);
while (preg_match($pattern, $css, $m, 0, $offset)) {
$index = $this->index_of($css, $m[0], $offset);
$last_index = $index + strlen($m[0]);
$is_filter = (bool) $m[1];
$sb[] = $this->substring($css, $_index, $index);
if ($is_filter) {
// Restore, maintain case, otherwise filter will break
$sb[] = $m[1] . '#' . $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7];
} else {
if (strtolower($m[2]) == strtolower($m[3]) &&
strtolower($m[4]) == strtolower($m[5]) &&
strtolower($m[6]) == strtolower($m[7])) {
// Compress.
$hex = '#' . strtolower($m[3] . $m[5] . $m[7]);
} else {
// Non compressible color, restore but lower case.
$hex = '#' . strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]);
}
// replace Hex colors to short safe color names
$sb[] = array_key_exists($hex, $short_safe) ? $short_safe[$hex] : $hex;
}
$_index = $offset = $last_index - strlen($m[8]);
}
$sb[] = $this->substring($css, $_index);
return implode('', $sb);
}
/* CALLBACKS
* ---------------------------------------------------------------------------------------------
*/
private function replace_string($matches)
{
$match = $matches[0];
$quote = substr($match, 0, 1);
// Must use addcslashes in PHP to avoid parsing of backslashes
$match = addcslashes($this->str_slice($match, 1, -1), '\\');
// maybe the string contains a comment-like substring?
// one, maybe more? put'em back then
if (($pos = $this->index_of($match, self::COMMENT)) >= 0) {
for ($i = 0, $max = count($this->comments); $i < $max; $i++) {
$match = preg_replace('/' . self::COMMENT . $i . '___/', $this->comments[$i], $match, 1);
}
}
// minify alpha opacity in filter strings
$match = preg_replace('/progid\:DXImageTransform\.Microsoft\.Alpha\(Opacity\=/i', 'alpha(opacity=', $match);
$this->preserved_tokens[] = $match;
return $quote . self::TOKEN . (count($this->preserved_tokens) - 1) . '___' . $quote;
}
private function replace_colon($matches)
{
return preg_replace('/\:/', self::CLASSCOLON, $matches[0]);
}
private function replace_calc($matches)
{
$this->preserved_tokens[] = preg_replace('/\s?([\*\/\(\),])\s?/', '$1', $matches[0]);
return self::TOKEN . (count($this->preserved_tokens) - 1) . '___';
}
private function rgb_to_hex($matches)
{
// Support for percentage values rgb(100%, 0%, 45%);
if ($this->index_of($matches[1], '%') >= 0){
$rgbcolors = explode(',', str_replace('%', '', $matches[1]));
for ($i = 0; $i < count($rgbcolors); $i++) {
$rgbcolors[$i] = $this->round_number(floatval($rgbcolors[$i]) * 2.55);
}
} else {
$rgbcolors = explode(',', $matches[1]);
}
// Values outside the sRGB color space should be clipped (0-255)
for ($i = 0; $i < count($rgbcolors); $i++) {
$rgbcolors[$i] = $this->clamp_number(intval($rgbcolors[$i], 10), 0, 255);
$rgbcolors[$i] = sprintf("%02x", $rgbcolors[$i]);
}
// Fix for issue #2528093
if (!preg_match('/[\s\,\);\}]/', $matches[2])){
$matches[2] = ' ' . $matches[2];
}
return '#' . implode('', $rgbcolors) . $matches[2];
}
private function hsl_to_hex($matches)
{
$values = explode(',', str_replace('%', '', $matches[1]));
$h = floatval($values[0]);
$s = floatval($values[1]);
$l = floatval($values[2]);
// Wrap and clamp, then fraction!
$h = ((($h % 360) + 360) % 360) / 360;
$s = $this->clamp_number($s, 0, 100) / 100;
$l = $this->clamp_number($l, 0, 100) / 100;
if ($s == 0) {
$r = $g = $b = $this->round_number(255 * $l);
} else {
$v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l);
$v1 = (2 * $l) - $v2;
$r = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h + (1/3)));
$g = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h));
$b = $this->round_number(255 * $this->hue_to_rgb($v1, $v2, $h - (1/3)));
}
return $this->rgb_to_hex(array('', $r.','.$g.','.$b, $matches[2]));
}
/* HELPERS
* ---------------------------------------------------------------------------------------------
*/
private function hue_to_rgb($v1, $v2, $vh)
{
$vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh);
if ($vh * 6 < 1) return $v1 + ($v2 - $v1) * 6 * $vh;
if ($vh * 2 < 1) return $v2;
if ($vh * 3 < 2) return $v1 + ($v2 - $v1) * ((2/3) - $vh) * 6;
return $v1;
}
private function round_number($n)
{
return intval(floor(floatval($n) + 0.5), 10);
}
private function clamp_number($n, $min, $max)
{
return min(max($n, $min), $max);
}
/**
* PHP port of Javascript's "indexOf" function for strings only
* Author: Tubal Martin http://blog.margenn.com
*
* @param string $haystack
* @param string $needle
* @param int $offset index (optional)
* @return int
*/
private function index_of($haystack, $needle, $offset = 0)
{
$index = strpos($haystack, $needle, $offset);
return ($index !== FALSE) ? $index : -1;
}
/**
* PHP port of Javascript's "substring" function
* Author: Tubal Martin http://blog.margenn.com
* Tests: http://margenn.com/tubal/substring/
*
* @param string $str
* @param int $from index
* @param int|bool $to index (optional)
* @return string
*/
private function substring($str, $from = 0, $to = FALSE)
{
if ($to !== FALSE) {
if ($from == $to || ($from <= 0 && $to < 0)) {
return '';
}
if ($from > $to) {
$from_copy = $from;
$from = $to;
$to = $from_copy;
}
}
if ($from < 0) {
$from = 0;
}
$substring = ($to === FALSE) ? substr($str, $from) : substr($str, $from, $to - $from);
return ($substring === FALSE) ? '' : $substring;
}
/**
* PHP port of Javascript's "slice" function for strings only
* Author: Tubal Martin http://blog.margenn.com
* Tests: http://margenn.com/tubal/str_slice/
*
* @param string $str
* @param int $start index
* @param int|bool $end index (optional)
* @return string
*/
private function str_slice($str, $start = 0, $end = FALSE)
{
if ($end !== FALSE && ($start < 0 || $end <= 0)) {
$max = strlen($str);
if ($start < 0) {
if (($start = $max + $start) < 0) {
return '';
}
}
if ($end < 0) {
if (($end = $max + $end) < 0) {
return '';
}
}
if ($end <= $start) {
return '';
}
}
$slice = ($end === FALSE) ? substr($str, $start) : substr($str, $start, $end - $start);
return ($slice === FALSE) ? '' : $slice;
}
/**
* Convert strings like "64M" or "30" to int values
* @param mixed $size
* @return int
*/
private function normalize_int($size)
{
if (is_string($size)) {
switch (substr($size, -1)) {
case 'M': case 'm': return $size * 1048576;
case 'K': case 'k': return $size * 1024;
case 'G': case 'g': return $size * 1073741824;
}
}
return (int) $size;
}
}

View file

@ -0,0 +1,12 @@
name = AdvAgg CDN Javascript
description = Use a shared CDN for javascript libraries, Google Libraries API currently.
package = Advanced CSS/JS Aggregation
core = 6.x
dependencies[] = advagg
; Information added by Drupal.org packaging script on 2017-03-18
version = "6.x-1.11"
core = "6.x"
project = "advagg"
datestamp = "1489800488"

View file

@ -0,0 +1,166 @@
<?php
/**
* @file
* Advanced aggregation js cdn library module.
*
*/
/**
* Default value to see if jquery should be grabbed from the Google CDN.
*/
define('ADVAGG_JS_CDN_JQUERY', TRUE);
/**
* Default value to see if jquery-ui should be grabbed from the Google CDN.
*/
define('ADVAGG_JS_CDN_JQUERY_UI', TRUE);
/**
* Default value to see if SWFObject should be grabbed from the Google CDN.
*/
define('ADVAGG_JS_CDN_SWFOBJECT', TRUE);
/**
* Implement hook_advagg_js_pre_alter.
*/
function advagg_js_cdn_advagg_js_pre_alter(&$javascript, $preprocess_js, $public_downloads, $scope) {
// Exit early if we are not going to CDN any code.
$cdn_jquery = variable_get('advagg_js_cdn_jquery', ADVAGG_JS_CDN_JQUERY);
$cdn_jquery_ui = variable_get('advagg_js_cdn_jquery_ui', ADVAGG_JS_CDN_JQUERY_UI);
$cdn_swfobject = variable_get('advagg_js_cdn_swfobject', ADVAGG_JS_CDN_SWFOBJECT);
if ( !$cdn_jquery
&& !$cdn_jquery_ui
&& !$cdn_swfobject
) {
return FALSE;
}
// Set filepaths/versions
$jquery_version = variable_get('advagg_js_cdn_jquery_version', '1.2.6');
$jquery_filepath = 'misc/jquery.js';
if (module_exists('jquery_update') && variable_get('jquery_update_replace', TRUE)) {
// jquery_update_get_version hits disk and doesn't get cached.
// advagg_js_cdn_advagg_js_pre_alter will run mutiple times per request so
// caching the value of $jquery_update_version here.
static $jquery_update_version;
if (empty($jquery_update_version)) {
$jquery_update_version = variable_get('advagg_js_cdn_jquery_update_version', jquery_update_get_version());
}
$jquery_update_filepath = advagg_js_cdn_get_jquery_path();
}
if (module_exists('jquery_ui')) {
// jquery_ui_get_version hits disk and doesn't get cached.
// advagg_js_cdn_advagg_js_pre_alter will run mutiple times per request so
// caching the value of jquery_ui_get_version here.
static $jquery_ui_version;
if (empty($jquery_ui_version)) {
$jquery_ui_version = jquery_ui_get_version();
}
$jquery_ui_path = advagg_js_cdn_get_jquery_ui_path();
}
if (module_exists('swftools')) {
$swfobject_version = variable_get('advagg_js_cdn_swfobject_version', '2.2');
if (function_exists('swftools_get_player_path')) {
$swfobject_filepath = swftools_get_player_path() . '/swfobject2/swfobject.js';
}
elseif (function_exists('swftools_get_library')) {
$swfobject_filepath = swftools_get_library('swfobject') . '/swfobject.js';
}
}
// http or https.
$schema = advagg_get_server_schema();
foreach ($javascript as $type => $data) {
// Skip inline and setting js.
if (!$data || $type == 'setting' || $type == 'inline') {
continue;
}
// Search and replace.
foreach ($data as $path => $info) {
// jquery.js
if ($cdn_jquery) {
if ( isset($jquery_update_filepath)
&& ( $path == $jquery_update_filepath . '/jquery.min.js'
|| $path == $jquery_update_filepath . '/jquery.js'
)
) {
$info['preprocess'] = FALSE;
$javascript['external'][$schema . '://ajax.googleapis.com/ajax/libs/jquery/' . $jquery_update_version . '/jquery.min.js'] = $info;
unset($javascript[$type][$path]);
}
elseif ($path == $jquery_filepath) {
$info['preprocess'] = FALSE;
$javascript['external'][$schema . '://ajax.googleapis.com/ajax/libs/jquery/' . $jquery_version . '/jquery.min.js'] = $info;
unset($javascript[$type][$path]);
}
}
// Replace all jquery_ui scripts (jquery.ui.*.js) with a single
// Google-hosted minified version containing all of them.
if ($cdn_jquery_ui) {
if ( isset($jquery_ui_path)
&& isset($jquery_ui_version)
&& strpos($path, $jquery_ui_path) === 0
) {
$info['preprocess'] = FALSE;
$javascript['external'][$schema . '://ajax.googleapis.com/ajax/libs/jqueryui/' . $jquery_ui_version . '/jquery-ui.min.js'] = $info;
unset($javascript[$type][$path]);
}
}
// swfobject.js
if ($cdn_swfobject) {
if ( isset($swfobject_filepath)
&& $path == $swfobject_filepath
) {
$info['preprocess'] = FALSE;
$javascript['external'][$schema . '://ajax.googleapis.com/ajax/libs/swfobject/' . $swfobject_version . '/swfobject.js'] = $info;
unset($javascript[$type][$path]);
}
}
}
}
}
/**
* Get the path for the jquery-ui.js file.
*
* @param string $file
* filename.
*/
function advagg_js_cdn_get_jquery_ui_path() {
$jquery_ui_path_const = 'JQUERY_UI_PATH';
if (!defined($jquery_ui_path_const)) {
if (!function_exists('jquery_ui_get_path')) {
return FALSE;
}
$jquery_ui_path = jquery_ui_get_path();
if ($jquery_ui_path === FALSE) {
return FALSE;
}
}
else {
$jquery_ui_path = constant($jquery_ui_path_const);
}
$jquery_ui_path .= '/ui';
return $jquery_ui_path;
}
/**
* Get the path for the jquery.js file.
*
* @return
* path to jquery.js file.
*/
function advagg_js_cdn_get_jquery_path() {
if (function_exists('jquery_update_jquery_path')) {
return dirname(jquery_update_jquery_path());
}
else {
$jquery_update_filepath = drupal_get_path('module', 'jquery_update');
return $jquery_update_filepath . '/replace/';
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Admin page callbacks for the advagg JS compression module.
*/
/**
* Page generation function for admin/settings/js-compress
*/
function advagg_js_compress_admin_page() {
$output = '';
return $output . drupal_get_form('advagg_js_compress_admin_settings_form');
}
/**
* Form builder; Configure advagg settings.
*
* @ingroup forms
* @see system_settings_form()
*/
function advagg_js_compress_admin_settings_form() {
$form = array();
$form['advagg_js_compress_agg_files'] = array(
'#type' => 'checkbox',
'#title' => t('Compress JS Files'),
'#default_value' => variable_get('advagg_js_compress_agg_files', ADVAGG_JS_COMPRESS_AGG_FILES),
);
$form['advagg_js_compress_inline'] = array(
'#type' => 'checkbox',
'#title' => t('Compress Inline JS'),
'#default_value' => variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE),
);
$description = '';
$options = array(0 => t('JSMin+'));
if (function_exists('jsmin')) {
$options[1] = t('JSMin');
$description .= t('JSMin is the C complied version and is about 25 times faster. Recommend using it.');
}
else {
$description .= t('You can use the much faster C version of JSMin by installing the <a href="@php_jsmin">JSMin PHP Extension</a> on this server.', array('@php_jsmin' => 'http://www.ypass.net/software/php_jsmin/'));
}
$form['advagg_js_compressor'] = array(
'#type' => 'radios',
'#title' => t('Select the compression program to use'),
'#default_value' => variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR),
'#options' => $options,
'#description' => filter_xss($description),
);
$form['advagg_js_compress_packer_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Enable Packer'),
'#default_value' => variable_get('advagg_js_compress_packer_enable', ADVAGG_JS_COMPRESS_PACKER_ENABLE),
'#description' => t('If enabled the non gzip version of JS files will be compressed using the JS Packer. WARNING: This has a high chance of breaking your JS. Only Enable on production after testing the non gzipped version locally.'),
);
return system_settings_form($form);
}

View file

@ -0,0 +1,12 @@
name = AdvAgg Compress Javascript
description = Compress Javascript with a 3rd party compressor, JSMin+ currently.
package = Advanced CSS/JS Aggregation
core = 6.x
dependencies[] = advagg
; Information added by Drupal.org packaging script on 2017-03-18
version = "6.x-1.11"
core = "6.x"
project = "advagg"
datestamp = "1489800488"

View file

@ -0,0 +1,197 @@
<?php
/**
* @file
* Handles AdvAgg JS compress installation and upgrade tasks.
*/
/**
* Implementation of hook_enable().
*/
function advagg_js_compress_enable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
// Check runtime requirements
register_shutdown_function('advagg_js_compress_requirements', 'runtime');
}
/**
* Implementation of hook_disable().
*/
function advagg_js_compress_disable() {
// Flush advagg caches.
$cache_tables = advagg_flush_caches();
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
}
}
/**
* Implementation of hook_install().
*/
function advagg_js_compress_install() {
drupal_install_schema('advagg_js_compress');
}
/**
* Implementation of hook_uninstall().
*/
function advagg_js_compress_uninstall() {
// Remove variables.
variable_del('advagg_js_compress_packer_enable');
variable_del('advagg_js_compress_inline_cache');
variable_del('advagg_js_compress_file_cache');
variable_del('advagg_js_compress_agg_files');
variable_del('advagg_js_max_compress_ratio');
variable_del('advagg_js_compress_callback');
variable_del('advagg_js_compress_inline');
variable_del('advagg_js_compress_ratio');
variable_del('advagg_js_compressor');
// Remove our cache table.
cache_clear_all('*', 'cache_advagg_js_compress_inline', TRUE);
cache_clear_all('*', 'cache_advagg_js_compress_file', TRUE);
drupal_uninstall_schema('advagg_js_compress');
}
/**
* Implementation of hook_requirements().
*/
function advagg_js_compress_requirements($phase) {
$requirements = array();
// Ensure translations don't break at install time
$t = get_t();
// Report Drupal version
if ($phase == 'runtime') {
$compressible = advagg_js_compress_check_callback();
$advagg_js_compress_callback = variable_get('advagg_js_compress_callback', ADVAGG_JS_COMPRESS_CALLBACK);
if (is_array($compressible)) {
$requirements['advagg_js_compress_callback'] = array(
'title' => $t('AdvAgg JS Compress - Callback'),
'severity' => REQUIREMENT_WARNING,
'value' => $t('The callback for testing if a JS file is compressible is not working.'),
'description' => $t('As a result if jsmin+ encounters a file that it cannot compress, it will kill that PHP process.'),
);
if ($advagg_js_compress_callback != FALSE) {
variable_set('advagg_js_compress_callback', FALSE);
}
}
else {
$requirements['advagg_js_compress_callback'] = array(
'title' => $t('AdvAgg JS Compress - Callback'),
'severity' => REQUIREMENT_OK,
'value' => $t('The callback is working correctly.'),
);
if ($advagg_js_compress_callback == FALSE) {
variable_set('advagg_js_compress_callback', TRUE);
}
}
// Test the 'memory_limit' PHP configuration directive
$memory_limit = ini_get('memory_limit');
$compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
// If $memory_limit contains a value of -1, the PHP runtime
// doesn't impose a limit on memory used by PHP scripts
if ($compressor == 0 && $memory_limit && $memory_limit != -1 && parse_size($memory_limit) < parse_size('96M')) {
$requirements['advagg_js_compress_memory_limit'] = array(
'title' => $t('AdvAgg JS Compress - Memory Limit'),
'value' => $memory_limit,
'severity' => REQUIREMENT_WARNING,
'description' => $t('It is highly recommended that you set your PHP memory_limit at least 96M if you are going to use JSMin+.'),
);
}
}
return $requirements;
}
/**
* Check to see if the CSS/JS generator is working.
*/
function advagg_js_compress_check_callback() {
$filename = drupal_get_path('module', 'advagg_js_compress') . '/jquery.form.js';
$files_to_test = array();
$files_to_test[] = array(
'md5' => md5($filename),
'filename' => $filename,
);
$compressible = advagg_js_compress_test_compression($files_to_test);
return $compressible;
}
/**
* Implementation of hook_schema().
*/
function advagg_js_compress_schema() {
$schema = array();
// Create cache tables.
$schema['cache_advagg_js_compress_inline'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_advagg_js_compress_inline']['description'] = t('Cache table for Advanced CSS/JS Aggregations JS Compress module. Used to keep inline versions of compressed JS.');
$schema['cache_advagg_js_compress_file'] = drupal_get_schema_unprocessed('system', 'cache');
$schema['cache_advagg_js_compress_file']['description'] = t('Cache table for Advanced CSS/JS Aggregations JS Compress module. Used to keep the compressed JavaScript from the js files.');
return $schema;
}
/**
* Update 6100 - Clear file cache.
*/
function advagg_js_compress_update_6100() {
$ret = array();
cache_clear_all('*', 'cache_advagg_files_data', TRUE);
$ret[] = array(
'success' => TRUE,
'query' => 'advagg files_data cache flushed.',
);
return $ret;
}
/**
* Update 6101 - Create the cache_advagg_css_compress_inline cache table.
*/
function advagg_js_compress_update_6101() {
$ret = array();
// Create cache table.
$schema = advagg_js_compress_schema();
db_create_table($ret, 'cache_advagg_js_compress_inline', $schema['cache_advagg_js_compress_inline']);
return $ret;
}
/**
* Update 6102 - Create the cache_advagg_css_compress_file cache table.
*/
function advagg_js_compress_update_6102() {
$ret = array();
// Create cache table.
$schema = advagg_js_compress_schema();
db_create_table($ret, 'cache_advagg_js_compress_file', $schema['cache_advagg_js_compress_file']);
return $ret;
}
/**
* Update 6103 - Clear the cache_advagg_css_compress_file cache table.
*/
function advagg_js_compress_update_6103() {
$ret = array();
// Clear cache_advagg_js_compress_file cache.
cache_clear_all('*', 'cache_advagg_js_compress_file', TRUE);
$ret[] = array(
'success' => TRUE,
'query' => 'The cache_advagg_js_compress_file table has been cleared.',
);
return $ret;
}

View file

@ -0,0 +1,616 @@
<?php
/**
* @file
* Advanced CSS/JS aggregation js compression module.
*
*/
/**
* Default value to see if the callback is working.
*/
define('ADVAGG_JS_COMPRESS_CALLBACK', FALSE);
/**
* Default value to see packer is enabled.
*/
define('ADVAGG_JS_COMPRESS_PACKER_ENABLE', FALSE);
/**
* Default value to see what compressor to use. 0 is JSMin+.
*/
define('ADVAGG_JS_COMPRESSOR', 0);
/**
* Default value for the compression ratio test.
*/
define('ADVAGG_JS_COMPRESS_RATIO', 0.1);
/**
* Default value for the compression ratio test.
*/
define('ADVAGG_JS_MAX_COMPRESS_RATIO', 0.98);
/**
* Default value to see if this will compress aggregated files.
*/
define('ADVAGG_JS_COMPRESS_AGG_FILES', TRUE);
/**
* Default value to see if this will compress inline js.
*/
define('ADVAGG_JS_COMPRESS_INLINE', TRUE);
/**
* Default value to see if this will cache the compressed inline js.
*/
define('ADVAGG_JS_COMPRESS_INLINE_CACHE', TRUE);
/**
* Default value to see if this will cache the compressed inline js.
*/
define('ADVAGG_JS_COMPRESS_FILE_CACHE', TRUE);
/**
* Implementation of hook_menu
*/
function advagg_js_compress_menu() {
$items = array();
$file_path = drupal_get_path('module', 'advagg_js_compress');
$items['advagg/js_compress_test_file'] = array(
'page callback' => 'advagg_js_compress_test_file',
'type' => MENU_CALLBACK,
'access callback' => TRUE,
);
$items['admin/settings/advagg/js-compress'] = array(
'title' => 'JS Compression',
'description' => 'Adjust JS Compression settings.',
'page callback' => 'advagg_js_compress_admin_page',
'type' => MENU_LOCAL_TASK,
'access arguments' => array('administer site configuration'),
'file path' => $file_path,
'file' => 'advagg_js_compress.admin.inc',
'weight' => 10,
);
return $items;
}
/**
* Implement hook_init.
*/
function advagg_js_compress_init() {
global $conf;
if (variable_get('advagg_js_compress_packer_enable', ADVAGG_JS_COMPRESS_PACKER_ENABLE)) {
$conf['advagg_file_save_function'] = 'advagg_js_compress_file_saver';
}
}
/**
* Implement hook_advagg_files_table.
*/
function advagg_js_compress_advagg_files_table($row, $checksum) {
// IF the file has changed, test it's compressibility.
if ($row['filetype'] === 'js' && $checksum !== $row['checksum']) {
$files_to_test[] = array(
'md5' => $row['filename_md5'],
'filename' => $row['filename'],
);
advagg_js_compress_test_compression($files_to_test);
}
}
/**
* Implement hook_advagg_js_pre_alter.
*/
function advagg_js_compress_advagg_js_pre_alter(&$javascript, $preprocess_js, $public_downloads, $scope) {
if (module_exists('jquery_update')) {
return;
}
foreach ($javascript as $type => $data) {
if (!$data) {
continue;
}
if ($type == 'setting' || $type == 'inline') {
continue;
}
foreach ($data as $path => $info) {
if ($path == 'misc/jquery.form.js') {
$new_path = drupal_get_path('module', 'advagg_js_compress') . '/jquery.form.js';
$javascript[$type][$new_path] = $info;
unset($javascript[$type][$path]);
}
}
}
}
/**
* Implement hook_advagg_js_alter.
*/
function advagg_js_compress_advagg_js_alter(&$contents, $files, $bundle_md5) {
if (!variable_get('advagg_js_compress_agg_files', ADVAGG_JS_COMPRESS_AGG_FILES)) {
return;
}
advagg_js_compress_prep($contents, $files, $bundle_md5);
}
/**
* Implement hook_advagg_js_inline_alter.
*/
function advagg_js_compress_advagg_js_inline_alter(&$contents) {
if (!variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE)) {
return;
}
$compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
// If using a cache, try to get the contents of it.
if (variable_get('advagg_js_compress_inline_cache', ADVAGG_JS_COMPRESS_INLINE_CACHE)) {
$key = md5($contents) . $compressor;
$table = 'cache_advagg_js_compress_inline';
$data = cache_get($key, $table);
if (!empty($data->data)) {
$contents = $data->data;
return;
}
}
if ($compressor == 0) {
$original_contents = $contents;
list($before, $after) = advagg_js_compress_jsminplus($contents);
$ratio = 0;
if ($before != 0) {
$ratio = ($before - $after) / $before;
}
// Make sure the returned string is not empty or has a VERY high
// compression ratio.
if (empty($contents) || empty($ratio) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
$contents = $original_contents;
}
}
if ($compressor == 1) {
$contents = jsmin($contents);
// Ensure that $contents ends with ; or }.
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
// ; or } not found, add in ; to the end of $contents.
$contents = trim($contents) . ';';
}
}
// If using a cache set it.
if (isset($key)) {
cache_set($key, $contents, $table, CACHE_TEMPORARY);
}
}
/**
* Compress a JS string
*
* @param $contents
* Javascript string.
*/
function advagg_js_compress_prep(&$contents, $files, $bundle_md5) {
// Make sure every file in this aggregate is compressible.
$files_to_test = array();
$list_bad = array();
foreach ($files as $filename) {
$filename_md5 = md5($filename);
$data = advagg_get_file_data($filename_md5);
// File needs to be tested.
if (empty($data['advagg_js_compress']['tested'])) {
$files_to_test[] = array(
'md5' => $filename_md5,
'filename' => $filename,
);
}
elseif ($data['advagg_js_compress']['tested']['jsminplus'] != 1) {
$list_bad[$filename] = $filename;
}
}
$advagg_js_compress_callback = variable_get('advagg_js_compress_callback', ADVAGG_JS_COMPRESS_CALLBACK);
if ($advagg_js_compress_callback) {
// Send test files to worker.
if (!empty($files_to_test)) {
$compressible = advagg_js_compress_test_compression($files_to_test);
// If an array then it is a list of files that can not be compressed.
if (is_array($compressible)) {
// Place filename in an array key.
foreach ($compressible as $filedata) {
$filename = $filedata['filename'];
$list_bad[$filename] = $filename;
}
}
}
}
$contents = '';
// Do not compress the file that it bombs on.
// Compress each file individually.
foreach ($files as $file) {
if (!empty($list_bad[$file])) {
$contents .= advagg_build_js_bundle(array($file));
}
else {
$data = advagg_build_js_bundle(array($file));
// If using a cache, try to get the contents of it.
$cached = FALSE;
if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
$key = $file;
$table = 'cache_advagg_js_compress_file';
$cached_data = cache_get($key, $table);
if (!empty($cached_data->data)) {
$data = $cached_data->data;
$cached = TRUE;
}
}
if (!$cached && !empty($data)) {
$compressor = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR);
if ($compressor == 0) {
list($before, $after) = advagg_js_compress_jsminplus($data);
$ratio = 0;
if ($before != 0) {
$ratio = ($before - $after) / $before;
}
// Make sure the returned string is not empty or has a VERY high
// compression ratio.
if (empty($data) || empty($ratio) || $ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
$data = advagg_build_js_bundle(array($file));
}
elseif (isset($key)) {
// If using a cache set it.
cache_set($key, $data, $table);
}
}
elseif ($compressor == 1) {
$contents = jsmin($contents);
// Ensure that $contents ends with ; or }.
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
// ; or } not found, add in ; to the end of $contents.
$contents = trim($contents) . ';';
}
}
}
$url = url($file, array('absolute' => TRUE));
$contents .= "/* Source and licensing information for the line(s) below can be found at $url. */\n" . $data . ";\n/* Source and licensing information for the above line(s) can be found at $url. */\n";
}
}
}
/**
* Compress a JS string using jsmin+
*
* @param $contents
* Javascript string.
* @return
* array with the size before and after.
*/
function advagg_js_compress_jsminplus(&$contents) {
// Try to allocate enough time to run JSMin+.
if (function_exists('set_time_limit')) {
@set_time_limit(variable_get('advagg_set_time_limit', ADVAGG_SET_TIME_LIMIT));
}
// Only include jsminplus.inc if the JSMinPlus class doesn't exist.
if (!class_exists('JSMinPlus')) {
include(drupal_get_path('module', 'advagg_js_compress') . '/jsminplus.inc');
}
// Get the JS string length before the compression operation.
$before = strlen($contents);
$original_contents = $contents;
try {
// Strip Byte Order Marks (BOM's) from the file, JSMin+ cannot parse these.
$contents = str_replace(pack("CCC", 0xef, 0xbb, 0xbf), "", $contents);
ob_start();
// JSMin+ the contents of the aggregated file.
$contents = JSMinPlus::minify($contents);
$error = trim(ob_get_contents());
if (!empty($error)) {
throw new Exception($error);
}
// Ensure that $contents ends with ; or }.
if (strpbrk(substr(trim($contents), -1), ';}') === FALSE) {
// ; or } not found, add in ; to the end of $contents.
$contents = trim($contents) . ';';
}
// Get the JS string length after the compression operation.
$after = strlen($contents);
}
catch (Exception $e) {
// Log the exception thrown by JSMin+ and roll back to uncompressed content.
watchdog('advagg', $e->getMessage() . '<pre>' . $original_contents . '</pre>', NULL, WATCHDOG_WARNING);
$contents = $original_contents;
$after = $before;
}
ob_end_clean();
return array($before, $after);
}
/**
* Run various theme functions so the cache is primed.
*
* @param $files_to_test
* array with md5 and filename.
* @return
* TRUE if all files are compressible. List of files that failed otherwise.
*/
function advagg_js_compress_test_compression($files_to_test) {
global $base_path;
$bad_files = array();
// Blacklist jquery.min.js from getting compressed.
if (module_exists('jquery_update')) {
foreach ($files_to_test as $key => $info) {
if (strpos($info['filename'], 'jquery.min.js') !== FALSE) {
// Add file to the bad list.
$bad_files[] = $info;
unset($files_to_test[$key]);
// Get file data.
$filename_md5 = md5($info['filename']);
$lock_name = 'advagg_set_file_data_' . $filename_md5;
if (!lock_acquire($lock_name, 10)) {
lock_wait($lock_name);
continue;
}
$data = advagg_get_file_data($filename_md5);
// Set to -2
if (!isset($data->data['advagg_js_compress']['tested']['jsminplus']) || $data->data['advagg_js_compress']['tested']['jsminplus'] != -2) {
$data['advagg_js_compress']['tested']['jsminplus'] = -2;
advagg_set_file_data($filename_md5, $data);
}
lock_release($lock_name);
}
}
}
foreach ($files_to_test as $info) {
$key = variable_get('advagg_js_compress_url_key', FALSE);
if (empty($key)) {
$key = mt_rand();
variable_set('advagg_js_compress_url_key', $key);
}
// Clear the cache for this file
cache_clear_all($info['filename'], 'cache_advagg_js_compress_file');
// Setup request URL and headers.
$query['values'] = $info;
$query['key'] = $key;
$query_string = http_build_query($query, '', '&');
$url = _advagg_build_url('advagg/js_compress_test_file');
$headers = array(
'Host' => $_SERVER['HTTP_HOST'],
'Content-Type' => 'application/x-www-form-urlencoded',
'Connection' => 'close',
);
$results = drupal_http_request($url, $headers, 'POST', $query_string);
// Get file data.
$filename_md5 = md5($info['filename']);
$data = advagg_get_file_data($filename_md5);
// Mark as a bad file.
if (empty($data['advagg_js_compress']['tested']['jsminplus']) || $data['advagg_js_compress']['tested']['jsminplus'] != 1) {
$bad_files[] = $info;
}
}
if (empty($bad_files)) {
return TRUE;
}
return $bad_files;
}
/**
* Run various theme functions so the cache is primed.
*
* @param $values
* object File info
*/
function advagg_js_compress_test_file($values = NULL) {
// watchdog('debug', str_replace(' ', '&nbsp;&nbsp;&nbsp;&nbsp;', nl2br(htmlentities(print_r($values, TRUE) . print_r($_REQUEST, TRUE)))));
// Exit if key does not match & called with $file not set.
if (is_null($values)) {
if (empty($_POST['key']) || empty($_POST['values'])) {
return;
}
$key = variable_get('advagg_js_compress_url_key', FALSE);
if ($key != $_POST['key']) {
return;
}
$values = array();
$values['values'] = $_POST['values'];
}
$filename = $values['values']['filename'];
$md5 = $values['values']['md5'];
// Compression test file if it exists.
advagg_clearstatcache(TRUE, $filename);
if (file_exists($filename)) {
$contents = file_get_contents($filename);
$filesize = filesize($filename);
$lock_name = 'advagg_set_file_data_' . $md5;
if (!lock_acquire($lock_name, 45)) {
lock_wait($lock_name);
echo $md5;
exit;
}
$data = advagg_get_file_data($md5);
// Set to "-1" so if php bombs out, the file will be marked as bad.
$data['advagg_js_compress']['tested']['jsminplus'] = -1;
advagg_set_file_data($md5, $data);
// Compress the data.
list($before, $after) = advagg_js_compress_jsminplus($contents);
// Set to "-2" if compression ratio sucks.
$ratio = 0;
if ($before != 0) {
$ratio = ($before - $after) / $before;
}
if ($ratio < variable_get('advagg_js_compress_ratio', ADVAGG_JS_COMPRESS_RATIO)) {
$data['advagg_js_compress']['tested']['jsminplus'] = -2;
advagg_set_file_data($md5, $data);
lock_release($lock_name);
echo $md5;
exit;
}
// Set to "-3" if the compression ratio is way too good.
if ($ratio > variable_get('advagg_js_max_compress_ratio', ADVAGG_JS_MAX_COMPRESS_RATIO)) {
$data['advagg_js_compress']['tested']['jsminplus'] = -3;
advagg_set_file_data($md5, $data);
lock_release($lock_name);
echo $md5;
exit;
}
// Everything worked, mark this file as compressible.
$data['advagg_js_compress']['tested']['jsminplus'] = 1;
advagg_set_file_data($md5, $data);
// Set the file cache.
if (variable_get('advagg_js_compress_file_cache', ADVAGG_JS_COMPRESS_FILE_CACHE)) {
$key = $filename;
$table = 'cache_advagg_js_compress_file';
cache_set($key, $contents, $table);
}
}
if (isset($lock_name)) {
lock_release($lock_name);
}
echo $md5;
exit;
}
/**
* Save a string to the specified destination. Verify that file size is not zero.
*
* @param $data
* A string containing the contents of the file.
* @param $dest
* A string containing the destination location.
* @return
* Boolean indicating if the file save was successful.
*/
function advagg_js_compress_file_saver($data, $dest, $force, $type) {
if ($type == 'css') {
return advagg_file_saver($data, $dest, $force, $type);
}
if (!variable_get('advagg_gzip_compression', ADVAGG_GZIP_COMPRESSION) || !extension_loaded('zlib')) {
return advagg_file_saver($data, $dest, $force, $type);
}
// Get file save function
$file_save_data = 'file_save_data';
$custom_path = variable_get('advagg_custom_files_dir', ADVAGG_CUSTOM_FILES_DIR);
if (!empty($custom_path)) {
$file_save_data = 'advagg_file_save_data';
}
// Gzip first.
$gzip_dest = $dest . '.gz';
advagg_clearstatcache(TRUE, $gzip_dest);
if (!file_exists($gzip_dest) || $force) {
$gzip_data = gzencode($data, 9, FORCE_GZIP);
if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
return FALSE;
}
// Make sure filesize is not zero.
advagg_clearstatcache(TRUE, $gzip_dest);
if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
if (!$file_save_data($gzip_data, $gzip_dest, FILE_EXISTS_REPLACE)) {
return FALSE;
}
advagg_clearstatcache(TRUE, $gzip_dest);
if (@filesize($gzip_dest) == 0 && !empty($gzip_data)) {
// Filename is bad, create a new one next time.
file_delete($gzip_dest);
return FALSE;
}
}
}
// Use packer on JS data.
advagg_js_compress_jspacker($data);
// Write File.
if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
return FALSE;
}
// Make sure filesize is not zero.
advagg_clearstatcache(TRUE, $dest);
if (@filesize($dest) == 0 && !empty($data)) {
if (!$file_save_data($data, $dest, FILE_EXISTS_REPLACE)) {
return FALSE;
}
advagg_clearstatcache(TRUE, $dest);
if (@filesize($dest) == 0 && !empty($data)) {
// Filename is bad, create a new one next time.
file_delete($dest);
return FALSE;
}
}
// Make sure .htaccess file exists.
advagg_htaccess_check_generate($dest);
cache_set($dest, time(), 'cache_advagg', CACHE_PERMANENT);
return TRUE;
}
/**
* Compress a JS string using packer.
*
* @param $contents
* Javascript string.
*/
function advagg_js_compress_jspacker(&$contents) {
// Use Packer on the contents of the aggregated file.
require_once(drupal_get_path('module', 'advagg_js_compress') . '/jspacker.inc');
// Add semicolons to the end of lines if missing.
$contents = str_replace("}\n", "};\n", $contents);
$contents = str_replace("\nfunction", ";\nfunction", $contents);
// Remove char returns, looking at you lightbox2.
$contents = str_replace("\n\r", "", $contents);
$contents = str_replace("\r", "", $contents);
$contents = str_replace("\n", "", $contents);
$packer = new JavaScriptPacker($contents, 62, TRUE, FALSE);
$contents = $packer->pack();
}
/**
* Implementation of hook_flush_caches().
*/
function advagg_js_compress_flush_caches() {
return array('cache_advagg_js_compress_inline');
}
/**
* Implementation of hook_advagg_master_reset().
*/
function advagg_js_compress_advagg_master_reset() {
cache_clear_all('*', 'cache_advagg_js_compress_inline', TRUE);
cache_clear_all('*', 'cache_advagg_js_compress_file', TRUE);
}

View file

@ -0,0 +1,660 @@
/*
* jQuery Form Plugin
* version: 2.36 (07-NOV-2009)
* @requires jQuery v1.2.6 or later
*
* Examples and documentation at: http://malsup.com/jquery/form/
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
;(function($) {
/*
Usage Note:
-----------
Do not use both ajaxSubmit and ajaxForm on the same form. These
functions are intended to be exclusive. Use ajaxSubmit if you want
to bind your own submit handler to the form. For example,
$(document).ready(function() {
$('#myForm').bind('submit', function() {
$(this).ajaxSubmit({
target: '#output'
});
return false; // <-- important!
});
});
Use ajaxForm when you want the plugin to manage all the event binding
for you. For example,
$(document).ready(function() {
$('#myForm').ajaxForm({
target: '#output'
});
});
When using ajaxForm, the ajaxSubmit function will be invoked for you
at the appropriate time.
*/
/**
* ajaxSubmit() provides a mechanism for immediately submitting
* an HTML form using AJAX.
*/
$.fn.ajaxSubmit = function(options) {
// fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
if (!this.length) {
log('ajaxSubmit: skipping submit process - no element selected');
return this;
}
if (typeof options == 'function')
options = { success: options };
var url = $.trim(this.attr('action'));
if (url) {
// clean url (don't include hash vaue)
url = (url.match(/^([^#]+)/)||[])[1];
}
url = url || window.location.href || '';
options = $.extend({
url: url,
type: this.attr('method') || 'GET',
iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
}, options || {});
// hook for manipulating the form data before it is extracted;
// convenient for use with rich editors like tinyMCE or FCKEditor
var veto = {};
this.trigger('form-pre-serialize', [this, options, veto]);
if (veto.veto) {
log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
return this;
}
// provide opportunity to alter form data before it is serialized
if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
log('ajaxSubmit: submit aborted via beforeSerialize callback');
return this;
}
var a = this.formToArray(options.semantic);
if (options.data) {
options.extraData = options.data;
for (var n in options.data) {
if(options.data[n] instanceof Array) {
for (var k in options.data[n])
a.push( { name: n, value: options.data[n][k] } );
}
else
a.push( { name: n, value: options.data[n] } );
}
}
// give pre-submit callback an opportunity to abort the submit
if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
log('ajaxSubmit: submit aborted via beforeSubmit callback');
return this;
}
// fire vetoable 'validate' event
this.trigger('form-submit-validate', [a, this, options, veto]);
if (veto.veto) {
log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
return this;
}
var q = $.param(a);
if (options.type.toUpperCase() == 'GET') {
options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
options.data = null; // data is null for 'get'
}
else
options.data = q; // data is the query string for 'post'
var $form = this, callbacks = [];
if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
// perform a load on the target only if dataType is not provided
if (!options.dataType && options.target) {
var oldSuccess = options.success || function(){};
callbacks.push(function(data) {
$(options.target).html(data).each(oldSuccess, arguments);
});
}
else if (options.success)
callbacks.push(options.success);
options.success = function(data, status) {
for (var i=0, max=callbacks.length; i < max; i++)
callbacks[i].apply(options, [data, status, $form]);
};
// are there files to upload?
var files = $('input:file', this).fieldValue();
var found = false;
for (var j=0; j < files.length; j++)
if (files[j])
found = true;
var multipart = false;
// var mp = 'multipart/form-data';
// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
// options.iframe allows user to force iframe mode
// 06-NOV-09: now defaulting to iframe mode if file input is detected
if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
// hack to fix Safari hang (thanks to Tim Molendijk for this)
// see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
if (options.closeKeepAlive)
$.get(options.closeKeepAlive, fileUpload);
else
fileUpload();
}
else
$.ajax(options);
// fire 'notify' event
this.trigger('form-submit-notify', [this, options]);
return this;
// private function for handling file uploads (hat tip to YAHOO!)
function fileUpload() {
var form = $form[0];
if ($(':input[name=submit]', form).length) {
alert(Drupal.t('Error: Form elements must not be named "submit".'));
return;
}
var opts = $.extend({}, $.ajaxSettings, options);
var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
var id = 'jqFormIO' + (new Date().getTime());
var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />');
var io = $io[0];
$io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
var xhr = { // mock object
aborted: 0,
responseText: null,
responseXML: null,
status: 0,
statusText: 'n/a',
getAllResponseHeaders: function() {},
getResponseHeader: function() {},
setRequestHeader: function() {},
abort: function() {
this.aborted = 1;
$io.attr('src', opts.iframeSrc); // abort op in progress
}
};
var g = opts.global;
// trigger ajax global events so that activity/block indicators work like normal
if (g && ! $.active++) $.event.trigger("ajaxStart");
if (g) $.event.trigger("ajaxSend", [xhr, opts]);
if (s.beforeSend && s.beforeSend(xhr, s) === false) {
s.global && $.active--;
return;
}
if (xhr.aborted)
return;
var cbInvoked = 0;
var timedOut = 0;
// add submitting element to data if we know it
var sub = form.clk;
if (sub) {
var n = sub.name;
if (n && !sub.disabled) {
options.extraData = options.extraData || {};
options.extraData[n] = sub.value;
if (sub.type == "image") {
options.extraData[name+'.x'] = form.clk_x;
options.extraData[name+'.y'] = form.clk_y;
}
}
}
// take a breath so that pending repaints get some cpu time before the upload starts
setTimeout(function() {
// make sure form attrs are set
var t = $form.attr('target'), a = $form.attr('action');
// update form attrs in IE friendly way
form.setAttribute('target',id);
if (form.getAttribute('method') != 'POST')
form.setAttribute('method', 'POST');
if (form.getAttribute('action') != opts.url)
form.setAttribute('action', opts.url);
// ie borks in some cases when setting encoding
if (! options.skipEncodingOverride) {
$form.attr({
encoding: 'multipart/form-data',
enctype: 'multipart/form-data'
});
}
// support timout
if (opts.timeout)
setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
// add "extra" data to form if provided in options
var extraInputs = [];
try {
if (options.extraData)
for (var n in options.extraData)
extraInputs.push(
$('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
.appendTo(form)[0]);
// add iframe to doc and submit the form
$io.appendTo('body');
io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
form.submit();
}
finally {
// reset attrs and remove "extra" input elements
form.setAttribute('action',a);
t ? form.setAttribute('target', t) : $form.removeAttr('target');
$(extraInputs).remove();
}
}, 10);
var domCheckCount = 50;
function cb() {
if (cbInvoked++) return;
io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
var ok = true;
try {
if (timedOut) throw 'timeout';
// extract the server response from the iframe
var data, doc;
doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
log('isXml='+isXml);
if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
if (--domCheckCount) {
// in some browsers (Opera) the iframe DOM is not always traversable when
// the onload callback fires, so we loop a bit to accommodate
cbInvoked = 0;
setTimeout(cb, 100);
return;
}
log('Could not access iframe DOM after 50 tries.');
return;
}
xhr.responseText = doc.body ? doc.body.innerHTML : null;
xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
xhr.getResponseHeader = function(header){
var headers = {'content-type': opts.dataType};
return headers[header];
};
if (opts.dataType == 'json' || opts.dataType == 'script') {
// see if user embedded response in textarea
var ta = doc.getElementsByTagName('textarea')[0];
if (ta)
xhr.responseText = ta.value;
else {
// account for browsers injecting pre around json response
var pre = doc.getElementsByTagName('pre')[0];
if (pre)
xhr.responseText = pre.innerHTML;
}
}
else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
xhr.responseXML = toXml(xhr.responseText);
}
data = $.httpData(xhr, opts.dataType);
}
catch(e){
ok = false;
$.handleError(opts, xhr, 'error', e);
}
// ordering of these callbacks/triggers is odd, but that's how $.ajax does it
if (ok) {
opts.success(data, 'success');
if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
}
if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
if (g && ! --$.active) $.event.trigger("ajaxStop");
if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
// clean up
setTimeout(function() {
$io.remove();
xhr.responseXML = null;
}, 100);
};
function toXml(s, doc) {
if (window.ActiveXObject) {
doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = 'false';
doc.loadXML(s);
}
else
doc = (new DOMParser()).parseFromString(s, 'text/xml');
return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
};
};
};
/**
* ajaxForm() provides a mechanism for fully automating form submission.
*
* The advantages of using this method instead of ajaxSubmit() are:
*
* 1: This method will include coordinates for <input type="image" /> elements (if the element
* is used to submit the form).
* 2. This method will include the submit element's name/value data (for the element that was
* used to submit the form).
* 3. This method binds the submit() method to the form for you.
*
* The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
* passes the options argument along after properly binding events for submit elements and
* the form itself.
*/
$.fn.ajaxForm = function(options) {
return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
$(this).ajaxSubmit(options);
return false;
}).bind('click.form-plugin', function(e) {
var target = e.target;
var $el = $(target);
if (!($el.is(":submit,input:image"))) {
// is this a child element of the submit el? (ex: a span within a button)
var t = $el.closest(':submit');
if (t.length == 0)
return;
target = t[0];
}
var form = this;
form.clk = target;
if (target.type == 'image') {
if (e.offsetX != undefined) {
form.clk_x = e.offsetX;
form.clk_y = e.offsetY;
} else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
var offset = $el.offset();
form.clk_x = e.pageX - offset.left;
form.clk_y = e.pageY - offset.top;
} else {
form.clk_x = e.pageX - target.offsetLeft;
form.clk_y = e.pageY - target.offsetTop;
}
}
// clear form vars
setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
});
};
// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
$.fn.ajaxFormUnbind = function() {
return this.unbind('submit.form-plugin click.form-plugin');
};
/**
* formToArray() gathers form element data into an array of objects that can
* be passed to any of the following ajax functions: $.get, $.post, or load.
* Each object in the array has both a 'name' and 'value' property. An example of
* an array for a simple login form might be:
*
* [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
*
* It is this array that is passed to pre-submit callback functions provided to the
* ajaxSubmit() and ajaxForm() methods.
*/
$.fn.formToArray = function(semantic) {
var a = [];
if (this.length == 0) return a;
var form = this[0];
var els = semantic ? form.getElementsByTagName('*') : form.elements;
if (!els) return a;
for(var i=0, max=els.length; i < max; i++) {
var el = els[i];
var n = el.name;
if (!n) continue;
if (semantic && form.clk && el.type == "image") {
// handle image inputs on the fly when semantic == true
if(!el.disabled && form.clk == el) {
a.push({name: n, value: $(el).val()});
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
}
continue;
}
var v = $.fieldValue(el, true);
if (v && v.constructor == Array) {
for(var j=0, jmax=v.length; j < jmax; j++)
a.push({name: n, value: v[j]});
}
else if (v !== null && typeof v != 'undefined')
a.push({name: n, value: v});
}
if (!semantic && form.clk) {
// input type=='image' are not found in elements array! handle it here
var $input = $(form.clk), input = $input[0], n = input.name;
if (n && !input.disabled && input.type == 'image') {
a.push({name: n, value: $input.val()});
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
}
}
return a;
};
/**
* Serializes form data into a 'submittable' string. This method will return a string
* in the format: name1=value1&amp;name2=value2
*/
$.fn.formSerialize = function(semantic) {
//hand off to jQuery.param for proper encoding
return $.param(this.formToArray(semantic));
};
/**
* Serializes all field elements in the jQuery object into a query string.
* This method will return a string in the format: name1=value1&amp;name2=value2
*/
$.fn.fieldSerialize = function(successful) {
var a = [];
this.each(function() {
var n = this.name;
if (!n) return;
var v = $.fieldValue(this, successful);
if (v && v.constructor == Array) {
for (var i=0,max=v.length; i < max; i++)
a.push({name: n, value: v[i]});
}
else if (v !== null && typeof v != 'undefined')
a.push({name: this.name, value: v});
});
//hand off to jQuery.param for proper encoding
return $.param(a);
};
/**
* Returns the value(s) of the element in the matched set. For example, consider the following form:
*
* <form><fieldset>
* <input name="A" type="text" />
* <input name="A" type="text" />
* <input name="B" type="checkbox" value="B1" />
* <input name="B" type="checkbox" value="B2"/>
* <input name="C" type="radio" value="C1" />
* <input name="C" type="radio" value="C2" />
* </fieldset></form>
*
* var v = $(':text').fieldValue();
* // if no values are entered into the text inputs
* v == ['','']
* // if values entered into the text inputs are 'foo' and 'bar'
* v == ['foo','bar']
*
* var v = $(':checkbox').fieldValue();
* // if neither checkbox is checked
* v === undefined
* // if both checkboxes are checked
* v == ['B1', 'B2']
*
* var v = $(':radio').fieldValue();
* // if neither radio is checked
* v === undefined
* // if first radio is checked
* v == ['C1']
*
* The successful argument controls whether or not the field element must be 'successful'
* (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
* The default value of the successful argument is true. If this value is false the value(s)
* for each element is returned.
*
* Note: This method *always* returns an array. If no valid value can be determined the
* array will be empty, otherwise it will contain one or more values.
*/
$.fn.fieldValue = function(successful) {
for (var val=[], i=0, max=this.length; i < max; i++) {
var el = this[i];
var v = $.fieldValue(el, successful);
if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
continue;
v.constructor == Array ? $.merge(val, v) : val.push(v);
}
return val;
};
/**
* Returns the value of the field element.
*/
$.fieldValue = function(el, successful) {
var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
if (typeof successful == 'undefined') successful = true;
if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
(t == 'checkbox' || t == 'radio') && !el.checked ||
(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
tag == 'select' && el.selectedIndex == -1))
return null;
if (tag == 'select') {
var index = el.selectedIndex;
if (index < 0) return null;
var a = [], ops = el.options;
var one = (t == 'select-one');
var max = (one ? index+1 : ops.length);
for(var i=(one ? index : 0); i < max; i++) {
var op = ops[i];
if (op.selected) {
var v = op.value;
if (!v) // extra pain for IE...
v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
if (one) return v;
a.push(v);
}
}
return a;
}
return el.value;
};
/**
* Clears the form data. Takes the following actions on the form's input fields:
* - input text fields will have their 'value' property set to the empty string
* - select elements will have their 'selectedIndex' property set to -1
* - checkbox and radio inputs will have their 'checked' property set to false
* - inputs of type submit, button, reset, and hidden will *not* be effected
* - button elements will *not* be effected
*/
$.fn.clearForm = function() {
return this.each(function() {
$('input,select,textarea', this).clearFields();
});
};
/**
* Clears the selected form elements.
*/
$.fn.clearFields = $.fn.clearInputs = function() {
return this.each(function() {
var t = this.type, tag = this.tagName.toLowerCase();
if (t == 'text' || t == 'password' || tag == 'textarea')
this.value = '';
else if (t == 'checkbox' || t == 'radio')
this.checked = false;
else if (tag == 'select')
this.selectedIndex = -1;
});
};
/**
* Resets the form data. Causes all form elements to be reset to their original value.
*/
$.fn.resetForm = function() {
return this.each(function() {
// guard against an input with the name of 'reset'
// note that IE reports the reset function as an 'object'
if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
this.reset();
});
};
/**
* Enables or disables any matching elements.
*/
$.fn.enable = function(b) {
if (b == undefined) b = true;
return this.each(function() {
this.disabled = !b;
});
};
/**
* Checks/unchecks any matching checkboxes or radio buttons and
* selects/deselects and matching option elements.
*/
$.fn.selected = function(select) {
if (select == undefined) select = true;
return this.each(function() {
var t = this.type;
if (t == 'checkbox' || t == 'radio')
this.checked = select;
else if (this.tagName.toLowerCase() == 'option') {
var $sel = $(this).parent('select');
if (select && $sel[0] && $sel[0].type == 'select-one') {
// deselect all other options
$sel.find('option').selected(false);
}
this.selected = select;
}
});
};
// helper fn for console logging
// set $.fn.ajaxSubmit.debug to true to enable debug logging
function log() {
if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
};
})(jQuery);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,777 @@
<?php
/* 9 April 2008. version 1.1
*
* This is the php version of the Dean Edwards JavaScript's Packer,
* Based on :
*
* ParseMaster, version 1.0.2 (2005-08-19) Copyright 2005, Dean Edwards
* a multi-pattern parser.
* KNOWN BUG: erroneous behavior when using escapeChar with a replacement
* value that is a function
*
* packer, version 2.0.2 (2005-08-19) Copyright 2004-2005, Dean Edwards
*
* License: http://creativecommons.org/licenses/LGPL/2.1/
*
* Ported to PHP by Nicolas Martin.
*
* ----------------------------------------------------------------------
* changelog:
* 1.1 : correct a bug, '\0' packed then unpacked becomes '\'.
* ----------------------------------------------------------------------
*
* examples of usage :
* $myPacker = new JavaScriptPacker($script, 62, true, false);
* $packed = $myPacker->pack();
*
* or
*
* $myPacker = new JavaScriptPacker($script, 'Normal', true, false);
* $packed = $myPacker->pack();
*
* or (default values)
*
* $myPacker = new JavaScriptPacker($script);
* $packed = $myPacker->pack();
*
*
* params of the constructor :
* $script: the JavaScript to pack, string.
* $encoding: level of encoding, int or string :
* 0,10,62,95 or 'None', 'Numeric', 'Normal', 'High ASCII'.
* default: 62.
* $fastDecode: include the fast decoder in the packed result, boolean.
* default : true.
* $specialChars: if you are flagged your private and local variables
* in the script, boolean.
* default: false.
*
* The pack() method return the compressed JavasScript, as a string.
*
* see http://dean.edwards.name/packer/usage/ for more information.
*
* Notes :
* # need PHP 5 . Tested with PHP 5.1.2, 5.1.3, 5.1.4, 5.2.3
*
* # The packed result may be different than with the Dean Edwards
* version, but with the same length. The reason is that the PHP
* function usort to sort array don't necessarily preserve the
* original order of two equal member. The Javascript sort function
* in fact preserve this order (but that's not require by the
* ECMAScript standard). So the encoded keywords order can be
* different in the two results.
*
* # Be careful with the 'High ASCII' Level encoding if you use
* UTF-8 in your files...
*/
class JavaScriptPacker {
// constants
const IGNORE = '$1';
// validate parameters
private $_script = '';
private $_encoding = 62;
private $_fastDecode = true;
private $_specialChars = false;
private $LITERAL_ENCODING = array(
'None' => 0,
'Numeric' => 10,
'Normal' => 62,
'High ASCII' => 95,
);
public function __construct($_script, $_encoding = 62, $_fastDecode = true, $_specialChars = false) {
$this->_script = $_script . "\n";
if (array_key_exists($_encoding, $this->LITERAL_ENCODING)) {
$_encoding = $this->LITERAL_ENCODING[$_encoding];
}
$this->_encoding = min((int) $_encoding, 95);
$this->_fastDecode = $_fastDecode;
$this->_specialChars = $_specialChars;
}
public function pack() {
$this->_addParser('_basicCompression');
if ($this->_specialChars) {
$this->_addParser('_encodeSpecialChars');
}
if ($this->_encoding) {
$this->_addParser('_encodeKeywords');
}
// go!
return $this->_pack($this->_script);
}
// apply all parsing routines
private function _pack($script) {
for ($i = 0; isset($this->_parsers[$i]); $i++) {
$script = call_user_func(array(&$this, $this->_parsers[$i]), $script);
}
return $script;
}
// keep a list of parsing functions, they'll be executed all at once
private $_parsers = array();
private function _addParser($parser) {
$this->_parsers[] = $parser;
}
// zero encoding - just removal of white space and comments
private function _basicCompression($script) {
$parser = new ParseMaster();
// make safe
$parser->escapeChar = '\\';
// protect strings
$parser->add('/\'[^\'\\n\\r]*\'/', self::IGNORE);
$parser->add('/"[^"\\n\\r]*"/', self::IGNORE);
// remove comments
$parser->add('/\\/\\/[^\\n\\r]*[\\n\\r]/', ' ');
$parser->add('/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//', ' ');
// protect regular expressions
$parser->add('/\\s+(\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?)/', '$2'); // IGNORE
$parser->add('/[^\\w\\x24\\/\'"*)\\?:]\\/[^\\/\\n\\r\\*][^\\/\\n\\r]*\\/g?i?/', self::IGNORE);
// remove: ;;; doSomething();
if ($this->_specialChars) {
$parser->add('/;;;[^\\n\\r]+[\\n\\r]/');
}
// remove redundant semi-colons
$parser->add('/\\(;;\\)/', self::IGNORE); // protect for (;;) loops
$parser->add('/;+\\s*([};])/', '$2');
// apply the above
$script = $parser->exec($script);
// remove white-space
$parser->add('/(\\b|\\x24)\\s+(\\b|\\x24)/', '$2 $3');
$parser->add('/([+\\-])\\s+([+\\-])/', '$2 $3');
$parser->add('/\\s+/', '');
// done
return $parser->exec($script);
}
private function _encodeSpecialChars($script) {
$parser = new ParseMaster();
// replace: $name -> n, $$name -> na
$parser->add('/((\\x24+)([a-zA-Z$_]+))(\\d*)/',
array('fn' => '_replace_name')
);
// replace: _name -> _0, double-underscore (__name) is ignored
$regexp = '/\\b_[A-Za-z\\d]\\w*/';
// build the word list
$keywords = $this->_analyze($script, $regexp, '_encodePrivate');
// quick ref
$encoded = $keywords['encoded'];
$parser->add($regexp,
array(
'fn' => '_replace_encoded',
'data' => $encoded,
)
);
return $parser->exec($script);
}
private function _encodeKeywords($script) {
// escape high-ascii values already in the script (i.e. in strings)
if ($this->_encoding > 62) {
$script = $this->_escape95($script);
}
// create the parser
$parser = new ParseMaster();
$encode = $this->_getEncoder($this->_encoding);
// for high-ascii, don't encode single character low-ascii
$regexp = ($this->_encoding > 62) ? '/\\w\\w+/' : '/\\w+/';
// build the word list
$keywords = $this->_analyze($script, $regexp, $encode);
$encoded = $keywords['encoded'];
// encode
$parser->add($regexp,
array(
'fn' => '_replace_encoded',
'data' => $encoded,
)
);
if (empty($script)) {
return $script;
}
else {
//$res = $parser->exec($script);
//$res = $this->_bootStrap($res, $keywords);
//return $res;
return $this->_bootStrap($parser->exec($script), $keywords);
}
}
private function _analyze($script, $regexp, $encode) {
// analyse
// retreive all words in the script
$all = array();
preg_match_all($regexp, $script, $all);
$_sorted = array(); // list of words sorted by frequency
$_encoded = array(); // dictionary of word->encoding
$_protected = array(); // instances of "protected" words
$all = $all[0]; // simulate the javascript comportement of global match
if (!empty($all)) {
$unsorted = array(); // same list, not sorted
$protected = array(); // "protected" words (dictionary of word->"word")
$value = array(); // dictionary of charCode->encoding (eg. 256->ff)
$this->_count = array(); // word->count
$i = count($all);
$j = 0; //$word = null;
// count the occurrences - used for sorting later
do {
--$i;
$word = '$' . $all[$i];
if (!isset($this->_count[$word])) {
$this->_count[$word] = 0;
$unsorted[$j] = $word;
// make a dictionary of all of the protected words in this script
// these are words that might be mistaken for encoding
//if (is_string($encode) && method_exists($this, $encode))
$values[$j] = call_user_func(array(&$this, $encode), $j);
$protected['$' . $values[$j]] = $j++;
}
// increment the word counter
$this->_count[$word]++;
} while ($i > 0);
// prepare to sort the word list, first we must protect
// words that are also used as codes. we assign them a code
// equivalent to the word itself.
// e.g. if "do" falls within our encoding range
// then we store keywords["do"] = "do";
// this avoids problems when decoding
$i = count($unsorted);
do {
$word = $unsorted[--$i];
if (isset($protected[$word]) /*!= null*/) {
$_sorted[$protected[$word]] = substr($word, 1);
$_protected[$protected[$word]] = true;
$this->_count[$word] = 0;
}
} while ($i);
// sort the words by frequency
// Note: the javascript and php version of sort can be different :
// in php manual, usort :
// " If two members compare as equal,
// their order in the sorted array is undefined."
// so the final packed script is different of the Dean's javascript version
// but equivalent.
// the ECMAscript standard does not guarantee this behaviour,
// and thus not all browsers (e.g. Mozilla versions dating back to at
// least 2003) respect this.
usort($unsorted, array(&$this, '_sortWords'));
$j = 0;
// because there are "protected" words in the list
// we must add the sorted words around them
do {
if (!isset($_sorted[$i])) {
$_sorted[$i] = substr($unsorted[$j++], 1);
}
$_encoded[$_sorted[$i]] = $values[$i];
} while (++$i < count($unsorted));
}
return array(
'sorted' => $_sorted,
'encoded' => $_encoded,
'protected' => $_protected,
);
}
private $_count = array();
private function _sortWords($match1, $match2) {
return $this->_count[$match2] - $this->_count[$match1];
}
// build the boot function used for loading and decoding
private function _bootStrap($packed, $keywords) {
$ENCODE = $this->_safeRegExp('$encode\\($count\\)');
// $packed: the packed script
$packed = "'" . $this->_escape($packed) . "'";
// $ascii: base for encoding
$ascii = min(count($keywords['sorted']), $this->_encoding);
if ($ascii == 0) {
$ascii = 1;
}
// $count: number of words contained in the script
$count = count($keywords['sorted']);
// $keywords: list of words contained in the script
foreach ($keywords['protected'] as $i => $value) {
$keywords['sorted'][$i] = '';
}
// convert from a string to an array
ksort($keywords['sorted']);
$keywords = "'" . implode('|', $keywords['sorted']) . "'.split('|')";
$encode = ($this->_encoding > 62) ? '_encode95' : $this->_getEncoder($ascii);
$encode = $this->_getJSFunction($encode);
$encode = preg_replace('/_encoding/', '$ascii', $encode);
$encode = preg_replace('/arguments\\.callee/', '$encode', $encode);
$inline = '\\$count' . ($ascii > 10 ? '.toString(\\$ascii)' : '');
// $decode: code snippet to speed up decoding
if ($this->_fastDecode) {
// create the decoder
$decode = $this->_getJSFunction('_decodeBody');
if ($this->_encoding > 62) {
$decode = preg_replace('/\\\\w/', '[\\xa1-\\xff]', $decode);
}
// perform the encoding inline for lower ascii values
elseif ($ascii < 36) {
$decode = preg_replace($ENCODE, $inline, $decode);
}
// special case: when $count==0 there are no keywords. I want to keep
// the basic shape of the unpacking funcion so i'll frig the code...
if ($count == 0) {
$decode = preg_replace($this->_safeRegExp('($count)\\s*=\\s*1'), '$1=0', $decode, 1);
}
}
// boot function
$unpack = $this->_getJSFunction('_unpack');
if ($this->_fastDecode) {
// insert the decoder
$this->buffer = $decode;
$unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastDecode'), $unpack, 1);
}
$unpack = preg_replace('/"/', "'", $unpack);
if ($this->_encoding > 62) { // high-ascii
// get rid of the word-boundaries for regexp matches
$unpack = preg_replace('/\'\\\\\\\\b\'\s*\\+|\\+\s*\'\\\\\\\\b\'/', '', $unpack);
}
if ($ascii > 36 || $this->_encoding > 62 || $this->_fastDecode) {
// insert the encode function
$this->buffer = $encode;
$unpack = preg_replace_callback('/\\{/', array(&$this, '_insertFastEncode'), $unpack, 1);
}
else {
// perform the encoding inline
$unpack = preg_replace($ENCODE, $inline, $unpack);
}
// pack the boot function too
$unpackPacker = new JavaScriptPacker($unpack, 0, false, true);
$unpack = $unpackPacker->pack();
// arguments
$params = array($packed, $ascii, $count, $keywords);
if ($this->_fastDecode) {
$params[] = 0;
$params[] = '{}';
}
$params = implode(',', $params);
// the whole thing
return 'eval(' . $unpack . '(' . $params . "))\n";
}
private $buffer;
private function _insertFastDecode($match) {
return '{' . $this->buffer . ';';
}
private function _insertFastEncode($match) {
return '{$encode=' . $this->buffer . ';';
}
// mmm.. ..which one do i need ??
private function _getEncoder($ascii) {
return $ascii > 10 ? $ascii > 36 ? $ascii > 62 ?
'_encode95' : '_encode62' : '_encode36' : '_encode10';
}
// zero encoding
// characters: 0123456789
private function _encode10($charCode) {
return $charCode;
}
// inherent base36 support
// characters: 0123456789abcdefghijklmnopqrstuvwxyz
private function _encode36($charCode) {
return base_convert($charCode, 10, 36);
}
// hitch a ride on base36 and add the upper case alpha characters
// characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
private function _encode62($charCode) {
$res = '';
if ($charCode >= $this->_encoding) {
$res = $this->_encode62((int) ($charCode / $this->_encoding));
}
$charCode = $charCode % $this->_encoding;
if ($charCode > 35) {
return $res . chr($charCode + 29);
}
else {
return $res . base_convert($charCode, 10, 36);
}
}
// use high-ascii values
// characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
private function _encode95($charCode) {
$res = '';
if ($charCode >= $this->_encoding) {
$res = $this->_encode95($charCode / $this->_encoding);
}
return $res . chr(($charCode % $this->_encoding) + 161);
}
private function _safeRegExp($string) {
return '/' . preg_replace('/\$/', '\\\$', $string) . '/';
}
private function _encodePrivate($charCode) {
return "_" . $charCode;
}
// protect characters used by the parser
private function _escape($script) {
return preg_replace('/([\\\\\'])/', '\\\$1', $script);
}
// protect high-ascii characters already in the script
private function _escape95($script) {
return preg_replace_callback(
'/[\\xa1-\\xff]/',
array(&$this, '_escape95Bis'),
$script
);
}
private function _escape95Bis($match) {
return '\x' . ((string) dechex(ord($match)));
}
private function _getJSFunction($aName) {
if (defined('self::JSFUNCTION' . $aName)) {
return constant('self::JSFUNCTION' . $aName);
}
else {
return '';
}
}
// JavaScript Functions used.
// Note : In Dean's version, these functions are converted
// with 'String(aFunctionName);'.
// This internal conversion complete the original code, ex :
// 'while (aBool) anAction();' is converted to
// 'while (aBool) { anAction(); }'.
// The JavaScript functions below are corrected.
// unpacking function - this is the boot strap function
// data extracted from this packing routine is passed to
// this function when decoded in the target
// NOTE ! : without the ';' final.
const JSFUNCTION_unpack =
'function($packed, $ascii, $count, $keywords, $encode, $decode) {
while ($count--) {
if ($keywords[$count]) {
$packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
}
}
return $packed;
}';
/*
'function($packed, $ascii, $count, $keywords, $encode, $decode) {
while ($count--)
if ($keywords[$count])
$packed = $packed.replace(new RegExp(\'\\\\b\' + $encode($count) + \'\\\\b\', \'g\'), $keywords[$count]);
return $packed;
}';
*/
// code-snippet inserted into the unpacker to speed up decoding
const JSFUNCTION_decodeBody =
//_decode = function() {
// does the browser support String.replace where the
// replacement value is a function?
' if (!\'\'.replace(/^/, String)) {
// decode all the values we need
while ($count--) {
$decode[$encode($count)] = $keywords[$count] || $encode($count);
}
// global replacement function
$keywords = [function ($encoded) {return $decode[$encoded]}];
// generic match
$encode = function () {return \'\\\\w+\'};
// reset the loop counter - we are now doing a global replace
$count = 1;
}
';
//};
/*
' if (!\'\'.replace(/^/, String)) {
// decode all the values we need
while ($count--) $decode[$encode($count)] = $keywords[$count] || $encode($count);
// global replacement function
$keywords = [function ($encoded) {return $decode[$encoded]}];
// generic match
$encode = function () {return\'\\\\w+\'};
// reset the loop counter - we are now doing a global replace
$count = 1;
}';
*/
// zero encoding
// characters: 0123456789
const JSFUNCTION_encode10 =
'function($charCode) {
return $charCode;
}'; //;';
// inherent base36 support
// characters: 0123456789abcdefghijklmnopqrstuvwxyz
const JSFUNCTION_encode36 =
'function($charCode) {
return $charCode.toString(36);
}'; //;';
// hitch a ride on base36 and add the upper case alpha characters
// characters: 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
const JSFUNCTION_encode62 =
'function($charCode) {
return ($charCode < _encoding ? \'\' : arguments.callee(parseInt($charCode / _encoding))) +
(($charCode = $charCode % _encoding) > 35 ? String.fromCharCode($charCode + 29) : $charCode.toString(36));
}';
// use high-ascii values
// characters: ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþ
const JSFUNCTION_encode95 =
'function($charCode) {
return ($charCode < _encoding ? \'\' : arguments.callee($charCode / _encoding)) +
String.fromCharCode($charCode % _encoding + 161);
}';
}
class ParseMaster {
public $ignoreCase = false;
public $escapeChar = '';
// constants
const EXPRESSION = 0;
const REPLACEMENT = 1;
const LENGTH = 2;
// used to determine nesting levels
private $GROUPS = '/\\(/'; //g
private $SUB_REPLACE = '/\\$\\d/';
private $INDEXED = '/^\\$\\d+$/';
private $TRIM = '/([\'"])\\1\\.(.*)\\.\\1\\1$/';
private $ESCAPE = '/\\\./'; //g
private $QUOTE = '/\'/';
private $DELETED = '/\\x01[^\\x01]*\\x01/'; //g
public function add($expression, $replacement = '') {
// count the number of sub-expressions
// - add one because each pattern is itself a sub-expression
$length = 1 + preg_match_all($this->GROUPS, $this->_internalEscape((string) $expression), $out);
// treat only strings $replacement
if (is_string($replacement)) {
// does the pattern deal with sub-expressions?
if (preg_match($this->SUB_REPLACE, $replacement)) {
// a simple lookup? (e.g. "$2")
if (preg_match($this->INDEXED, $replacement)) {
// store the index (used for fast retrieval of matched strings)
$replacement = (int) (substr($replacement, 1)) - 1;
}
else { // a complicated lookup (e.g. "Hello $2 $1")
// build a function to do the lookup
$quote = preg_match($this->QUOTE, $this->_internalEscape($replacement))
? '"' : "'";
$replacement = array(
'fn' => '_backReferences',
'data' => array(
'replacement' => $replacement,
'length' => $length,
'quote' => $quote,
),
);
}
}
}
// pass the modified arguments
if (!empty($expression)) {
$this->_add($expression, $replacement, $length);
}
else {
$this->_add('/^$/', $replacement, $length);
}
}
public function exec($string) {
// execute the global replacement
$this->_escaped = array();
// simulate the _patterns.toSTring of Dean
$regexp = '/';
foreach ($this->_patterns as $reg) {
$regexp .= '(' . substr($reg[self::EXPRESSION], 1, -1) . ')|';
}
$regexp = substr($regexp, 0, -1) . '/';
$regexp .= ($this->ignoreCase) ? 'i' : '';
$string = $this->_escape($string, $this->escapeChar);
$string = preg_replace_callback(
$regexp,
array(
&$this,
'_replacement',
),
$string
);
$string = $this->_unescape($string, $this->escapeChar);
return preg_replace($this->DELETED, '', $string);
}
public function reset() {
// clear the patterns collection so that this object may be re-used
$this->_patterns = array();
}
// private
private $_escaped = array(); // escaped characters
private $_patterns = array(); // patterns stored by index
// create and add a new pattern to the patterns collection
private function _add() {
$arguments = func_get_args();
$this->_patterns[] = $arguments;
}
// this is the global replace function (it's quite complicated)
private function _replacement($arguments) {
if (empty($arguments)) {
return '';
}
$i = 1;
$j = 0;
// loop through the patterns
while (isset($this->_patterns[$j])) {
$pattern = $this->_patterns[$j++];
// do we have a result?
if (isset($arguments[$i]) && ($arguments[$i] != '')) {
$replacement = $pattern[self::REPLACEMENT];
if (is_array($replacement) && isset($replacement['fn'])) {
if (isset($replacement['data'])) {
$this->buffer = $replacement['data'];
}
return call_user_func(array(&$this, $replacement['fn']), $arguments, $i);
}
elseif (is_int($replacement)) {
return $arguments[$replacement + $i];
}
$delete = ($this->escapeChar == '' ||
strpos($arguments[$i], $this->escapeChar) === false)
? '' : "\x01" . $arguments[$i] . "\x01";
return $delete . $replacement;
// skip over references to sub-expressions
}
else {
$i += $pattern[self::LENGTH];
}
}
}
private function _backReferences($match, $offset) {
$replacement = $this->buffer['replacement'];
$quote = $this->buffer['quote'];
$i = $this->buffer['length'];
while ($i) {
$replacement = str_replace('$' . $i--, $match[$offset + $i], $replacement);
}
return $replacement;
}
private function _replace_name($match, $offset) {
$length = strlen($match[$offset + 2]);
$start = $length - max($length - strlen($match[$offset + 3]), 0);
return substr($match[$offset + 1], $start, $length) . $match[$offset + 4];
}
private function _replace_encoded($match, $offset) {
return $this->buffer[$match[$offset]];
}
// php : we cannot pass additional data to preg_replace_callback,
// and we cannot use &$this in create_function, so let's go to lower level
private $buffer;
// encode escaped characters
private function _escape($string, $escapeChar) {
if ($escapeChar) {
$this->buffer = $escapeChar;
return preg_replace_callback(
'/\\' . $escapeChar . '(.)' . '/',
array(&$this, '_escapeBis'),
$string
);
}
else {
return $string;
}
}
private function _escapeBis($match) {
$this->_escaped[] = $match[1];
return $this->buffer;
}
// decode escaped characters
private function _unescape($string, $escapeChar) {
if ($escapeChar) {
$regexp = '/' . '\\' . $escapeChar . '/';
$this->buffer = array(
'escapeChar' => $escapeChar,
'i' => 0,
);
return preg_replace_callback(
$regexp,
array(&$this, '_unescapeBis'),
$string
);
}
else {
return $string;
}
}
private function _unescapeBis() {
if (isset($this->_escaped[$this->buffer['i']])
&& $this->_escaped[$this->buffer['i']] != '') {
$temp = $this->_escaped[$this->buffer['i']];
}
else {
$temp = '';
}
$this->buffer['i']++;
return $this->buffer['escapeChar'] . $temp;
}
private function _internalEscape($string) {
return preg_replace($this->ESCAPE, '', $string);
}
}