| CSS-LESSp documentation | Contained in the CSS-LESSp distribution. |
CSS::LESSp - LESS for perl. Parse .less files and returns valid css (lesscss.org for more info about less files)
use CSS::LESSp;
my $buffer;
open(IN, "file.less");
for ( <IN> ) { $buffer .= $_ };
close(IN);
my @css = CSS::LESSp->parse($buffer);
print join("", @css);
or you could simply use the lessp.pl tool in the package
$ lessp.pl css.less > css.css
This module is designed to parse and compile .less files in to .css files.
About the documentation and syntax of less files please visit lesscss.org
What is the benefits of LESS for perl ...
It's extremely fast :
# time ./lessp.pl big.less > big.css real 0m2.198s user 0m2.174s sys 0m0.020s # time lessc big.less big.css real 0m18.805s user 0m18.437s sys 0m0.184s
Main parse method, returns array of the css file
a ) You can not import other less files ...
You can't do this
@import url('/other.less')
It might be added in future versions
b ) You can not use hsl as a color
You can't do this
color: hsl(125,125,125);
All other bugs should be reported via http://rt.cpan.org/Public/Dist/Display.html?Name=CSS-LESSp or bug-CSS-LESSp@rt.cpan.org.
Ivan Drinchev <drinchev@gmail.com>
People who've helped with this project :
Michael Schout <mschout@gkg.net>
Copyright (c) 2010.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
| CSS-LESSp documentation | Contained in the CSS-LESSp distribution. |
#!/usr/bin/perl package CSS::LESSp; use warnings; use strict; use Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw('parse'); our $VERSION = '0.86'; my $id = 1; # # specs check # # accessors.less ( ok ) # big.less ( ok ) # colors.less ( ok ) exceptions : no hsl # comments.less ( ok ) # css-3.less ( ok ) # css.less ( ok ) # dash-prefix.less ( ok ) # functions.less ( unsupported ) # hidden.less ( unsupported ) # import-with-extra-paths.less ( unsupported ) # import.less ( unsupported ) # lazy-eval.less ( unsupported ) # literal-css.less ( unsupported ) # mixins-args.less ( ok ) # mixins.less ( ok ) # operations.less ( ok ) # parens.less ( ok ) # rulesets.less ( ok ) # scope.less ( ok ) # selectors.less ( ok ) # strings.less ( ok ) # variables.less ( ok ) # whitespace.less ( ok ) exceptions : doesn't merge same lines # sub new { my $class = shift; my $self = { 'variables' => {}, 'rules' => [], 'type' => 'child', 'children' => [] }; bless $self, $class; return $self; } sub isFunction { my ($self) = @_; if ( $self->{'type'} eq 'function' ) { return 1; } return 0; } sub insertChild { my ($self, $name, $parents) = @_; my @parents = @{$parents} if $parents; $id++; $self->{'children'} = [] if !defined($self->{'children'}); $name =~ s/\n/ /mg; # # Find the full name my $fullName = $name; if ( defined($self->{'id'}) ) { my @names = $name =~ /\,/ ? split(/\s*\,\s*/, $name) : ( $name ); for my $parent ( reverse @parents ) { next if !defined($parent->{'name'}); my @prenames = $parent->{'name'} =~ /\,/ ? split(/\s*\,\s*/, $parent->{'name'}) : ($parent->{'name'}); my @postnames = (); for my $prename ( @prenames ) { for my $name ( @names ) { push @postnames, $prename.( $name =~ /^\:/ ? "" : " " ).$name; } } @names = @postnames; $fullName = join(", ", @names); } } push @{$self->{'children'}}, { 'name' => $name, 'fullname' => $fullName, 'type' => 'child', 'id' => $id, 'variables' => {} }; return bless $self->{'children'}->[$#{$self->{'children'}}]; } sub insertVariable { my ($self, $variable, $value) = @_; if ( $self->isFunction ) { push @{$self->{'variables'}}, { $variable => $value }; } else { $self->{'variables'}->{$variable} = $value; } return $self; } sub insertRule { my ($self, $property, $value) = @_; $self->{'rules'} = [] if !defined($self->{'rules'}); push @{$self->{'rules'}}, { $property => $value }; return $self; } sub insertFunction { my ($self, $name, $parents) = @_; my @parents = @{$parents} if $parents; #my @variables = @{$variables} if $variables; $name =~ s/\n/ /mg; $self->{'functions'} = [] if !defined($self->{'functions'}); push @{$self->{'functions'}}, { 'name' => $name, 'type' => 'function', 'variables' => [] }; return bless $self->{'functions'}->[$#{$self->{'functions'}}]; } sub getVariable { my ($self, $variable, $parents) = @_; my @parents = @{$parents} if $parents; # # First try to see if the variable is here if ( defined($self->{'variables'}->{$variable}) ) { return $self->{'variables'}->{$variable}; } # # If we have parents parse them too for my $parent ( reverse @parents ) { return $parent->{'variables'}->{$variable} if defined($parent->{'variables'}->{$variable}); } return 0; } sub getSelector { my ($self, $name, $parents) = @_; my @parents = @{$parents} if $parents; # # First try to see if it's a previous sibling if ( defined($self->{'children'}) ) { for my $style ( @{$self->{'children'}} ) { return $style if ($style->{'name'} eq $name); } } # # Next try to find from parents for my $parent ( reverse @parents ) { for my $style ( @{$parent->{'children'}} ) { return $style if ($style->{'name'} eq $name); } } # # Last we start search of everything if it is root my $root = $parents[0] if !defined($parents[0]->{'id'}); if ( defined($root->{'children'}) ) { my $return; my $fullNameSelector; $root->process(sub { my ($style) = @_; if ( defined($style->{'name'}) and $style->{'name'} eq $name ) { $return = $style; } if ( defined($style->{'fullname'}) and $style->{'fullname'} eq $name ) { $fullNameSelector = $style; } }); return $return if $return; return $fullNameSelector if $fullNameSelector; } # # If we can't find ... then try for functions the same way if ( defined($self->{'functions'}) ) { for my $function ( @{$self->{'functions'}} ) { return $function if ($function->{'name'} eq $name); } } for my $parent ( reverse @parents ) { if ( defined($parent->{'functions'}) ) { for my $function ( @{$parent->{'functions'}} ) { return $function if ($function->{'name'} eq $name); } } } if ( defined($root->{'children'}) ) { my $return; my $fullNameSelector; $root->process(sub { my ($style) = @_; if ( defined($style->{'functions'}) ) { for my $function ( @{$style->{'functions'}} ) { if ( defined($function->{'name'}) and $function->{'name'} eq $name ) { $return = $function; } } } }); return $return if $return; return $fullNameSelector if $fullNameSelector; } return 0; } sub getValue { my ($self, $property) = @_; my $value; if ( defined($self->{'rules'}) ) { for my $rule ( @{$self->{'rules'}} ) { $value = join('', values %{$rule}) if join('', keys %{$rule}) eq $property; } } return $value; } sub process { my ($styles, $function) = @_; my @parents = (); push @parents, $styles->{'children'}; $styles = $styles->{'children'}; my $level = 0; push my @position, 0; while ( 1 ) { my $style = $styles->[$position[$level]]; &$function($style, $level, (\@parents, \@position)); if ( defined($style->{'children'}) ) { $level++; $position[$level] = 0; push @parents, $styles; $styles = $style->{'children'}; } else { while ( $position[$level] == $#{$styles} ) { return 1 if $level == 0; $styles = pop @parents; $level--; } $position[$level]++; } } return 0; } sub copyTo { my ($self, $targetSelector) = @_; my ($rules, $children, $variables); # copy rules if ( defined($self->{'rules'}) ) { $targetSelector->{'rules'} = [] if !defined($targetSelector->{'rules'}); for my $rule ( @{$self->{'rules'}} ) { push @{$targetSelector->{'rules'}}, {%{$rule}}; } } # copy variables if ( defined($self->{'variables'}) ) { for my $variable ( keys %{$self->{'variables'}} ) { $targetSelector->{'variables'}->{$variable} = $variables->{$variable}; } } # copy children if ( defined($self->{'children'}) ) { my @targets = (); push @targets, $targetSelector; my $target = $targetSelector; my $l = 0; $self->process(sub { my ($style, $level) = @_; $target = $target->insertChild($style->{'name'}, \@targets); # # adding variables if ( defined($style->{'variables'}) ) { for my $variable ( keys %{$style->{'variables'}} ) { $target->insertVariable($variable, $style->{'variables'}->{$variable}); } } # # adding rules if ( defined($style->{'rules'}) ) { for my $rule ( @{$style->{'rules'}} ) { my $property = join("", keys %{$rule}); my $value = join("", values %{$rule}); $target->insertRule($property, $value); } } # # children leveling if ( defined($style->{'children'}) ) { push @targets, $target; } else { if ( $l != $level ) { $l = $level; $target = pop @targets; } } }); } return 0; } sub copyFunction { my ($self, $targetSelector, $variables, $parents) = @_; my ($rules, $children); my @variables = @{$variables}; my @parents = @{$parents}; # copy vars if ( defined($self->{'variables'}) ) { for ( my $position = 0; $position <= $#{$self->{'variables'}}; $position++ ) { my $variable = join("",keys %{$self->{'variables'}->[$position]}); my $value = join("", values %{$self->{'variables'}->[$position]}); $value = $variables[$position] if $variables[$position]; $targetSelector->{'variables'}->{$variable} = $value; } } # copy rules if ( defined($self->{'rules'}) ) { $targetSelector->{'rules'} = [] if !defined($targetSelector->{'rules'}); for my $rule ( @{$self->{'rules'}} ) { my $property = join("", keys %{$rule}); my $value = join("", values %{$rule}); while ( $value =~ /\@(\w+\-*\w*)/ ) { my $word = $1; # get the variable my $var = $targetSelector->getVariable($word, \@parents); $value =~ s/\@$word/$var/; # if variable value is negative if ( $var =~ s/^\-// ) { $value =~ s/\-\s*\-$var/\+ $var/g; $value =~ s/\+\s*\-$var/\- $var/g; } } # parse for other stuff $value = _parse_value($value); push @{$targetSelector->{'rules'}}, {$property => $value}; } } # copy children if ( defined($self->{'children'}) ) { my @targets = (); push @targets, $targetSelector; my $target = $targetSelector; my $l = 0; $self->process(sub { my ($style, $level) = @_; $target = $target->insertChild($style->{'name'}); # # adding variables if ( defined($style->{'variables'}) ) { for my $variable ( keys %{$style->{'variables'}} ) { $target->insertVariable($variable, $style->{'variables'}->{$variable}); } } # # adding rules if ( defined($style->{'rules'}) ) { for my $rule ( @{$style->{'rules'}} ) { my $property = join("", keys %{$rule}); my $value = join("", values %{$rule}); $target->insertRule($property, $value); } } # # children leveling if ( defined($style->{'children'}) ) { push @targets, $target; } else { if ( $l != $level ) { $l = $level; $target = pop @targets; } } }); } return 0; } sub dump { my ($self) = @_; my @return = (); if ( defined($self->{'rules'}) ) { for my $rule ( @{$self->{'rules'}} ) { push @return, join('', keys %{$rule}).join('', values %{$rule}).";\n"; } push @return, "\n"; } $self->process(sub { my ($style, $level, @arrays) = @_; my @parents = @{$arrays[0]}; my @position = @{$arrays[1]}; if ( defined($style->{'rules'}) ) { push @return, $style->{'fullname'} ." { "; push @return, "\n" if $#{$style->{'rules'}} > 0; for my $rule ( @{$style->{'rules'}} ) { push @return, "\t" if $#{$style->{'rules'}} > 0; push @return, join('', keys %{$rule}).": ".join('', values %{$rule})."; "; push @return, "\n" if $#{$style->{'rules'}} > 0; } push @return, "}\n"; } }); return @return; } sub _parse_value { my $value = shift; # convert rgb(255,255,255) to #ffffff ( hsl doesn't work right now ) while ( $value =~ /rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/ ) { my $color = sprintf("#%0.2X%0.2X%0.2X", $1,$2,$3); $value =~ s/rgb\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/$color/; } # expand colors ( from #fff to #ffffff ) while ( $value =~ /\#([abcdef0123456789]{3})([^abcdef0123456789]|$)/i ) { my $color = $1; my $replace = join('', map { $_ x 2 } split(//, $color)); # equals to : for ( split(//, $color) ) { $replace .= $_ x 2; } $value =~ s/\#$color/\#$replace/; } # expressions in brackets while ( $value =~ /\(\s*(\-?\d+\s*((px|pt|em|cm|%))*\s*[\+\*\/\-]\s*\-?\d+\s*((px|pt|em|cm|%))*)\s*\)/ ) { my $expression = my $eval = $1; my ($removed) = $eval =~ m/(px|pt|em|cm|%)/; $eval =~ s/(px|pt|em|cm|%)//; if ( $eval !~ /[a-z]/i and defined(my $result = eval($eval)) ) { $result .= "$removed" if defined $result and $removed; $value =~ s/(\(\Q$expression\E\))/$result/; }; } # expression (+,-,*,/) ; whole expressions if ( $value =~ /(\d+)\s*(px|pt|em|cm|%)*\s*(\+|\*|\/)\s*((\d+)\s*(px|pt|em|cm|%)*|\d+)/ or $value =~ /(\d+)\s*(px|pt|em|cm|%)*\s*(\-)\s+((\d+)\s*(px|pt|em|cm|%)*|\d+)/ ) { my $eval = $value; my $removed = $1 if $eval =~ s/(px|pt|em|cm|%)//g; if ( $eval !~ /[a-z]/i ) { my $result = eval($eval); $result .= "$removed" if defined($result) and $removed; $value = $result if !$@ and defined $result; } } # expression with color if ( $value =~ /\#[abcdef0123456789]{6}/i and $value =~ /(\+|\-|\*|\/)/ ) { my @rgb = ( $value, $value, $value ); $rgb[0] =~ s/\#([abcdef0123456789]{2})[abcdef0123456789]{4}/\#$1/ig; $rgb[1] =~ s/\#[abcdef0123456789]{2}([abcdef0123456789]{2})[abcdef0123456789]{2}/\#$1/ig; $rgb[2] =~ s/\#[abcdef0123456789]{4}([abcdef0123456789]{2})/\#$1/ig; my $return = ""; for ( @rgb ) { while ( /\#([abcdef0123456789]{2})/i ) { my $dec = hex($1); s/\#$1/$dec/; } if ( !/[a-z]/i ) { my $eval = eval; if ( $eval < 0 ) { $eval = 0 }; if ( $eval > 255 ) { $eval = 255 }; $return .= sprintf("%0.2X", $eval); } } $value = "#".lc $return if $return; } return $value; } sub parse { my $self = shift; my $string = shift; my $styles = CSS::LESSp->new(); my $selector = $styles; my @parents; push @parents, $styles; # real parsing my $lastChar = my $buffer = my $mode = my $stop = ""; $string =~ s/^\xEF\xBB\xBF\x0A//; # removing special characters from front of file for ( split //, $string ) { $buffer .= $_; if ( $mode ) { $buffer =~ s/.$// if $mode eq "delete"; if ( length($stop) == 1 and $_ eq $stop ) { $mode = "" }; if ( length($stop) == 2 and $lastChar.$_ eq $stop ) { $mode = "" } else { $lastChar = $_ }; next; } next if /\n/; # # The program # if ( /\}/ or /\;/ ) { # clearing some buffer data $buffer =~ s/.$//; $buffer =~ s/^\s*|\s*$//g; # remove any spaces from front or back $buffer =~ s/\s*\n\s*/ /g; # removes any new line if ( $buffer ) { # if it's a property and rule if ( $buffer =~ s/^\s*([^:]*)\s*\:\s*(.*)\s*$// ) { my $property = $1; my $value = $2; $value =~ s/\n/ /g; $property =~ s/\s*$//; # remove any additional spaces left # different rule-set property ( mixins ) if ( $value =~ /^(#|.)(.*)\[(.*)\]$/ ) { my $targetSelectorName = $1.$2; my $targetProperty = $3; my $return; $targetProperty =~ s/['"]//g; $targetProperty =~ s/\s*//g; # get the target mixin selector my $targetSelector = $selector->getSelector($targetSelectorName, \@parents); # get the value if ( $targetProperty =~ /^\@/ ) { $targetProperty =~ s/^\@//; $return = $targetSelector->getVariable($targetProperty); } else { $return = $targetSelector->getValue($targetProperty); } $value = $return if $return; } # variable access if ( !$selector->isFunction ) { while ( $value =~ /\@(\w+\-*\w*)/ ) { my $word = $1; # get the variable my $var = $selector->getVariable($word, \@parents); $value =~ s/\@$word/$var/; # if variable value is negative if ( $var =~ s/^\-// ) { $value =~ s/\-\s*\-$var/\+ $var/g; $value =~ s/\+\s*\-$var/\- $var/g; } } # parse for other stuff $value = _parse_value($value); } # variable definition check ( after we have parsed the stuff ) if ( $property =~ /^\@(\w+\-*\w*)/ ) { $selector->insertVariable($1, $value); $property = $value = ""; } else { $selector->insertRule($property, $value); } } # nested rules (mixins) if ( $buffer =~ s/^(\..*)$// or $buffer =~ s/^(\#.*)$// ) { my $source = $1; $source =~ s/\s*\:\s*/\:/g; # for multiple mixins while ( $source =~ /\(.*,.*\)/ ) { $source =~ s/\(\s*(.*)\s*,\s*(.*)\s*\)/($1;$2)/ }; my @sources = $source =~ /\,/ ? split(/\s*\,\s*/, $source) : ( $source ); for my $source ( @sources ) { my @vars; # this is if any functions are found my $sourceSelector = $selector->getSelector($source , \@parents); if ( $source =~ /\>/ && !$sourceSelector ) { my ( $parent, $child ) = split(/\s*\>\s*/, $source); my $pSelector = $selector->getSelector($parent, \@parents); $sourceSelector = $pSelector->getSelector($child) if $pSelector; } if ( $source =~ /(.*)\((.*)\)/ && !$sourceSelector ) { my $function = $1; my $vars = $2; @vars = $vars =~ /\;/ ? split(/\;/, $vars) : ( $vars ); $sourceSelector = $selector->getSelector($function, \@parents); } next if !$sourceSelector; if ( $sourceSelector->isFunction ) { $sourceSelector->copyFunction($selector, \@vars, \@parents); } else { $sourceSelector->copyTo($selector); } } } # other stuff $selector->insertRule($buffer,"") if $buffer; } $selector = pop @parents if /\}/; $buffer = ""; } # # selectors if ( /\{/ ) { # clearing some buffer data $buffer =~ s/.$//g; $buffer =~ s/^\s*|\s*$//g; # insert the child if ( $buffer ) { push @parents, $selector; if ( $buffer =~ /(.*)\s*\((\@*.*)\)/ and $buffer !~ /^.*[:].*\(/ ) { my $function = $1; my $vars = $2; $function =~ s/^\s*|\s*$//g; $selector = $selector->insertFunction($function, \@parents); my @vars = $vars =~ /\,/ ? split(/\s*\,\s*/, $vars) : ( $vars ); for my $var ( @vars ) { next if !$var; my ($variable, $value) = split(/\s*\:\s*/, $var); $variable =~ s/^\@//; $selector->insertVariable($variable, $value); } } else { $selector = $selector->insertChild($buffer, \@parents); } } $buffer = ""; } # # # if ( /\"/ or /\'/ ) { $mode = "skip"; $stop = $_ }; if ( /\(/ ) { $mode = "skip"; $stop = ")" }; if ( $lastChar =~ /\// and /\// ) { $mode = "delete"; $stop = "\n"; $buffer =~ s/..$// } if ( $lastChar =~ /\// and /\*/ ) { $mode = "delete"; $stop = "*/"; $buffer =~ s/..$// }; $lastChar = $_; } return $styles->dump(); } 1;