27 November 2007

Ellipsis overflow w Firefox

W firmie pojawił się ostatnio problem przycinania tekstu. Wykonywanie go w trakcie generowania kodu HTML jest mało przyjazne, a co gorsza mało elastyczne, bo tekst lolita nie ma tej samej szerokości co wymówi przy tej samej liczbie znaków. Jak to polepszyć? CSS 3 zawiera text-overflow:ellipsis;, które automatycznie dodaje wielokropek (...), gdy tekst jest za długi. Problem w tym, że tę wersję obsługuje tylko IE6+, Safari i prawdopodobnie Konqueror (nie testowałem). Opera 9 ratuje nas własnym rozwiązaniem -o-text-overflow: ellipsis;. Ale co z Firefox?

Okazuje się, że Firefox 2 ani Firefox 3b1 nie obsługują wielokropka w CSS. Ktoś genialny [Firefox Ellipsis Hack](http://www.hedgerwow.com/360/dhtml/text_overflow/demo3.php] przypomniał sobie, że przecież Firefox obsługuje wielokropek w swoim interfejsie (choćby w about:config) i spróbował to wykorzystać. Niestety, rozwiązanie to wymaga odrobiny kodu JavaScript. Co więcej, rozwiązanie autora wymaga użycia Yui. Prezentowane poniżej rozwiazanie jest zlepkiem trzech rozwiązań - wspomnianej wcześniej sztuczki, funkcji Getelementsbyclassname z The Ultimate Getelementsbyclassname oraz trzech linijek kodu zaczerpniętych z biblioteki jQuery. Razem powstaje kod, który zajmuje 30 linii i nie wymaga zewnętrznych bibliotek. Do dzieła...

Kod HTML i CSS

<div id="e">
<span class="ellipsis">To jest bardzo długi tekst do przycięcia.
</div></span>

Ponieważ wszystko działa tylko wtedy, gdy element blokowy ma ustaloną szerokość, sam tekst umieszczamy w div. By ułatwić całą sztuczkę i zwiększyć jej elastyczność, sam tekst do przycięcia opakowujemy w span. Ponieważ przycinany tekst może zajmować tylko jedną linię, podejście to pozwala sterować tym, które fragmenty będą przycinane, a które nie (przykład znajduje się na CSS3 text).

Kod CSS również trudny nie jest i działa od razu we wszystkich przeglądarkach obsługujących wielokropek na poziomie CSS.

div#e {          
width: 100px; /* Ściśle określona szerokość musi wystąpić, dla dodatkowo użyj table-layout: fixed. */
text-overflow: ellipsis; /* Dla IE i Safari. */
-o-text-overflow: ellipsis; /* Dla Opery. */
overflow: hidden; /* Wymagane, by działał ellipsis. */
border: 1px solid red; /* By ładnie widzieć granicę */
}
div#e span.ellipsis {
white-space: nowrap; /* Wymagane, by działał ellipsis. */
width:100%; /* Bez tego, w IE nie zadziała. */
}
/* Oba poniższe wpisy dotyczą Firefoxa i muszą wystąpić, by kod JS zadziałał poprawnie. */
div#e window{ width:100%; -moz-user-focus:normal; -moz-user-select:text; }
div#e description{ -moz-user-focus:normal; -moz-user-select:text; }

Na tym możemy zakończyć, jeśli nie interesuje nas Firefox. Ponieważ to nasza ulubiona (no, może akurat nie w tym temacie) przeglądarka, idźmy dalej...

Kod JavaScript

Poniższy kod wystarczy umieścić w dowolnym miejscu na stronie lub załadować z zewnętrznego pliku.

//  Autorzy funkcji: Jonathan Snook (http://www.snook.ca/jonathan) i Robert Nyman (http://www.robertnyman.com)
function getElementsByClassName(oElm, strTagName, strClassName){
var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
};
};
return (arrReturnElements);
};

// Właściwy kod powodujący magię wielokropka.
var _userAgent = navigator.userAgent.toLowerCase();
if (/mozilla/.test(_userAgent) && !/(compatible|webkit)/.test(_userAgent)) {
document.addEventListener( "DOMContentLoaded", function() {
var sNS = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul';
var xml = document.createElementNS(sNS , 'window');
var label = document.createElementNS(sNS, 'description');
label.setAttribute('crop','end');
xml.appendChild(label);
var e = getElementsByClassName(document,'span','ellipsis');
for (i in e) {
var xml2 = xml.cloneNode(true);
xml2.firstChild.setAttribute('value',e[i].textContent);
e[i].innerHTML = '';
e[i].appendChild(xml2);
};
}, false );
};

Kod będzie działał poprawnie dla wszystkich elementów span z przypisaną klasą ellipsis.

Podsumowanie

