django-compressor / django-compressor Goto Github PK
View Code? Open in Web Editor NEWCompresses linked and inline javascript or CSS into a single cached file.
Home Page: https://django-compressor.readthedocs.io
License: Other
Compresses linked and inline javascript or CSS into a single cached file.
Home Page: https://django-compressor.readthedocs.io
License: Other
Since 37cb543 (I haven't done a bisect but it's between 0.6.2 and 0.7.1 and I don't see any other changes in js filters) django_compressor's jsmin filter chokes on some very specific javascript code. Here is a testcase:
var a = 'bar';
switch(a) {
case 'foo':// foo //
break;
}
alert(a);
Here is the jsmin output:
var a='bar';switch(a){case'foo'://foobreak;}
alert(a);
Note the end of the comment, which was kept. While it's a rather ugly way of writing comments (and it might even be invalid, didn't check the spec) but it used to work before. Is it fixable ?
A fairly common technique in media is to use querystrings to issue new URLs (which are ignored in static file service) and then issue far-future cache headers. Compressor currently maps COMPRESS_URL to COMPRESS_ROOT literally, so that any querystring is expected to exist on the file system.
It would be good to have an option to ignore querystrings in media URLs.
Here's what I did in client code for now:
from compressor.conf import settings as compressor_settings
from compressor.exceptions import UncompressableFileError
from compressor.base import Compressor
from compressor.css import CssCompressor
from compressor.js import JsCompressor
class FooBaseCompressor(Compressor):
def get_filename(self, url):
try:
base_url = self.storage.base_url
except AttributeError:
base_url = settings.COMPRESS_URL
if not url.startswith(base_url):
raise UncompressableFileError(
"'%s' isn't accesible via COMPRESS_URL ('%s') and can't be"
" compressed" % (url, base_url))
basename = url.replace(base_url, "", 1)
# the one custom bit, alas:
basename = basename.split("?", 1)[0] # drop the querystring, which is used for non-compressed cache-busting.
filename = os.path.join(compressor_settings.COMPRESS_ROOT, basename)
if not os.path.exists(filename):
raise UncompressableFileError("'%s' does not exist" % filename)
return filename
class FooCssCompressor(FooBaseCompressor, CssCompressor):
pass
class FooJsCompressor(FooBaseCompressor, JsCompressor):
pass
The most recent version of django_compressor on PyPi seems to be 0.5.3 from February 2010.
Hi,
I've written a quick & dirty documentation about what {% compress %} does behind the scene, to help our admins and devs understand django_compressor cache strategy and potential performance problems. I think it could be a nice addition to the project, so here is the gist link if you want to copy it: https://gist.github.com/909599
Of course, if there is anything wrong in the doc, I'd like to know about it :)
Upgraded a project that previously used django_compressor 0.6b5 to 0.7 and my compressed media broke with a traceback on each request. Traceback:
Traceback (most recent call last): File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/core/servers/basehttp.py", line 280, in run self.result = application(self.environ, self.start_response) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 67, in __call__ return super(StaticFilesHandler, self).__call__(environ, start_response) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/core/handlers/wsgi.py", line 248, in __call__ response = self.get_response(request) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 57, in get_response return self.serve(request) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/handlers.py", line 50, in serve return serve(request, self.file_path(request.path), insecure=True) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/views.py", line 35, in serve absolute_path = finders.find(normalized_path) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 236, in find for finder in get_finders(): File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 253, in get_finders yield get_finder(finder_path) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/utils/functional.py", line 124, in wrapper result = func(*args) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/staticfiles/finders.py", line 262, in _get_finder mod = import_module(module) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/django/utils/importlib.py", line 35, in import_module __import__(name) File "/Users/luke/.virtualenvs/helpdesk/lib/python2.6/site-packages/compressor/finders.py", line 4, in class CompressorFinder(staticfiles.finders.BaseStorageFinder): AttributeError: 'NoneType' object has no attribute 'BaseStorageFinder'
Did some digging, removed the try at https://github.com/jezdez/django_compressor/blob/master/compressor/utils/staticfiles.py#L15 and got a template import error from CssCompressor.
Traced the error back to what appears to be namespace issue with the compressor.utils.staticfiles module stepping on staticfiles, which causes an import of staticfiles.conf to fail even when the same import works fine in the same python session. Traceback that shows that is below:
In [1]: from compressor.css import CssCompressor --------------------------------------------------------------------------- ImportError Traceback (most recent call last) /Users/luke/projects/project/ in () /Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/css.py in () ----> 2 from compressor.base import Compressor 3 from compressor.exceptions import UncompressableFileError 4 5 6 class CssCompressor(Compressor): /Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/base.py in () 10 from compressor.filters import CompilerFilter 11 from compressor.storage import default_storage ---> 12 from compressor.utils import get_class, staticfiles 13 from compressor.utils.cache import cached_property 14 /Users/luke/.virtualenvs/env/lib/python2.6/site-packages/compressor/utils/staticfiles.py in () 14 else: 15 from staticfiles import finders ---> 16 from staticfiles.conf import settings 17 18 if INSTALLED and "compressor.finders.CompressorFinder" \ ImportError: No module named conf In [2]: from staticfiles.conf import settings In [3]: settings Out[3]:
I'm having some problems getting django_compressor to work when running apache.
I'm using S3 as my remote storage so when I run 'python manage.py compress' and the expected js + css files appear in the correct place in my S3 bucket.
However when the page renders from the apache+mod_wsgi server the line linking to the compressed js/css does not appear at all (the whole block that i wrapped in {% compress %} is missing.
What's very odd is that when i run the django dev server (with the same exact settings) the expected compresssed js+css files do exist.
Any suggestions about where I should look to debug this? I'd love to use django_compressor in production
-Evan
Trying to compress a line like this one:
var html = "<//div>"
Raise this error:
Error while initializing HtmlParser: bad end tag: u'<//div>', at line 28, column 47
Seems to happen with any html closing tag.
This is likely an HtlmParser error and maybe should it be removed from the parsers, BeautifulSoup works fine.
Using this setting:
COMPRESS_PARSER = 'compressor.parser.AutoSelectParser'
fails silently, but using 'HtmlParser' shows the error.
NOTE: installing lxml is a pain in the ass :(
If I set COMPRESS_DEBUG_TOGGLE to 'whatever' and uses that in querystring, compress tag will return all the elements within as is. This is a problem for me because the lessc files too are returned unfiltered, which break the whole page.
I'd really like something to toggle the compression independent from the filters, something like FILTER_DEBUG_TOGGLE.
I can put together some patches to do that, if that's OK.
With the latest changes in the develop branch, filters are getting applied in DEBUG mode, when they shouldn't be.
It works fine before those changes were made.
An UncompressableFileError is raised when django_compressor is used in conjunction with the staticfiles app in development when static files have not been collected to STATIC_ROOT.
There have been a few requests for offline generation in the old repo's issue tracker (mintchaos#34 and mintchaos#56).
I've had the need for offline generation too and have built an implementation that works for my use case (and should work for most needs IMHO): https://github.com/ulope/django_compressor/tree/offline
There are a few limitations however:
I have the following settings in settings.py:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
STATIC_URL = MEDIA_URL + 'static/'
STATIC_ROOT = os.path.join(MEDIA_ROOT, 'static')
STATICFILES_DIRS = ('%s/static/' % BASE_DIR,)
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'django.contrib.staticfiles.finders.DefaultStorageFinder',
'compressor.finders.CompressorFinder',
)
It seems Django compressor is over time generating new directories under /media/static/static/static/... with the contents of the immediately preceding directory.
I am doing self.content = smart_unicode(content or "")
in the init methods of Compressor and FilterBase to resolve decoding errors. Is this a good idea?
CssAbsoluteFilter does not replace url(...) in CSS files when the CSS is not present in the compressed root but in some other directory.
I believe the problem is around
css_default.py file
if not filename or not filename.startswith(self.root):
return self.content
setup.py requires BeautifulSoup to be installed next to django_compressor even though the parser is easily switchable. There even is built in lxml support.
In an old version of compressor (0.6a3, I think), CompressorNode had this gross but useful hack:
request = context.get('request')
...
if request and request.REQUEST.get('DEBUG'):
return content
That is, if the request has a querystring DEBUG, return the uncompressed content. This was useful for debugging in prod, and it doesn't exist in latest (0.6b5).
I can see why that hack was removed (or not included in this line of development), but since it is a useful feature, perhaps we can introduce a setting:
CONTEXT_DEBUG_VARIABLE = None
# e.g. "request.GET.NO_COMPRESS"
CompressorNode could then resolve the variable and return uncompressed if it resolved to something truthy.
Thoughts?
In: compressor.filters.css_default on line 17 it checks if the filename starts with the root, except that in the development environment it doesn't so now the css files from my apps are combined and placed in the CACHE folder, but the relative url's in the css are all wrong.
The "link" self-close generated element is malformed:
<link rel="stylesheet" href="http://static.domain.tld/CACHE/css/64673ae00d55.css" type="text/css">
rather than:
<link rel="stylesheet" href="http://static.domain.tld/CACHE/css/64673ae00d55.css" type="text/css" />
COMPRESS = False
COMPRESS_OFFLINE = TRUE
./manage.py compress --force
Does not create the cache dir, but COMPRESS = True does
Will django_compressor support css preprocessors in near future (like http://lesscss.org/)?
Hi Jannis,
Currently compressor's COMPRESS_ROOT setting defaults to STATIC_ROOT. If the file is already present in STATIC_ROOT, running "collectstatic -l" will cause an infinite symbolic link loop. I setup compressor with pretty much default settings, visit the site which causes compressor to generate minified CSS and ran the following:
selwin@selwin:~/dev/spark$ ./manage.py collectstatic -l
You have requested to collect static files at the destination
location as specified in your settings file.
This will overwrite existing files.
Are you sure you want to do this?
Type 'yes' to continue, or 'no' to cancel: yes
<-- snip -->
Linking '/home/selwin/dev/spark/static/CACHE/css/bdec3fe0ab18.css'
413 static files symlinked to '/home/selwin/dev/spark/../spark/static/'.
nginx stopped responding and when I checked compressor generated file, apparently for compressor generated files, staticfiles created a symlink to itself:
selwin@selwin:~/dev/spark$ more static/CACHE/css/bdec3fe0ab18.css
static/CACHE/css/bdec3fe0ab18.css: Too many levels of symbolic links
selwin@selwin:~/dev/spark$ ls -la static/CACHE/css
total 8
drwxr-xr-x 2 selwin selwin 4096 2011-06-09 14:47 .
drwxr-xr-x 3 selwin selwin 4096 2011-06-09 14:47 ..
lrwxrwxrwx 1 selwin selwin 56 2011-06-09 14:47 bdec3fe0ab18.css -> /home/selwin/dev/spark/static/CACHE/css/bdec3fe0ab18.css
Setting COMPRESS_ROOT to another dir solves this, but that means having one extra directory in my project dir.
I thought about modifying CompressorFinder but couldn't find enough documentation to fully understand what it does. If you could point me in the right direction though, I'd be more than happy to help.
The jsmin filter fails to produce the correct output on a complex script (included below), a line break is inserted in the wrong place. The same javascript is compressed correctly with the older jsmin filter included in the django-compressor 0.5.3 (mintchaos?).
The line of code causing the issue is the '/.../' comment at the end of the script after the var assignment. If I remove the comment the compressor generates a working compressed javascript.
$ LC_ALL=en_US diff -c widget-working.js widget-broken.js
*** widget-working.js 2011-05-03 11:09:41.553279754 +0200
--- widget-broken.js 2011-05-03 10:25:59.120443254 +0200
***************
*** 46,50 ****
var addToolbarHtmlClickEventListener=function(){jQuery(document.html).click(function(event){jQuery('#kth-toolbar ul li ul').hide();event.stopPropagation();});}
var mouseLeaveTimer;var isApiDataLoaded;function renderKthToolbar(){renderInitialToolbar();initializeToolbar("#kth-toolbar","#showkth-toolbar");addToolbarDeactivateClickEventListener("#hidekth-toolbar","#kth-toolbar","#showkth-toolbar");addToolbarActivateClickEventListener("#kth-toolbar","#showkth-toolbar");addToolbarHtmlClickEventListener();jQuery(".kth-toolbar-menu").live('mouseleave',function(){mouseLeaveTimer=window.setTimeout(function(){hideAllMenus();mouseLeaveTimer=null;},500);});jQuery(".kth-toolbar-menu").live('mouseenter',function(){if(mouseLeaveTimer){window.clearTimeout(mouseLeaveTimer);}});isApiDataLoaded=false;window.setTimeout(function(){if(!isApiDataLoaded){renderErrorToolbar();}},10000);jQuery.ajax({url:"http://fjo.ite.kth.se:8081/toolbar/api/1.0/"+KthToolbarConfig.language,dataType:"jsonp",jsonpCallback:"KthToolbarJSONPLoader",success:function(toolbarapi_data){isApiDataLoaded=true;if(toolbarapi_data.status=="OK"){renderPersonalizedToolbar(toolbarapi_data);}else if(toolbarapi_data.status=="anonymous"){renderAnonymousToolbar(toolbarapi_data);}else{renderErrorToolbar();}}
});}
! jQuery(document).ready(function(){renderKthToolbar();});}
! KthToolbar();
\ No newline at end of file
--- 46,49 ----
var addToolbarHtmlClickEventListener=function(){jQuery(document.html).click(function(event){jQuery('#kth-toolbar ul li ul').hide();event.stopPropagation();});}
var mouseLeaveTimer;var isApiDataLoaded;function renderKthToolbar(){renderInitialToolbar();initializeToolbar("#kth-toolbar","#showkth-toolbar");addToolbarDeactivateClickEventListener("#hidekth-toolbar","#kth-toolbar","#showkth-toolbar");addToolbarActivateClickEventListener("#kth-toolbar","#showkth-toolbar");addToolbarHtmlClickEventListener();jQuery(".kth-toolbar-menu").live('mouseleave',function(){mouseLeaveTimer=window.setTimeout(function(){hideAllMenus();mouseLeaveTimer=null;},500);});jQuery(".kth-toolbar-menu").live('mouseenter',function(){if(mouseLeaveTimer){window.clearTimeout(mouseLeaveTimer);}});isApiDataLoaded=false;window.setTimeout(function(){if(!isApiDataLoaded){renderErrorToolbar();}},10000);jQuery.ajax({url:"http://fjo.ite.kth.se:8081/toolbar/api/1.0/"+KthToolbarConfig.language,dataType:"jsonp",jsonpCallback:"KthToolbarJSONPLoader",success:function(toolbarapi_data){isApiDataLoaded=true;if(toolbarapi_data.status=="OK"){renderPersonalizedToolbar(toolbarapi_data);}else if(toolbarapi_data.status=="anonymous"){renderAnonymousToolbar(toolbarapi_data);}else{renderErrorToolbar();}}
});}
! jQuery(document).ready(function(){renderKthToolbar();});}KthToolbar();
\ No newline at end of file
if (!KthToolbarConfig) {
var KthToolbarConfig = {};
}
if (!KthToolbarConfig.frontUrl) {
KthToolbarConfig.frontUrl = "http://fjo.ite.kth.se:8081";
}
if (!KthToolbarConfig.loginUrl) {
KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href);
} else {
if (KthToolbarConfig.loginUrl.charAt(0) == '?') {
KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href + KthToolbarConfig.loginUrl);
} else {
KthToolbarConfig.loginUrl = "http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(KthToolbarConfig.loginUrl);
}
}
if (!KthToolbarConfig.loginMethod) {
KthToolbarConfig.loginMethod = 'POST';
}
if (!KthToolbarConfig.locale) {
KthToolbarConfig.locale = "sv_SE";
}
KthToolbarConfig.language = KthToolbarConfig.locale.split('_')[0];
if (KthToolbar && console && console.warn) {
console.warn("KthToolbar already defined on page!");
}
var KthToolbar = function() {
jQuery("head").append("<link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/toolbar-screen.css'>" +
"<link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/reset-context-min.css'>" +
"<!--[if IE 7]><link rel='stylesheet' type='text/css' href='" + KthToolbarConfig.frontUrl + "/static/toolbar/ie7-toolbar-screen.css'><![endif]-->");
var lang = {
"Startsida för www.kth.se": { en: "Start page for www.kth.se" },
"Dölj": { en: "Hide" },
"Dölj toolbaren": { en: "Hide toolbar" },
"Mina sidor": { en: "My pages" },
"Visa": { en: "Show" },
"Logga in": { en: "Log in" },
"Laddar personlig information": { en: "Loading personalized information" },
"Kunde inte ladda personlig information (klicka för att försöka igen)": { en: "Failed to load customized information (click to retry)" }
};
function translate(text) {
function trans(sv) {
var translations = lang[sv];
if (!translations) {
return sv;
}
var translation = translations[KthToolbarConfig.language];
if (!translation) {
return sv;
}
return translation;
}
return text.replace(/#\{(.*?)\}/g, function(str, p1) {
return trans(p1);
});
}
/**
* Create the initial toolbar that is displayed until more information i known
*/
function renderInitialToolbar() {
var html = "<div class='yui3-cssreset'><div id='kth-toolbar' class='yui3-cssreset'><ul class='kth-toolbar-items'><li class='kth-toolbar-left kth-toolbar-home'><a href='http://www.kth.se' title='#{Startsida för www.kth.se}'><img src='" + KthToolbarConfig.frontUrl + "/static/toolbar/kth-logo-24-24.png' width='24' height='24'></a></li><li id='kth-toolbar-profile' class='kth-toolbar-left'><span style='color: #cccccc'>#{Laddar personlig information}...</span></li><li id='kth-toolbar-linksets-insertion-point' style='display: none'></li><li id='hidekth-toolbar' title='#{Dölj toolbaren}'>#{Dölj}</li><li id='kth-toolbar-webmail' class='kth-toolbar-right kth-toolbar-button'><a href='https://webmail.kth.se'>#{Webmail}</a></li><li id='kth-toolbar-my-pages' class='kth-toolbar-right kth-toolbar-button'><a id='kth-toolbar-mypages-link'>#{Mina sidor}</a></li></ul></div><div id='showkth-toolbar'>#{Visa}</div></div>";
jQuery('body').append(translate(html));
jQuery('#kth-toolbar-mypages-link').attr('href', 'https://www.kth.se/student/minasidor/?l=' + KthToolbarConfig.locale);
}
/**
* adjust toolbar for (async deduced) error situation so personal/loggedin cannot be known
*/
function renderErrorToolbar() {
jQuery('#kth-toolbar #kth-toolbar-profile').html(translate("<a href='http://fjo.ite.kth.se:8081/accounts/login/?next=" + escape(location.href) + "'>#{Kunde inte ladda personlig information (klicka för att försöka igen)}</a>"));
}
/**
* adjust toolbar for (async deduced) not logged in
*/
function renderAnonymousToolbar(toolbarapi_data) {
jQuery('#kth-toolbar #kth-toolbar-profile').html(translate("<form action='" + KthToolbarConfig.loginUrl + "' method='" + KthToolbarConfig.loginMethod + "'><input type='submit' name='kth-toolbar-login-button' value='#{Logga in}' class='kth-toolbar-login' /></form>"));
}
/**
* adjust toolbar for (async fetched) personal information
*/
function renderPersonalizedToolbar(toolbarapi_data) {
/* NOTE! name and avatar_html is leaked into the global name space on purpose. */
name = '';
avatar_html = '';
if (toolbarapi_data.avatar) {
avatar_html = '<span class="kth-toolbar-small-profile-picture"><img src="' + toolbarapi_data.avatar.url + '" alt="' + toolbarapi_data.avatar.alt + '" width="' + toolbarapi_data.avatar.width + '" height="' + toolbarapi_data.avatar.height + '"></span>';
}
name = toolbarapi_data.name.firstname + ' ' + toolbarapi_data.name.lastname;
jQuery('#kth-toolbar #kth-toolbar-profile').html('<a href="' + toolbarapi_data.homeurl + '" class="kth-toolbar-profile-link">'
+ avatar_html +
'<span class="kth-toolbar-label">' + name + '</span></a>');
jQuery("#kth-toolbar-insert-username").html(name);
jQuery.each(toolbarapi_data.linksets, function(index, linkset) {
if (linkset.links.length > 0) {
var html = "<li id='" + index + "' class='kth-toolbar-left kth-toolbar-menu " + linkset.type + "'><ul id='kth-toolbar-toolbar-menu" + index + "' class='kth-toolbar-menuitems'>";
jQuery.each(linkset.links, function(index, link) {
html += "<li class='kth-toolbar-item'>";
html += "<a href='" + link.url + "' class='kth-toolbar-label'>" + link.name + "</a>";
if (link.extra) {
html += "<a class='kth-toolbar-extra' href='" + link.url + "'>" + link.extra + "</a>";
}
html += "</li>";
});
html += "</ul><span class='kth-toolbar-button title'>" + linkset.name + "</span></li>";
jQuery("#kth-toolbar-linksets-insertion-point").before(html);
addMenuActivatorClickEventListener("#kth-toolbar-toolbar-menu" + index, "#" + index);
}
});
}
/**
* Toolbar functions
*/
function slideElementVertically(element, from, to, animationSpeed) {
jQuery(element).css({height: from}).animate({ height: to }, animationSpeed);
}
/**
* Shows the toolbar by sliding it up.
*
* @param toolbarId the actual toolbar element.
*/
function showToolbar(toolbarId, animationSpeed) {
slideElementVertically(toolbarId, 0, 34, animationSpeed);
}
/**
* Hides the toolbar by sliding it down.
*
* @param toolbarId the actual toolbar element.
*/
function hideToolbar(toolbarId) {
slideElementVertically(toolbarId, 34, 0, 'slow');
}
/**
* Shows the "Show toolbar" tab by sliding it up.
*
* @param tabId the element for the "Show toolbar".
*/
function showTab(tabId) {
slideElementVertically(tabId, -5, 24, 'slow');
}
/**
* Hides the "Show toolbar" tab by sliding it down.
*
* @param tabId the element for the "Show toolbar".
*/
function hideTab(tabId) {
slideElementVertically(tabId, 24, -5, 'slow');
}
/**
* Checks if the toolbar is activated (visible).
*
* @param toolbarId the actual toolbar element.
* @return true if the toolbar is activated, else false.
*/
function isActivated(toolbarId) {
if(jQuery(toolbarId).height() > 0) {
return true;
} else {
return false;
}
}
/**
* Hides all the menus in the toolbar.
*/
function hideAllMenus() {
jQuery('#kth-toolbar ul li ul').hide();
jQuery('#kth-toolbar .kth-toolbar-items .kth-toolbar-left.kth-toolbar-menu.kth-toolbar-active').removeClass('kth-toolbar-active');
}
/**
* Gets the toolbar visibility state value from the kthToolbar cookie.
*
* @param name the name of the cookie to retrieve the toolbar state from.
* @return the visibility state of the toolbar (true/false) .
*/
function getToolbarCookie(name) {
if (document.cookie.length > 0) {
start = document.cookie.indexOf(name + "=");
if (start != -1) {
start = start + name.length+1;
end = document.cookie.indexOf(";",start);
if (end == -1) {
end = document.cookie.length;
}
return unescape(document.cookie.substring(start,end));
}
}
return "";
}
/**
* Sets a kthToolbar cookie and prepare it with the toolbar state
* and expire time.
*
* @param name the name of the cookie
* @param value the value/state (true or false)
* @param expiredays number of days until the cookie should expire.
*/
function setToolbarCookies(name, value, expiredays) {
var exdate=new Date();
exdate.setDate(exdate.getDate() + expiredays);
var domain="kth.se";
var path="/";
document.cookie = name + "=" +escape( value ) +
( ( expiredays ) ? ";expires=" + exdate.toUTCString() : "" ) +
( ( path ) ? ";path=" + path : "" ) +
( ( domain ) ? ";domain=" + domain : "" );
}
/**
* Activates the toolbar and deactivates the "Show toolbar" tab.
*
* @param toolbarId the actual toolbar element.
* @param tabId the element for the "Show toolbar".
*/
function activateToolbar(toolbarId, tabId) {
hideTab(tabId);
showToolbar(toolbarId, 600);
}
/**
* Deactivates the toolbar, hide all menus, and activates
* the "Show toolbar" tab.
*
* @param toolbarId the actual toolbar element.
* @param tabId the element for the "Show toolbar".
*/
function deActivateToolbar(toolbarId, tabId) {
hideAllMenus();
hideToolbar(toolbarId);
showTab(tabId);
}
/**
* Calculates the menu position based on the number of
* elements in the menu and the height of the toolbar.
*
* The browser specific code sucks, but is the best I
* can figure out thus far. /fjo 20110103
*
* @param menuId element
* @return
*/
function calculateMenuPosition(menuId) {
var maxValue = 404;
var items = jQuery(menuId).children().size();
var itemHeight = 25;
var adjustment = 4;
if (jQuery.browser.msie) {
var pos = (items * itemHeight) + adjustment;
} else {
var pos = jQuery(menuId).height() + adjustment;
}
if (pos < maxValue) {
return pos;
} else {
return maxValue;
}
}
/**
* Initializes the toolbar by checking the cookie for the toolar state.
* If no cookie is set, show the toolar and hide the "Show toolbar" tab.
*
* @param toolbarId the actual toolbar element.
* @param tabId the element for the "Show toolbar".
*/
function initializeToolbar(toolbarId, tabId) {
var isVisible = getToolbarCookie("kthToolbar");
if (isVisible=="") {
jQuery(tabId).toggle();
showToolbar(toolbarId, 0);
} else if (isVisible == "true") {
jQuery(toolbarId).css({height: 34});
jQuery(tabId).toggle();
} else if (isVisible == "false") {
jQuery(tabId).css({height: 20});
}
}
/**
* Deactivates the toolbar and shows the "Show toolbar" tab.
* This function also sets a cookie to remember the toolbar state.
*
* @param activatorId the activator element for the toolbar.
* @param toolbarId the actual toolbar element.
* @param tabId the element for the "Show toolbar".
*/
function addToolbarDeactivateClickEventListener(activatorId, toolbarId, tabId) {
jQuery(activatorId).click(function (event) {
if(isActivated(toolbarId)) {
deActivateToolbar(toolbarId, tabId);
setToolbarCookies("kthToolbar", "false", 7);
}
event.stopPropagation();
});
}
/**
* Activates the toolbar and hides the "Show toolbar" tab.
* This function also sets a cookie to remember the toolbar state.
*
* @param toolbarId the actual toolbar element.
* @param tabId the element for the "Show toolbar".
*/
function addToolbarActivateClickEventListener(toolbarId, tabId) {
jQuery(tabId).click(function (event) {
activateToolbar(toolbarId, tabId);
setToolbarCookies("kthToolbar", "true", 7);
event.stopPropagation();
});
}
/**
* Check if the given menu is showing.
*
* @param menuId the element containing the menu.
* @return true if the menu is showing, else false.
*/
function showingMenu(menuId) {
var display = jQuery(menuId).css('display');
if (display != 'none') {
return true;
} else {
return false;
}
}
/**
* Toggle the given menu element and calculates
* the position of the expanded menu.
*
* @param menuId the element of the active menu.
*/
function toggleMenu(menuId) {
var pos = calculateMenuPosition(menuId);
jQuery(menuId).css('top', -pos);
jQuery(menuId).toggle();
}
/**
* Toggle the menus depending on their states.
*
* @param menuId the element that contains the menu.
* @param event the click event on the menu activator element.
*/
function toggleMenus(menuId, event, activator) {
var toggle = true;
if(showingMenu(menuId)) {
toggle = false;
}
hideAllMenus();
if(toggle) {
toggleMenu(menuId);
jQuery(activator).addClass('kth-toolbar-active');
}
event.stopPropagation();
}
/**
* Adds a click event listener for the given menu button on the toolbar.
*/
function addMenuActivatorClickEventListener(menuId, activator) {
jQuery(activator).click(function (event) {
toggleMenus(menuId, event, activator);
});
}
/**
* Close the menus if the HTML element is clicked.
*/
var addToolbarHtmlClickEventListener = function() {
jQuery(document.html).click(function (event) {
jQuery('#kth-toolbar ul li ul').hide();
event.stopPropagation();
});
}
var mouseLeaveTimer;
var isApiDataLoaded;
/**
* Main bootstrapt function that creates the toolbar and fills it,
* intented to be called once the page is loaded
*/
function renderKthToolbar() {
renderInitialToolbar();
initializeToolbar("#kth-toolbar", "#showkth-toolbar");
addToolbarDeactivateClickEventListener("#hidekth-toolbar", "#kth-toolbar", "#showkth-toolbar");
addToolbarActivateClickEventListener("#kth-toolbar", "#showkth-toolbar");
addToolbarHtmlClickEventListener();
jQuery(".kth-toolbar-menu").live('mouseleave', function() {
mouseLeaveTimer = window.setTimeout(function() {
hideAllMenus();
mouseLeaveTimer = null;
}, 500);
});
jQuery(".kth-toolbar-menu").live('mouseenter', function() {
if (mouseLeaveTimer) {
window.clearTimeout(mouseLeaveTimer);
}
});
isApiDataLoaded = false;
window.setTimeout(function() {
if (!isApiDataLoaded) {
renderErrorToolbar();
}
}, 10000);
jQuery.ajax({
url: "http://fjo.ite.kth.se:8081/toolbar/api/1.0/" + KthToolbarConfig.language,
dataType: "jsonp",
jsonpCallback: "KthToolbarJSONPLoader",
success: function(toolbarapi_data) {
isApiDataLoaded = true;
if (toolbarapi_data.status == "OK") {
renderPersonalizedToolbar(toolbarapi_data);
} else if (toolbarapi_data.status == "anonymous") {
renderAnonymousToolbar(toolbarapi_data);
} else {
renderErrorToolbar();
}
}
/*timeout, error and complete do not work when we use jsonp in 1.4 */
});
}
/**
* Activate when document is loaded.
*/
jQuery(document).ready(function () {
renderKthToolbar();
});
}; /*end KthToolbar*/
KthToolbar();
I've actually got this working with very minor customisations to django_compressor. My approach is to use a custom context processor to set MEDIA_URL
(which I use everywhere for loading JS/CSS) to an appropriate subdomain e.g. http://media.
(or potentially http://media{0,1,2,...}
). This forces django_compressor to create a new cache key for this content.
Next was to ensure django_compressor passed the current context through via the template tag. This was essential to perform custom manipulation of django_compressor's URL e.g. for handling SSL/non-SSL, for which I needed the current request
.
I also had to modify get_filename
to be slightly more liberal and strip the protocol+hostname before checking if a URL is compressible.
It would be nice to have a more "core" way to do this, rather than relying on the request
context variable being available, for example. I may submit a pull request if I get time, otherwise feel free to implement!
Problem: In DEBUG mode, script(s) is/are continually recompiled by the PRECOMPILER on each page load, whereas in production mode, scripts are only precompiled (then filtered) if the script actually changed any (or at end of default 30 day period).
I propose to ONLY recompile scripts in DEBUG mode when the script content changes, and it needs to be recompiled, just like in production. This way there'll be no recompiling of the script when it's not needed, but it will be when it's needed.
the file htmlparser.py has this import statement:
from HTMLParser import HTMLParser
since the file itself is called htmlparser and there is a class HTMLParser in it, this import will not work on a case-insensitive file-system
fix:
change:
from HTMLParser import HTMLParser
to:
from ..HTMLParser import HTMLParser
Too much information is lost from TextNode objects because of smart_str being used for hashing:
def __repr__(self):
return "<Text Node: '%s'>" % smart_str(self.s[:25], 'ascii',
errors='replace')
Here's a proposed new implementation of get_offline_cachekey:
def get_offline_cachekey(source):
to_hexdigest = [smart_str(getattr(s, 's', s)) for s in source]
return get_cachekey("offline.%s" % get_hexdigest("".join(to_hexdigest)))
I'm testing the new cool COMPASS filter, and it's great.
The problem comes when I disable the compression for debugging purposes. The scss files are not compiled.
I've been playing with COMPRESS_PRECOMPILERS, but I don't understand when it should be used.
I have:
COMPRESS_PRECOMPILERS = (
('text/x-sass', 'sass {infile} {outfile}'),
('text/x-scss', 'sass --scss {infile} {outfile}'),
)
I supposed that the precompilers will check if any of the files matching the mimetypes, and compile them, but leaving all the other files untouched. But I was wrong.
Well, what i want to do is to show the css and js the same as when COMPRESS = False, but compiling the scss files.
What is the best approach to solve this problem?
Do you think is a good idea to add a new setting like: COMPILE_ALWAYS similar to COMPRESS_PRECOMPILERS?
An example of the desired behaviour with COMPRESS = FALSE
Template:
{% compress js %}
<script type="text"javascript" src="{{ STATIC_URL }}/js/file1.js" ></script>
{% endcompress %}
{% compress css %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/stylesheets.css" />
<!-- Note that this is a SCSS file to be processed by COMPASS FILTER -->
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/other_styles.scss" />
{% endcompress %}
Result wanted:
<script type="text"javascript" src="/static/js/file1.js" ></script>
<link rel="stylesheet" type="text/css" href="/static/css/stylesheets.css" />
<link rel="stylesheet" type="text/css" href="/static/CACHE/3b1f6363b80d.css" />
Regards,
Adrian
In addition to creating a gzipped version of the compressed files, GzipCompressorFileStorage
appends a .gz at the end of URLs.
This behavior is not documented, and I don't understand it.
I want the gzipped version so that the webserver doesn't have to re-compress the file every time it's requested by a client that supports gzip, but I still want the original URLs in the HTML.
Did I miss something? Or is it a bug?
I have a project with 100s of domains and related settings files. I'd like to generate compressed files for each of them (offline) upon deploy. It'd be nice if there were a way to map the required settings for use as a single management command to avoid the startup time of django-admin for each settings file. (It's a large project, startup is about 4 seconds on a very capable machine.)
In the documentation for enabling compress, you have listed that the setting to enable is:
COMPRESS = True
that didn't work for me. I had to use:
COMPRESS_ENABLED = True
I think this is just a typo in your docs; I thought I'd let you know.
The documentation gives an example where the save method is overridden of a storage class. Depending on your storage and debug settings, you may need to override more methods to make the compress management command work. Here is the relevant code.
Is there a reason why self.finders
should not be used even when DEBUG is false?
It's writtent that in case of staticfiles usage I should add CompressorFinder to the finders setting, and it should do the trick
However compressor
does only see the files collected with collectstatic
, thus not noticing any new or changed files in app/static
directory. In comparison, staticfiles
is quite good in working with these files when runserver command is used.
Am I missing something?
I have everything setup properly and working wonderfully, however linking to files isn't working.
it works fine like this
{% compress js %}
<script type="text/coffeescript">
# Functions:
square = (x) -> x * x
console.log square
</script>
{% endcompress %}
My template code (which doesn't work since it's using a url)
{% load compress %}
<html>
<head>
<title>{% block title %}{% endblock %}</title>
{% compress js %}
<script type="text/coffeescript" charset="utf-8" src="/static/tests.coffee"></script>
{% endcompress %}
</head>
<body>{% block content %}{% endblock %}</body>
</html>
tests.coffee
# Functions:
square = (x) -> x * x
console.log square
resulting code (was run in yuicompressor afterward). If not using coffeescript, there simply would be no files saved to cache, or javascript link to the cached javascript in the html; coffeescript is only an exception because it outputs the wrapper even if nothing is entered.
(function(){}).call(this);
my app settings
# add node.js to path
os.environ['PATH'] += ':%s' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors')
COMPRESS_PRECOMPILERS = (
('text/coffeescript', '%s --compile --stdio' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/coffeescript/bin/coffee')),
('text/less', '%s {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/less/bin/lessc')),
('text/x-sass', '%s {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/sass/bin/sass')),
('text/x-scss', '%s --scss {infile} {outfile}' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/sass/bin/sass')),
('text/clevercss', 'python %s' % os.path.join(PROJECT_ROOT, 'sys/compressor/compressors/clevercss.py'))
)
# caching backend to use
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
COMPRESS = True
COMPRESS_OUTPUT_DIR = 'cache'
COMPRESS_CSSTIDY_BINARY = os.path.join(PROJECT_ROOT, 'sys/compressor/filters/csstidy')
COMPRESS_YUI_BINARY = 'java -jar %s' % os.path.join(PROJECT_ROOT, 'sys/compressor/filters/yuicompressor.jar')
COMPRESS_CSS_FILTERS = ['apps.compressor.filters.csstidy.CSSTidyFilter', 'apps.compressor.filters.yui.YUICSSFilter']
COMPRESS_JS_FILTERS = ['apps.compressor.filters.yui.YUIJSFilter']
Using:
django 1.3
node 0.4.7
We're running a web app on a small virtual machine cluster behind a proxy. Each web app shares the compress output directory and we'd like for all apps to share the same compressed CSS and JS files. Because get_cachekey seems to use the server hostname when generating the cache key, we're ending up with a compressed resource for each app instance.
def get_cachekey(key):
return ("django_compressor.%s.%s" % (socket.gethostname(), key))
Can you suggest a patch that would allow our individual app instances to all share the same compressed resources? Possibly adding a settings flag to toggle the use of the hostname in the cache key, if that is indeed the only issue.
I'll do some hacking on this myself and see if I can come up with an elegant solution, but was wondering if you had any bright ideas.
Thanks!
When MEDIA_ROOT is pointing to a non existing dir (or maybe a directory that cannot be created because of wrong permissions) ), compressor fails silently. It writes the entry in the cache but it does not create a file (for obvious reasons) and does not output the link in the template
This happened with python 2.5 on a slackware install with django 1.3 , apache2 and mod_wsgi
I will try to create a patch for this.
When using django_compressor with DEBUG set to True and COMPRESS also set to True it throws a Caught TypeError while rendering: decoding Unicode is not supported error if it has url() in the stylesheet to replace. If you have CssAbsoluteFilter in your filters list the content passed to the input method is of str type. Then running return URL_PATTERN.sub(self.url_converter, self.content) the content then gets return as the unicode type.
In the Compressor runs over the hunks method it returns yield unicode(content, charset). When the content is of unicode type and the charset is set to utf-8 the unicode function raises the TypeError
As part of diagnosing a performance issue with our site, I discovered that a large portion of our page execution (74% in fact) was spent in django_compressor (jsmin.py, to be precise).
The core reason is that to determine if we already have a cached copy of the combined files, we have to run compressor.hash(), which in turn runs all the filters (which are slow). In the case of sites that have sizeable javascript, this becomes the dominant portion of page execution.
I have implemented a (crude) patch in a fork here: https://github.com/fennb/django_compressor that solves the problem by hashing on pre-filtered content, rather than post-filtered.
As a general approach, this seems to work, but I haven't done anything like see if this passes unit tests/etc. I ran into problems with compressor.concat() being a generator so secondary calls caused it to return '' (a separate bug?) so I used cached_property to memoize the output.
The results are fairly dramatic:
https://img.skitch.com/20110331-tg8wfpdk899s7ep1xghx1rwmy4.jpg
Also see profiling output here:
http://dl.dropbox.com/u/269071/temp/compressor_profile_orig.txt
http://dl.dropbox.com/u/269071/temp/compressor_profile_modified.txt
Interested in your thoughts. Thanks :)
option
defined in class is set to {}
here.
As a result I am getting KeyError
when I try to run YUI filters.
Caught KeyError while rendering: 'binary'
Hi,
I setup a server today (Django 1.2.4), and i'm using both django_staicfiles (v1.0b2) and django_compressor (dev). I'm also using django_storages (dev) with S3 backend storage for MEDIA only. All STATIC files are served through apache, while MEDIA files are served with Amazon S3.
When collecting STATIC files the following error is raised :
$ python manage.py collectstatic --noinput
Traceback (most recent call last):
File "manage.py", line 11, in
execute_manager(settings)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/init.py", line 438, in execute_manager
utility.execute()
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/init.py", line 379, in execute
self.fetch_command(subcommand).run_from_argv(self.argv)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 191, in run_from_argv
self.execute(_args, *_options.dict)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 220, in execute
output = self.handle(_args, _options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/django/core/management/base.py", line 351, in handle
return self.handle_noargs(_options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 89, in handle_noargs
self.copy_file(path, prefixed_path, storage, *_options)
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 184, in copy_file
if not self.delete_file(path, prefixed_path, source_storage, **options):
File "/home/ubuntu/.virtualenvs/ENV/lib/python2.6/site-packages/staticfiles/management/commands/collectstatic.py", line 125, in delete_file
source_last_modified = source_storage.modified_time(path)
AttributeError: 'CompressorFileStorage' object has no attribute 'modified_time'
What am i doing wrong ? Or is there a bug somewhere ?
Cheers,
Nice to see that you've taken over the project! :) Let me move a feature request from the old repo here:
Data URI:s are great, but IE6 and IE7 does not support them. Good thing is, they can be emulated with MHTML, and it actually isn't that tricky:
http://www.phpied.com/the-proper-mhtml-syntax/
This would be an excellent addition to django_compressor, don't you think?
Hello,
I'm not very good at this, please bear with me.
I have a dev and prod system both with Python 2.7, Django 1.3 and compressor 0.7.1.
On the local system, if I do
os.environ['DJANGO_SETTINGS_MODULE'] = "settings"
from compressor.conf import settings
and then check the attributes of settings, almost everything imaginable is there.
But on the prod system, if I do the same, setting's attribute just shows
settings.ABSOLUTE_CSS_URLS
settings.COMPRESS_JS_FILTERS
settings.OUTPUT_DIR
settings.COMPILER_FORMATS
settings.COMPRESS
settings.MEDIA_ROOT
settings.COMPRESS_CSS_FILTERS
settings.MEDIA_URL
If I do collectstatic or visit a page, I'll get no COMPRESS_ROOT or COMPRESS_CACHE_BACKEND attribute.
Any thoughts on what I might have done wrong?
Thanks,
Xiao
If you want to allow the less.js to parse the LESS css files on the client you need to set the rel attribute to stylesheet/less. Doing this makes django-compressor skip the files as it looks for rel="stylesheet". It would be great to have a CSS_COMPRESSOR that we could configure that would assume the files are LESS and compress them and out put one link element with the rel set to stylesheet/less so that less.js will parse the compressed file.
Hi! Is there anyway to collect and concatenate the CSS or JS in a particular block and then pass it through a filter chain? In particular, I'd like to use pyScss to process my SCSS. I've created a pySCSSFilter
extending FilterBase
, and I'd really like to do something like:
{% compress css %}
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/mixins.scss" />
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/base.scss" />
<link rel="stylesheet" type="text/css" media="screen" href="{{ MEDIA_URL }}css/other.scss" />
{% endcompress %}
but of course doesn't won't work as my files are processed in isolation from one another.
Is there a way?
Cheers!
COMPRESS is really COMPRESS_ENABLED in the code.
STATIC_URL = "http://cdn.example.com"
COMPRESS_URL = STATIC_URL
AWS_STORAGE_BUCKET_NAME = "static-example"
In this case the compressed files use a base URL which is based off of storage.url() rather than using COMPRESS_URL. In this case the URL generated for the compressed files starts with http://static-example.s3.aws.amazon.com rather than http://cdn.example.com
I think this line
url = self.storage.url(new_filepath)
needs to be changed
in base.py
class Compressor(object):
....
def output_file(self, mode, content, forced=False):
"""
The output method that saves the content to a file and renders
the appropriate template with the file's URL.
"""
new_filepath = self.filepath(content)
if not self.storage.exists(new_filepath) or forced:
self.storage.save(new_filepath, ContentFile(content))
url = self.storage.url(new_filepath)
return self.render_output(mode, {"url": url})
This causes an error:
{% compress %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/styles.scss" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}js/jquery/plugin/css/plugin.css" />
{% endcompress %}
Because it tries to filter plugin.css throug COMPASS filter.
There's a feature regression in commit 15ae5ae (wich fixed bug #18), after replacing
new_filepath = self.filepath(content)
with new_filepath = self.filepath(self.content)
filepath of output file no longer depends on contents of processed files. It's beacause self.content
contains html and content
was css(js) minified source.
Since I might want to have COMPRESS = True on my development and after my js-files are changed (but not their names), compressor won't create compressed js files again, because there is already such file from previous run.
The docs have a note about relative url() values in CSS being converted to absolute URLs while being processed, but this isn't happening in my case. Instead, the URLs are being left alone and the linked-to images aren't being found.
The settings that I've customized are:
COMPRESS_ROOT = os.path.join(PROJECT_ROOT, "..", "site_media", "static")
COMPRESS_URL = STATIC_URL
And I'm using LocMemCache as the cache backend, django version 1.2.4, and django-compressor version 0.5.3.
http://django_compressor.readthedocs.org/en/latest/index.html#css-notes
Let me know if any other info would be helpful.
I´m not entirely sure but I supposed that the defaults for COMPRESS_URL and COMPRESS_ROOT are STATIC_URL and STATIC_ROOT (instead of MEDIA_URL and MEDIA_ROOT).
any reasons for this?
thanks,
patrick
When loading template tag "compress", i get an error that indicates compressor's settings object is not being filled by the initial configure.
These are the only settings which are loaded by compressor.
['ABSOLUTE_CSS_URLS', 'COMPILER_FORMATS', 'COMPRESS', 'COMPRESS_CSS_FILTERS', 'COMPRESS_JS_FILTERS', 'ImproperlyConfigured', 'MEDIA_ROOT', 'MEDIA_URL', 'OUTPUT_DIR', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'settings']
I have no idea why it would load these but choke loading the other options.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.