Generalnie wielokropek działa we wszystkich systemach całkiem przyjemnie, ale jako taki ma jedną wadę ogólną i jedną z wiązaną z Firefoxem:

  • działa tylko dla fragmentów jednolinijkowych (nie jest dobry, gdy chcemy zużyć całe dostępne miejsce ograniczone zarówno szerokością, jak i wysokością (czyli mamy miejsce na 5 linijek, więc 4 wyświetlamy normalnie, ale 5 przycinamy). Użycie span nie zapewni w tym momencie elastyczności z powodów opisanych w pierwszym akapicie. Jedyne rozwiązanie to JavaScript, ale o tym innym razem.
  • ponieważ rozwiązanie dla Firefox działa, używając zwykłego tekstu, nie możemy użyć w przycinanym tekście znaczników.

12 November 2007

Simple CSS minifier

I know there are many similar tools, but not all of them are written in Python. Hmm... I should say most of them are not.

This is not a real compressor, because it just try to get rid of comments, unnecessary characters, empty lines etc. On the Net there are more advanced solutions trying to join similar rules, use the shortest version of them if possible. Joining those two types of minification can give very good results, especially when connected with GZip compression as on this site.

Code is very simple. I'm using four types of compression:

  • NONE -- nothing is really done except of removing Windows line endings
  • SIMPLE -- this method also remove longer comments and empty lines
  • NORMAL -- as above but also converts rules to one line versions and removes all unnecessary characters
  • FULL -- as above but also removes short comments and creates all CSS compressed to one line of text

Below code:

#test a {
/* Dummy comment */
text-align: left;
color: #222;
background-color: #fff;
}
#test a:hover {
color: #000;
}

will be transformed to something like this, using NORMAL compression level:

#test a{text-align:left;color:#222;background-color:#fff}
#test a:hover{color:#000}

Regular expression rules for compressing the CSS are separated from main code, so you can easily add your own. The code:

"""
Simple CSS Minifier.
Code were created by Rafał Jońca.
The code is available on double license: GPL and MIT.
"""

import
re

# Constants for use in compression level setting.
NONE = 0
SIMPLE = 1
NORMAL = 2
FULL = 3

_REPLACERS = {
NONE: (None), # dummy
SIMPLE: ((r'\/\*.{4,}?\*\/', ''), # comment
(r'\n\s*\n', r"\n"), # empty new lines
(r'(^\s*\n)|(\s*\n$)', "")), # new lines at start or end
NORMAL: ((r'/\*.{4,}?\*/', ''), # comments
(r"\n", ""), # delete new lines
('[\t ]+', " "), # change spaces and tabs to one space
(r'\s?([;:{},+>])\s?', r"\1"), # delete space where it is not needed, change ;} to }
(r';}', "}"), # because semicolon is not needed there
(r'}', r"}\n")), # add new line after each rule
FULL: ((r'\/\*.*?\*\/', ''), # comments
(r"\n", ""), # delete new lines
(r'[\t ]+', " "), # change spaces and tabs to one space
(r'\s?([;:{},+>])\s?', r"\1"), # delete space where it is not needed, change ;} to }
(r';}', "}")), # because semicolon is not needed there
}

class CssMin:
def __init__(self, level=NORMAL):
self.level = level

def compress(self, css):
"""Tries to minimize the length of CSS code passed as parameter. Returns string."""
css = css.replace("\r\n", "\n") # get rid of Windows line endings, if they exist
for rule in _REPLACERS[self.level]:
css = re.compile(rule[0], re.MULTILINE|re.UNICODE|re.DOTALL).sub(rule[1], css)
return css

def minimalize(css, level=NORMAL):
"""Compress css using level method and return new css as a string."""
return CssMin(level).compress(css)

if __name__ == '__main__':
import sys
if len(sys.argv) <> 3:
print "Usage: %s " % sys.argv[0]
sys.exit(1)

f = open(sys.argv[1])
inputcss = f.read()
f.close()
open(sys.argv[2], 'w').write(minimalize(inputcss))

To compress code use minimalize() function. From command line, just use cssmin.py input output.

If you are looking for similar thing but for JavaScript, don't reinvent the wheel, use this code. It is based on original C version of JS minifier and work as a function or from command line.

5 November 2007

Average site performance tester

Today I was doing some performance tests connected to dynamic site generation. As a result I created small script in Python that helps me doing those tests. Because the code is very simple (for example no exception handling) and is one of my first tools in Python, I want to share it.

from timeit import Timer
import urllib2
import random
import sys

REPETITIONS = 1000
REFERERS_LIST = ('http://www.example.com/1.html',
'http://www.example.com/2.html')
STD_URLS = ('http://www.example.com/3.html',
'www.example.com/4.html')

def get_ad(url_pool, referer_pool = REFERERS_LIST):
"""Read and forget several bytes from one of url_pool URLs using one of referer_pool URLs as Referer."""
url = random.choice(url_pool)
referer = random.choice(referer_pool)
req = urllib2.Request(url)
req.add_header('Referer', referer)
#Open url and read at least several bytes
urlfile = urllib2.urlopen(req)
urlfile.read(16)
urlfile.close()

def run_tests(pools):
"""Run performance test for URLs from pools list."""
for pool in pools:
print 'Running test for %s...' % (pool,)
list = getattr(sys.modules[__name__], pool)
t = Timer("get_ad(%s)" % pool, "from %s import get_ad, %s" % (__name__, pool))
print "%.3f msec/url" % (1000 * t.timeit(REPETITIONS)/REPETITIONS)

if __name__ == '__main__':
run_tests(('STD_URLS',))

You can define as many _URLS tuples or lists as you wish. Code will test them all using randomly selected URL for every simple connection. Then just add names of those lists to run_tests() call. Thats all folks!