PKQqH lesscpy/__init__.py__version_info__ = ('0', '11', '1') __version__ = '.'.join(__version_info__) def compile(file, minify=False, xminify=False, tabs=False, spaces=True): from .lessc import parser from .lessc import formatter class Opt(object): def __init__(self): self.minify = minify self.xminify = xminify self.tabs = tabs self.spaces = spaces p = parser.LessParser(fail_with_exc=True) opt = Opt() p.parse(file=file) f = formatter.Formatter(opt) return f.format(p) PKzE%j..lesscpy/exceptions.pyclass CompilationError(SyntaxError): pass PKpD-Clesscpy/scripts/__init__.pyPKPqHuClesscpy/scripts/compiler.py# -*- coding: utf8 -*- """ .. module:: lesscpy.scripts.compiler CSS/LESSCSS run script http://lesscss.org/#docs Copyright (c) See LICENSE for details .. moduleauthor:: Johann T. Mariusson """ from __future__ import print_function import os import sys import glob import copy import argparse sys.path.append(os.path.abspath(os.path.dirname(__file__))) from lesscpy.lessc import parser from lesscpy.lessc import lexer from lesscpy.lessc import formatter VERSION_STR = 'Lesscpy compiler 0.9h' def ldirectory(inpath, outpath, args, scope): """Compile all *.less files in directory Args: inpath (str): Path to compile outpath (str): Output directory args (object): Argparse Object scope (Scope): Scope object or None """ yacctab = 'yacctab' if args.debug else None if not outpath: sys.exit("Compile directory option needs -o ...") else: if not os.path.isdir(outpath): if args.verbose: print("Creating '%s'" % outpath, file=sys.stderr) if not args.dry_run: os.mkdir(outpath) less = glob.glob(os.path.join(inpath, '*.less')) f = formatter.Formatter(args) for lf in less: outf = os.path.splitext(os.path.basename(lf)) minx = '.min' if args.min_ending else '' outf = "%s/%s%s.css" % (outpath, outf[0], minx) if not args.force and os.path.exists(outf): recompile = os.path.getmtime(outf) < os.path.getmtime(lf) else: recompile = True if recompile: print('%s -> %s' % (lf, outf)) p = parser.LessParser(yacc_debug=(args.debug), lex_optimize=True, yacc_optimize=(not args.debug), scope=scope, tabfile=yacctab, verbose=args.verbose) p.parse(filename=lf, debuglevel=0) css = f.format(p) if not args.dry_run: with open(outf, 'w') as outfile: outfile.write(css) elif args.verbose: print('skipping %s, not modified' % lf, file=sys.stderr) sys.stdout.flush() if args.recurse: [ldirectory(os.path.join(inpath, name), os.path.join(outpath, name), args, scope) for name in os.listdir(inpath) if os.path.isdir(os.path.join(inpath, name)) and not name.startswith('.') and not name == outpath] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def run(): """Run compiler """ aparse = argparse.ArgumentParser(description='LessCss Compiler', epilog='<< jtm@robot.is @_o >>') aparse.add_argument('-v', '--version', action='version', version=VERSION_STR) aparse.add_argument('-I', '--include', action="store", type=str, help="Included less-files (comma separated)") aparse.add_argument('-V', '--verbose', action="store_true", default=False, help="Verbose mode") fgroup = aparse.add_argument_group('Formatting options') fgroup.add_argument('-x', '--minify', action="store_true", default=False, help="Minify output") fgroup.add_argument('-X', '--xminify', action="store_true", default=False, help="Minify output, no end of block newlines") fgroup.add_argument('-t', '--tabs', help="Use tabs", action="store_true") fgroup.add_argument( '-s', '--spaces', help="Number of startline spaces (default 2)", default=2) dgroup = aparse.add_argument_group('Directory options', 'Compiles all *.less files in directory that ' 'have a newer timestamp than it\'s css file.') dgroup.add_argument('-o', '--out', action="store", help="Output directory") dgroup.add_argument( '-r', '--recurse', action="store_true", help="Recursive into subdirectorys") dgroup.add_argument( '-f', '--force', action="store_true", help="Force recompile on all files") dgroup.add_argument('-m', '--min-ending', action="store_true", default=False, help="Add '.min' into output filename. eg, name.min.css") dgroup.add_argument('-D', '--dry-run', action="store_true", default=False, help="Dry run, do not write files") group = aparse.add_argument_group('Debugging') group.add_argument('-g', '--debug', action="store_true", default=False, help="Debugging information") group.add_argument('-S', '--scopemap', action="store_true", default=False, help="Scopemap") group.add_argument('-L', '--lex-only', action="store_true", default=False, help="Run lexer on target") group.add_argument('-N', '--no-css', action="store_true", default=False, help="No css output") aparse.add_argument('target', help="less file or directory") aparse.add_argument('output', nargs='?', help="output file path") args = aparse.parse_args() try: # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # if args.lex_only: lex = lexer.LessLexer() ll = lex.file(args.target) while True: tok = ll.token() if not tok: break if hasattr(tok, "lexer"): # literals don't have the lexer attribute print(tok, "State:", tok.lexer.lexstate) else: print(tok) print('EOF') sys.exit() # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # yacctab = 'yacctab' if args.debug else None scope = None if args.include: for u in args.include.split(','): if os.path.exists(u): p = parser.LessParser(yacc_debug=(args.debug), lex_optimize=True, yacc_optimize=(not args.debug), tabfile=yacctab, verbose=args.verbose) p.parse(filename=u, debuglevel=args.debug) if not scope: scope = p.scope else: scope.update(p.scope) else: sys.exit('included file `%s` not found ...' % u) sys.stdout.flush() p = None f = formatter.Formatter(args) if not os.path.exists(args.target): sys.exit("Target not found '%s' ..." % args.target) if os.path.isdir(args.target): ldirectory(args.target, args.out, args, scope) if args.dry_run: print('Dry run, nothing done.', file=sys.stderr) else: p = parser.LessParser(yacc_debug=(args.debug), lex_optimize=True, yacc_optimize=(not args.debug), scope=copy.deepcopy(scope), verbose=args.verbose) p.parse(filename=args.target, debuglevel=args.debug) if args.scopemap: args.no_css = True p.scopemap() if not args.no_css and p: out = f.format(p) if args.output: with open(args.output, "w") as f: f.write(out) else: print(out) except (KeyboardInterrupt, SystemExit, IOError): sys.exit('\nAborting...') PKbBDjWWlesscpy/lib/reserved.py""" Reserved token names Copyright (c) See LICENSE for details. """ tokens = { '@media': 'css_media', '@page': 'css_page', '@import': 'css_import', '@charset': 'css_charset', '@font-face': 'css_font_face', '@namespace': 'css_namespace', '@keyframes': 'css_keyframes', '@-moz-keyframes': 'css_keyframes', '@-webkit-keyframes': 'css_keyframes', '@-ms-keyframes': 'css_keyframes', '@-o-keyframes': 'css_keyframes', '@viewport': 'css_viewport', '@-ms-viewport': 'css_viewport', '@arguments': 'less_arguments', } PKbBDqr_P P lesscpy/lib/css.py""" CSS syntax names. Copyright (c) See LICENSE for details. """ css2 = [ 'azimuth', 'background-attachment', 'background-color', 'background-image', 'background-position', 'background-repeat', 'background', 'border-collapse', 'border-color', 'border-spacing', 'border-style', 'border-top', 'border-right', 'border-bottom', 'border-left', 'border-top-color', 'border-right-color', 'border-bottom-color', 'border-left-color', 'border-top-style', 'border-right-style', 'border-bottom-style', 'border-left-style', 'border-top-width', 'border-right-width', 'border-bottom-width', 'border-left-width', 'border-width', 'border', 'bottom', 'caption-side', 'clear', 'clip', 'color', 'content', 'counter-increment', 'counter-reset', 'cue-after', 'cue-before', 'cue', 'cursor', 'direction', 'display', 'elevation', 'empty-cells', 'float', 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', 'font', 'height', 'left', 'letter-spacing', 'line-height', 'list-style-image', 'list-style-position', 'list-style-type', 'list-style', 'margin-right', 'margin-left', 'margin-top', 'margin-bottom', 'margin', 'max-height', 'max-width', 'min-height', 'min-width', 'orphans', 'outline-color', 'outline-style', 'outline-width', 'outline', 'overflow', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', 'padding', 'page-break-after', 'page-break-before', 'page-break-inside', 'pause-after', 'pause-before', 'pause', 'pitch-range', 'pitch', 'play-during', 'position', 'quotes', 'richness', 'right', 'speak-header', 'speak-numeral', 'speak-punctuation', 'speak', 'speech-rate', 'stress', 'table-layout', 'text-align', 'text-decoration', 'text-indent', 'text-transform', 'top', 'unicode-bidi', 'vertical-align', 'visibility', 'voice-family', 'volume', 'white-space', 'widows', 'width', 'word-spacing', 'z-index', ] css3 = [ 'alignment-adjust', 'alignment-baseline', 'animation', 'animation-delay', 'animation-direction', 'animation-duration', 'animation-iteration-count', 'animation-name', 'animation-play-state', 'animation-timing-function', 'appearance', 'backface-visibility', 'background-clip', 'background-origin', 'background-size', 'baseline-shift', 'bookmark-label', 'bookmark-level', 'bookmark-target', 'border-bottom-left-radius', 'border-bottom-right-radius', 'border-image', 'border-image-outset', 'border-image-repeat', 'border-image-slice', 'border-image-source', 'border-image-width', 'border-radius', 'border-top-left-radius', 'border-top-right-radius', 'box-align', 'box-decoration-break', 'box-direction', 'box-flex', 'box-flex-group', 'box-lines', 'box-ordinal-group', 'box-orient', 'box-pack', 'box-shadow', 'box-sizing', 'color-profile', 'column-count', 'column-fill', 'column-gap', 'column-rule', 'column-rule-color', 'column-rule-style', 'column-rule-width', 'column-span', 'column-width', 'columns', 'crop', 'dominant-baseline', 'drop-initial-after-adjust', 'drop-initial-after-align', 'drop-initial-before-adjust', 'drop-initial-before-align', 'drop-initial-size', 'drop-initial-value', 'fit', 'fit-position', 'float-offset', 'font-size-adjust', 'font-stretch', 'grid-columns', 'grid-rows', 'hanging-punctuation', 'hyphenate-after', 'hyphenate-before', 'hyphenate-character', 'hyphenate-lines', 'hyphenate-resource', 'hyphens', 'icon', 'image-orientation', 'image-resolution', 'inline-box-align', 'line-stacking', 'line-stacking-ruby', 'line-stacking-shift', 'line-stacking-strategy', # 'mark', 'mark-after', 'mark-before', 'marks', 'marquee-direction', 'marquee-play-count', 'marquee-speed', 'marquee-style', 'move-to', 'nav-down', 'nav-index', 'nav-left', 'nav-right', 'nav-up', 'opacity', 'outline-offset', 'overflow-style', 'overflow-x', 'overflow-y', 'page', 'page-policy', 'perspective', 'perspective-origin', 'phonemes', 'punctuation-trim', 'rendering-intent', 'resize', 'rest', 'rest-after', 'rest-before', 'rotation', 'rotation-point', 'ruby-align', 'ruby-overhang', 'ruby-position', 'ruby-span', 'size', 'string-set', 'target', 'target-name', 'target-new', 'target-position', 'text-align-last', 'text-height', 'text-justify', 'text-outline', 'text-overflow', 'text-shadow', 'text-wrap', 'transform', 'transform-origin', 'transform-style', 'transition', 'transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function', 'voice-balance', 'voice-duration', 'voice-pitch', 'voice-pitch-range', 'voice-rate', 'voice-stress', 'voice-volume', 'word-break', 'word-wrap' ] # SVG only includes style not present in either css2 or css3: svg = [ # clipping / masking / compositing: 'clip-path', 'clip-rule', 'mask', # filter effects: 'enable-background', 'filter', 'flood-color', 'flood-opacity', 'lightning-color', # gradient: 'stop-color', 'stop-opacity', # interactivity: 'pointer-events', # color / painting: 'color-interpolation', 'color-interpolation-filters', 'color-rendering', 'fill', 'fill-opacity', 'fill-rule', 'image-rendering', 'marker', 'marker-end', 'marker-mid', 'marker-start', 'shape-rendering', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-rendering', # text: 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'kerning', 'text-anchor', 'writing-mode', ] vendor_prefix = [ '-ms-', '-moz-', '-o-', '-atsc-', '-wap-', '-webkit-', '-khtml-' '-xv-', 'mso-', ] vendor_ugly = [ 'accelerator', 'behavior', 'zoom', ] propertys = css2 + css3 + svg + vendor_ugly # CSS-2(.1) media types: http://www.w3.org/TR/CSS2/media.html#media-types # Include media types as defined in HTML4: http://www.w3.org/TR/1999/REC-html401-19991224/types.html#h-6.13 # Also explained in http://www.w3.org/TR/css3-mediaqueries/#background html4_media_types = [ 'all', 'aural', # deprecated by CSS 2.1, which reserves "speech" 'braille', 'handheld', 'print', 'projection', 'screen', 'tty', 'tv', ] css2_media_types = [ 'embossed', # CSS2, not HTML4 'speech', # CSS2. not HTML4 ] media_types = html4_media_types + css2_media_types css3_media_features = [ 'width', 'min-width', 'max-width', 'height', 'min-height', 'max-height', 'device-width', 'min-device-width', 'max-device-width', 'device-height', 'min-device-height', 'max-device-height', 'orientation', 'aspect-ratio', 'min-aspect-ratio', 'max-aspect-ratio', 'device-aspect-ratio', 'min-device-aspect-ratio', 'max-device-aspect-ratio', 'color', 'min-color', 'max-color', 'color-index', 'min-color-index', 'max-color-index', 'monochrome', 'min-monochrome', 'max-monochrome', 'resolution', 'min-resolution', 'max-resolution', 'scan', 'grid', ] vendor_media_features = [ '-webkit-min-device-pixel-ratio', 'min--moz-device-pixel-ratio', '-o-min-device-pixel-ratio', 'min-device-pixel-ratio', ] media_features = css3_media_features + vendor_media_features PKpD-Clesscpy/lib/__init__.pyPKiC Clesscpy/lib/colors.py""" """ lessColors = { 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000', 'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a', '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', 'darkgrey': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b', 'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc', 'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b', 'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969', 'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22', 'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700', 'goldenrod': '#daa520', 'gray': '#808080', 'grey': '#808080', 'green': '#008000', 'greenyellow': '#adff2f', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c', 'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa', 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6', 'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgray': '#d3d3d3', 'lightgrey': '#d3d3d3', 'lightgreen': '#90ee90', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a', 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32', 'linen': '#faf0e6', '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', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000', 'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6', 'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#d87093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb', 'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000', 'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072', 'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d', 'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090', 'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4', 'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32' } PKPqH@/ lesscpy/lib/dom.py""" HTML DOM names Copyright (c) See LICENSE for details. """ html4 = [ 'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'b', 'base', 'basefont', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', # 'link', 'map', 'mark', 'menu', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'p', 'param', 'pre', 'q', 's', 'samp', 'script', 'select', 'small', 'span', 'strike', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'u', 'ul', 'var', ] html5 = [ 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 'datalist', 'details', 'embed', 'figcaption', 'figure', 'footer', 'header', 'hgroup', 'keygen', 'main', 'mark', 'meter', 'nav', 'output', 'progress', ' progress-bar-stripes', 'rp', 'rt', 'ruby', 'section', 'source', 'summary', 'svg', 'time', 'track', 'video', 'wbr', 'only', # TODO/FIXME: What is this?!? ] svg = [ 'altGlyph', 'altGlyphDef', 'altGlyphItem', 'circle', 'desc', 'ellipse', 'glyphRef', 'line', 'path', 'polygon', 'polyline', 'rect', 'text', 'textPath', 'tref', 'tspan', ] elements = html4 elements.extend(html5) elements.extend(svg) PKPqH` 7 7lesscpy/lessc/color.py# -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.color :synopsis: Lesscpy Color functions Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import operator import colorsys import re import six from . import utility from lesscpy.lib import colors class Color(): def process(self, expression): """ Process color expression args: expression (tuple): color expression returns: str """ a, o, b = expression c1 = self._hextorgb(a) c2 = self._hextorgb(b) r = ['#'] for i in range(3): v = self.operate(c1[i], c2[i], o) if v > 0xff: v = 0xff if v < 0: v = 0 r.append("%02x" % v) return ''.join(r) def operate(self, left, right, operation): """ Do operation on colors args: left (str): left side right (str): right side operation (str): Operation returns: str """ operation = { '+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv }.get(operation) return operation(left, right) def rgb(self, *args): """ Translate rgb(...) to color string raises: ValueError returns: str """ if len(args) == 4: args = args[:3] if len(args) == 3: try: return self._rgbatohex(list(map(int, args))) except ValueError: if all((a for a in args if a[-1] == '%' and 100 >= int(a[:-1]) >= 0)): return self._rgbatohex([int(a[:-1]) * 255 / 100.0 for a in args]) raise ValueError('Illegal color values') def rgba(self, *args): """ Translate rgba(...) to color string raises: ValueError returns: str """ if len(args) == 4: try: falpha = float(list(args)[3]) if falpha > 1: args = args[:3] if falpha == 0: values = self._rgbatohex_raw(list(map(int, args))) return "rgba(%s)" % ','.join([str(a) for a in values]) return self._rgbatohex(list(map(int, args))) except ValueError: if all((a for a in args if a[-1] == '%' and 100 >= int(a[:-1]) >= 0)): alpha = list(args)[3] if alpha[-1] == '%' and float(alpha[:-1]) == 0: values = self._rgbatohex_raw([int(a[:-1]) * 255 / 100.0 for a in args]) return "rgba(%s)" % ','.join([str(a) for a in values]) return self._rgbatohex([int(a[:-1]) * 255 / 100.0 for a in args]) raise ValueError('Illegal color values') def argb(self, *args): """ Translate argb(...) to color string Creates a hex representation of a color in #AARRGGBB format (NOT #RRGGBBAA!). This format is used in Internet Explorer, and .NET and Android development. raises: ValueError returns: str """ if len(args) == 1 and type(args[0]) is str: match = re.match(r'rgba\((.*)\)', args[0]) if match: # NOTE(saschpe): Evil hack to cope with rgba(.., .., .., 0.5) passed through untransformed rgb = re.sub(r'\s+', '', match.group(1)).split(',') else: rgb = list(self._hextorgb(args[0])) else: rgb = list(args) if len(rgb) == 3: return self._rgbatohex([255] + list(map(int, rgb))) elif len(rgb) == 4: rgb = [rgb.pop()] + rgb # Move Alpha to front try: fval = float(list(rgb)[0]) if fval > 1: rgb = [255] + rgb[1:] # Clip invalid integer/float values elif 1 >= fval >= 0: rgb = [fval * 256] + rgb[1:] # Convert 0-1 to 0-255 range for _rgbatohex else: rgb = [0] + rgb[1:] # Clip lower bound return self._rgbatohex(list(map(int, rgb))) except ValueError: if all((a for a in rgb if a[-1] == '%' and 100 >= int(a[:-1]) >= 0)): return self._rgbatohex([int(a[:-1]) * 255 / 100.0 for a in rgb]) raise ValueError('Illegal color values') def hsl(self, *args): """ Translate hsl(...) to color string raises: ValueError returns: str """ if len(args) == 4: return self.hsla(*args) elif len(args) == 3: h, s, l = args rgb = colorsys.hls_to_rgb(int(h) / 360.0, utility.pc_or_float(l), utility.pc_or_float(s)) color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') def hsla(self, *args): """ Translate hsla(...) to color string raises: ValueError returns: str """ if len(args) == 4: h, s, l, a = args rgb = colorsys.hls_to_rgb(int(h) / 360.0, utility.pc_or_float(l), utility.pc_or_float(s)) color = [float(utility.convergent_round(c * 255)) for c in rgb] color.append(utility.pc_or_float(a)) return "rgba(%s,%s,%s,%s)" % tuple(color) raise ValueError('Illegal color values') def hue(self, color, *args): """ Return the hue value of a color args: color (str): color raises: ValueError returns: float """ if color: h, l, s = self._hextohls(color) return utility.convergent_round(h * 360.0, 3) raise ValueError('Illegal color values') def saturation(self, color, *args): """ Return the saturation value of a color args: color (str): color raises: ValueError returns: float """ if color: h, l, s = self._hextohls(color) return s * 100.0 raise ValueError('Illegal color values') def lightness(self, color, *args): """ Return the lightness value of a color args: color (str): color raises: ValueError returns: float """ if color: h, l, s = self._hextohls(color) return l * 100.0 raise ValueError('Illegal color values') def opacity(self, *args): """ """ pass def lighten(self, color, diff, *args): """ Lighten a color args: color (str): color diff (str): percentage returns: str """ if color and diff: return self._ophsl(color, diff, 1, operator.add) raise ValueError('Illegal color values') def darken(self, color, diff, *args): """ Darken a color args: color (str): color diff (str): percentage returns: str """ if color and diff: return self._ophsl(color, diff, 1, operator.sub) raise ValueError('Illegal color values') def saturate(self, color, diff, *args): """ Saturate a color args: color (str): color diff (str): percentage returns: str """ if color and diff: return self._ophsl(color, diff, 2, operator.add) raise ValueError('Illegal color values') def desaturate(self, color, diff, *args): """ Desaturate a color args: color (str): color diff (str): percentage returns: str """ if color and diff: return self._ophsl(color, diff, 2, operator.sub) raise ValueError('Illegal color values') def _clamp(self, value): # Clamp value return min(1, max(0, value)) def greyscale(self, color, *args): """ Simply 100% desaturate. args: color (str): color returns: str """ if color: return self.desaturate(color, 100.0) raise ValueError('Illegal color values') def grayscale(self, color, *args): """Wrapper for greyscale, other spelling """ return self.greyscale(color, *args) def spin(self, color, degree, *args): """ Spin color by degree. (Increase / decrease hue) args: color (str): color degree (str): percentage raises: ValueError returns: str """ if color and degree: if isinstance(degree, six.string_types): degree = float(degree.strip('%')) h, l, s = self._hextohls(color) h = ((h * 360.0) + degree) % 360.0 h = 360.0 + h if h < 0 else h rgb = colorsys.hls_to_rgb(h / 360.0, l, s) color = (utility.convergent_round(c * 255) for c in rgb) return self._rgbatohex(color) raise ValueError('Illegal color values') def mix(self, color1, color2, weight=50, *args): """This algorithm factors in both the user-provided weight and the difference between the alpha values of the two colors to decide how to perform the weighted average of the two RGB values. It works by first normalizing both parameters to be within [-1, 1], where 1 indicates "only use color1", -1 indicates "only use color 0", and all values in between indicated a proportionately weighted average. Once we have the normalized variables w and a, we apply the formula (w + a)/(1 + w*a) to get the combined weight (in [-1, 1]) of color1. This formula has two especially nice properties: * When either w or a are -1 or 1, the combined weight is also that number (cases where w * a == -1 are undefined, and handled as a special case). * When a is 0, the combined weight is w, and vice versa Finally, the weight of color1 is renormalized to be within [0, 1] and the weight of color2 is given by 1 minus the weight of color1. Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein http://sass-lang.com args: color1 (str): first color color2 (str): second color weight (int/str): weight raises: ValueError returns: str """ if color1 and color2: if isinstance(weight, six.string_types): weight = float(weight.strip('%')) weight = ((weight / 100.0) * 2) - 1 rgb1 = self._hextorgb(color1) rgb2 = self._hextorgb(color2) alpha = 0 w1 = (((weight if weight * alpha == -1 else weight + alpha) / (1 + weight * alpha)) + 1) w1 = w1 / 2.0 w2 = 1 - w1 rgb = [ rgb1[0] * w1 + rgb2[0] * w2, rgb1[1] * w1 + rgb2[1] * w2, rgb1[2] * w1 + rgb2[2] * w2, ] return self._rgbatohex(rgb) raise ValueError('Illegal color values') def fmt(self, color): """ Format CSS Hex color code. uppercase becomes lowercase, 3 digit codes expand to 6 digit. args: color (str): color raises: ValueError returns: str """ if utility.is_color(color): color = color.lower().strip('#') if len(color) in [3, 4]: color = ''.join([c * 2 for c in color]) return '#%s' % color raise ValueError('Cannot format non-color') def _rgbatohex_raw(self, rgba): values = ["%x" % v for v in [0xff if h > 0xff else 0 if h < 0 else h for h in rgba]] return values def _rgbatohex(self, rgba): return '#%s' % ''.join(["%02x" % v for v in [0xff if h > 0xff else 0 if h < 0 else h for h in rgba] ]) def _hextorgb(self, hex): if hex.lower() in colors.lessColors: hex = colors.lessColors[hex.lower()] hex = hex.strip() if hex[0] == '#': hex = hex.strip('#').strip(';') if len(hex) == 3: hex = [c * 2 for c in hex] else: hex = [hex[i:i + 2] for i in range(0, len(hex), 2)] return tuple(int(c, 16) for c in hex) try: return [int(hex, 16)] * 3 except: return [float(hex)] * 3 def _hextohls(self, hex): rgb = self._hextorgb(hex) return colorsys.rgb_to_hls(*[c / 255.0 for c in rgb]) def _ophsl(self, color, diff, idx, operation): if isinstance(diff, six.string_types): diff = float(diff.strip('%')) hls = list(self._hextohls(color)) hls[idx] = self._clamp(operation(hls[idx], diff / 100.0)) rgb = colorsys.hls_to_rgb(*hls) color = (utility.away_from_zero_round(c * 255) for c in rgb) return self._rgbatohex(color) PKPqH-U,lesscpy/lessc/scope.py""" .. module:: lesscpy.lessc.scope :synopsis: Scope class. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import six from . import utility class Scope(list): """ Scope class. A stack implementation. """ def __init__(self, init=False): """Scope Args: init (bool): Initiate scope """ super(Scope, self).__init__() self._mixins = {} if init: self.push() self.deferred = False self.real = [] def push(self): """Push level on scope """ self.append({ '__variables__': {}, '__blocks__': [], '__names__': [], '__current__': None }) @property def current(self): return self[-1]['__current__'] @current.setter def current(self, value): self[-1]['__current__'] = value @property def scopename(self): """Current scope name as list Returns: list """ return [r['__current__'] for r in self if r['__current__']] def add_block(self, block): """Add block element to scope Args: block (Block): Block object """ self[-1]['__blocks__'].append(block) self[-1]['__names__'].append(block.raw()) def remove_block(self, block, index="-1"): """Remove block element from scope Args: block (Block): Block object """ self[index]["__blocks__"].remove(block) self[index]["__names__"].remove(block.raw()) def add_mixin(self, mixin): """Add mixin to scope Args: mixin (Mixin): Mixin object """ raw = mixin.tokens[0][0].raw() if raw in self._mixins: self._mixins[raw].append(mixin) else: self._mixins[raw] = [mixin] def add_variable(self, variable): """Add variable to scope Args: variable (Variable): Variable object """ self[-1]['__variables__'][variable.name] = variable def variables(self, name): """Search for variable by name. Searches scope top down Args: name (string): Search term Returns: Variable object OR False """ if isinstance(name, tuple): name = name[0] if name.startswith('@{'): name = '@' + name[2:-1] i = len(self) while i >= 0: i -= 1 if name in self[i]['__variables__']: return self[i]['__variables__'][name] return False def mixins(self, name): """ Search mixins for name. Allow '>' to be ignored. '.a .b()' == '.a > .b()' Args: name (string): Search term Returns: Mixin object list OR False """ m = self._smixins(name) if m: return m return self._smixins(name.replace('?>?', ' ')) def _smixins(self, name): """Inner wrapper to search for mixins by name. """ return (self._mixins[name] if name in self._mixins else False) def blocks(self, name): """ Search for defined blocks recursively. Allow '>' to be ignored. '.a .b' == '.a > .b' Args: name (string): Search term Returns: Block object OR False """ b = self._blocks(name) if b: return b return self._blocks(name.replace('?>?', ' ')) def _blocks(self, name): """Inner wrapper to search for blocks by name. """ i = len(self) while i >= 0: i -= 1 if name in self[i]['__names__']: for b in self[i]['__blocks__']: r = b.raw() if r and r == name: return b else: for b in self[i]['__blocks__']: r = b.raw() if r and name.startswith(r): b = utility.blocksearch(b, name) if b: return b return False def update(self, scope, at=0): """Update scope. Add another scope to this one. Args: scope (Scope): Scope object Kwargs: at (int): Level to update """ if hasattr(scope, '_mixins') and not at: self._mixins.update(scope._mixins) self[at]['__variables__'].update(scope[at]['__variables__']) self[at]['__blocks__'].extend(scope[at]['__blocks__']) self[at]['__names__'].extend(scope[at]['__names__']) def swap(self, name): """ Swap variable name for variable value Args: name (str): Variable name Returns: Variable value (Mixed) """ if name.startswith('@@'): var = self.variables(name[1:]) if var is False: raise SyntaxError('Unknown variable %s' % name) name = '@' + utility.destring(var.value[0]) var = self.variables(name) if var is False: raise SyntaxError('Unknown variable %s' % name) elif name.startswith('@{'): var = self.variables('@' + name[2:-1]) if var is False: raise SyntaxError('Unknown escaped variable %s' % name) if isinstance(var.value[0], six.string_types): var.value[0] = utility.destring(var.value[0]) else: var = self.variables(name) if var is False: raise SyntaxError('Unknown variable %s' % name) return var.value PKf C›BBlesscpy/lessc/formatter.py# -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.formatter :synopsis: CSS Formatter class. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ class Formatter(object): def __init__(self, args): self.args = args def format(self, parse): """ """ if not parse.result: return '' eb = '\n' if self.args.xminify: eb = '' self.args.minify = True self.items = {} if self.args.minify: self.items.update({ 'nl': '', 'tab': '', 'ws': '', 'eb': eb }) else: tab = '\t' if self.args.tabs else ' ' * int(self.args.spaces) self.items.update({ 'nl': '\n', 'tab': tab, 'ws': ' ', 'eb': eb }) self.out = [u.fmt(self.items) for u in parse.result if u] return ''.join(self.out).strip() PKf C$үbblesscpy/lessc/__init__.py""" Main lesscss parse library. Contains lexer and parser, along with utility classes """ PKPqH$lesscpy/lessc/parser.py# -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.parser :synopsis: Lesscss parser. http://www.dabeaz.com/ply/ply.html http://www.w3.org/TR/CSS21/grammar.html#scanner http://lesscss.org/#docs Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from __future__ import print_function import os import tempfile import sys import ply.yacc import six from . import lexer from . import utility from .scope import Scope from .color import Color from lesscpy.exceptions import CompilationError from lesscpy.plib import Block, Call, Deferred, Expression, Identifier, Mixin, NegatedExpression, Property, Statement, Variable, Import, KeyframeSelector class ErrorRegister(object): """ Raises CompilationError when an error occurs. """ def __init__(self): self.errors = [] def register(self, error): self.errors.append(error) # we could store them or just raise here. def __close__(self): if self.errors: raise CompilationError("\n".join(self.errors)) close = __close__ class PrintErrorRegister(object): """ Colored error output to stderr. """ def __init__(self): self.has_errored = False def register(self, error): self.has_errored = True color = '\x1b[31m' if error[0] == 'E' else '\x1b[33m' print("%s%s\x1b[0m" % (color, error), end='\x1b[0m', file=sys.stderr) def __close__(self): pass close = __close__ class LessParser(object): precedence = ( ('left', '+', '-'), ('left', '*', '/'), ) def __init__(self, lex_optimize=True, yacc_optimize=True, tabfile='yacctab', yacc_debug=False, scope=None, outputdir=tempfile.gettempdir(), importlvl=0, verbose=False, fail_with_exc=False ): """ Parser object Kwargs: lex_optimize (bool): Optimize lexer yacc_optimize (bool): Optimize parser tabfile (str): Yacc tab filename yacc_debug (bool): yacc debug mode scope (Scope): Inherited scope outputdir (str): Output (debugging) importlvl (int): Import depth verbose (bool): Verbose mode fail_with_exc (bool): Throw exception on syntax error instead of printing to stderr """ self.verbose = verbose self.importlvl = importlvl self.lex = lexer.LessLexer() if not tabfile: tabfile = 'yacctab' self.ignored = ('css_comment', 'less_comment', 'css_vendor_hack') self.tokens = [t for t in self.lex.tokens if t not in self.ignored] self.parser = ply.yacc.yacc( module=self, start='tunit', debug=yacc_debug, optimize=yacc_optimize, tabmodule=tabfile, outputdir=outputdir ) self.scope = scope if scope else Scope() self.stash = {} self.result = None self.target = None self.fail_with_exc = fail_with_exc if fail_with_exc: self.register = ErrorRegister() else: self.register = PrintErrorRegister() def parse(self, filename=None, file=None, debuglevel=0): """ Parse file. kwargs: filename (str): File to parse debuglevel (int): Parser debuglevel """ self.scope.push() if not file: # We use a path. file = filename else: # We use a stream and try to extract the name from the stream. if hasattr(file, 'name'): if filename is not None: raise AssertionError( 'names of file and filename are in conflict') filename = file.name else: filename = '(stream)' self.target = filename if self.verbose and not self.fail_with_exc: print('Compiling target: %s' % filename, file=sys.stderr) self.result = self.parser.parse( file, lexer=self.lex, debug=debuglevel) self.post_parse() self.register.close() def post_parse(self): """ Post parse cycle. nodejs version allows calls to mixins not yet defined or known to the parser. We defer all calls to mixins until after first cycle when all names are known. """ if self.result: out = [] for pu in self.result: try: out.append(pu.parse(self.scope)) except SyntaxError as e: self.handle_error(e, 0) self.result = list(utility.flatten(out)) def scopemap(self): """ Output scopemap. """ utility.debug_print(self.result) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_tunit(self, p): """ tunit : unit_list """ p[0] = [u for u in p[1] if u] def p_unit_list(self, p): """ unit_list : unit_list unit | unit """ if isinstance(p[1], list): if len(p) >= 3: if isinstance(p[2], list): p[1].extend(p[2]) else: p[1].append(p[2]) else: p[1] = [p[1]] p[0] = p[1] def p_unit(self, p): """ unit : statement | variable_decl | block_decl | mixin_decl | call_mixin | import_statement """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_statement_aux(self, p): """ statement : css_charset t_ws css_string t_semicolon | css_namespace t_ws css_string t_semicolon """ p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) def p_statement_namespace(self, p): """ statement : css_namespace t_ws word css_string t_semicolon """ p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) def p_statement_import(self, p): """ import_statement : css_import t_ws string t_semicolon | css_import t_ws css_string t_semicolon | css_import t_ws css_string media_query_list t_semicolon | css_import t_ws fcall t_semicolon | css_import t_ws fcall media_query_list t_semicolon """ #import pdb; pdb.set_trace() if self.importlvl > 8: raise ImportError( 'Recrusive import level too deep > 8 (circular import ?)') if isinstance(p[3], six.string_types): ipath = utility.destring(p[3]) elif isinstance(p[3], list): p[3] = Import(p[3], p.lineno(4)).parse(self.scope) ipath = utility.destring(p[3]) elif isinstance(p[3], Call): # NOTE(saschpe): Always in the form of 'url("...");', so parse it # and retrieve the inner css_string. This whole func is messy. p[3] = p[3].parse(self.scope) # Store it as string, Statement.fmt expects it. ipath = utility.destring(p[3][4:-1]) fn, fe = os.path.splitext(ipath) if not fe or fe.lower() == '.less': try: cpath = os.path.dirname(os.path.abspath(self.target)) if not fe: ipath += '.less' filename = "%s%s%s" % (cpath, os.sep, ipath) if os.path.exists(filename): recurse = LessParser(importlvl=self.importlvl + 1, verbose=self.verbose, scope=self.scope) recurse.parse(filename=filename, debuglevel=0) p[0] = recurse.result else: err = "Cannot import '%s', file not found" % filename self.handle_error(err, p.lineno(1), 'W') p[0] = None except ImportError as e: self.handle_error(e, p) else: p[0] = Statement(list(p)[1:], p.lineno(1)) p[0].parse(None) sys.stdout.flush() # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_block(self, p): """ block_decl : block_open declaration_list brace_close """ p[0] = Block(list(p)[1:-1], p.lineno(3)) self.scope.pop() self.scope.add_block(p[0]) def p_block_replace(self, p): """ block_decl : identifier t_semicolon """ m = p[1].parse(None) block = self.scope.blocks(m.raw()) if block: p[0] = block.copy_inner(self.scope) else: # fallback to mixin. Allow calls to mixins without parens p[0] = Deferred(p[1], None, p.lineno(2)) def p_block_open(self, p): """ block_open : identifier brace_open """ try: p[1].parse(self.scope) except SyntaxError: pass p[0] = p[1] self.scope.current = p[1] def p_block_open_media_query(self, p): """ block_open : media_query_decl brace_open """ p[0] = Identifier(p[1]).parse(self.scope) def p_font_face_open(self, p): """ block_open : css_font_face t_ws brace_open """ p[0] = Identifier([p[1], p[2]]).parse(self.scope) def p_keyframe_open(self, p): """block_open : css_keyframe_selector brace_open | number brace_open """ p[0] = KeyframeSelector([p[1]]).parse(self.scope) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_mixin(self, p): """ mixin_decl : open_mixin declaration_list brace_close """ self.scope.add_mixin(Mixin(list(p)[1:], p.lineno(3)).parse(self.scope)) self.scope.pop() p[0] = None def p_open_mixin(self, p): """ open_mixin : identifier t_popen mixin_args_list t_pclose brace_open | identifier t_popen mixin_args_list t_pclose mixin_guard brace_open """ p[1].parse(self.scope) self.scope.current = p[1] p[0] = [p[1], p[3]] if len(p) > 6: p[0].append(p[5]) else: p[0].append(None) def p_mixin_guard(self, p): """ mixin_guard : less_when mixin_guard_cond_list """ p[0] = p[2] def p_mixin_guard_cond_list_aux(self, p): """ mixin_guard_cond_list : mixin_guard_cond_list t_comma mixin_guard_cond | mixin_guard_cond_list less_and mixin_guard_cond """ p[1].append(p[2]) p[1].append(p[3]) p[0] = p[1] def p_mixin_guard_cond_list(self, p): """ mixin_guard_cond_list : mixin_guard_cond """ p[0] = [p[1]] def p_mixin_guard_cond_rev(self, p): """ mixin_guard_cond : less_not t_popen argument mixin_guard_cmp argument t_pclose | less_not t_popen argument t_pclose """ p[0] = utility.reverse_guard(list(p)[3:-1]) def p_mixin_guard_cond(self, p): """ mixin_guard_cond : t_popen argument mixin_guard_cmp argument t_pclose | t_popen argument t_pclose """ p[0] = list(p)[2:-1] def p_mixin_guard_cmp(self, p): """ mixin_guard_cmp : '>' | '<' | '=' | '>' '=' | '=' '<' """ p[0] = ''.join(list(p)[1:]) def p_call_mixin(self, p): """ call_mixin : identifier t_popen mixin_args_list t_pclose t_semicolon """ p[1].parse(None) p[0] = Deferred(p[1], p[3], p.lineno(4)) def p_mixin_args_arguments(self, p): """ mixin_args_list : less_arguments """ p[0] = [p[1]] def p_mixin_args_list_aux(self, p): """ mixin_args_list : mixin_args_list t_comma mixin_args | mixin_args_list t_semicolon mixin_args """ p[1].extend([p[3]]) p[0] = p[1] def p_mixin_args_list(self, p): """ mixin_args_list : mixin_args """ p[0] = [p[1]] def p_mixin_args_aux(self, p): """ mixin_args : mixin_args argument """ p[1].extend(list(p)[2:]) p[0] = p[1] def p_mixin_args(self, p): """ mixin_args : argument | mixin_kwarg """ p[0] = [p[1]] def p_mixin_args_empty(self, p): """ mixin_args : empty """ p[0] = None def p_mixin_kwarg(self, p): """ mixin_kwarg : variable t_colon mixin_kwarg_arg_list """ p[0] = Variable(list(p)[1:], p.lineno(2)) def p_margument_list_aux(self, p): """ mixin_kwarg_arg_list : mixin_kwarg_arg_list argument """ p[1].extend(list(p)[2:]) p[0] = p[1] def p_margument_list(self, p): """ mixin_kwarg_arg_list : argument """ p[0] = [p[1]] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_declaration_list(self, p): """ declaration_list : declaration_list declaration | declaration | empty """ if len(p) > 2: p[1].extend(p[2]) p[0] = p[1] def p_declaration(self, p): """ declaration : variable_decl | property_decl | block_decl | mixin_decl | call_mixin | import_statement """ p[0] = p[1] if isinstance(p[1], list) else [p[1]] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_variable_decl(self, p): """ variable_decl : variable t_colon style_list t_semicolon """ p[0] = Variable(list(p)[1:-1], p.lineno(4)) p[0].parse(self.scope) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_property_decl(self, p): """ property_decl : prop_open style_list t_semicolon | prop_open style_list css_important t_semicolon | prop_open empty t_semicolon """ l = len(p) p[0] = Property(list(p)[1:-1], p.lineno(l - 1)) def p_property_decl_arguments(self, p): """ property_decl : prop_open less_arguments t_semicolon """ p[0] = Property([p[1], [p[2]]], p.lineno(3)) def p_prop_open_ie_hack(self, p): """ prop_open : '*' prop_open """ p[0] = (p[1][0], p[2][0]) def p_prop_open(self, p): """ prop_open : property t_colon | vendor_property t_colon | word t_colon """ p[0] = (p[1][0], '') # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_style_list_aux(self, p): """ style_list : style_list style | style_list t_comma style | style_list t_ws style """ p[1].extend(list(p)[2:]) p[0] = p[1] def p_style_list(self, p): """ style_list : style """ p[0] = [p[1]] def p_style(self, p): """ style : expression | string | word | property | vendor_property | estring """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_identifier(self, p): """ identifier : identifier_list | page | page filter """ p[0] = Identifier(p[1], 0) def p_identifier_istr(self, p): """ identifier : t_popen estring t_pclose """ p[0] = Identifier(Call([p[2], p[3]]), 0) def p_identifier_list_aux(self, p): """ identifier_list : identifier_list t_comma identifier_group """ p[1].extend([p[2]]) p[1].extend(p[3]) p[0] = p[1] def p_identifier_list(self, p): """ identifier_list : identifier_group """ p[0] = p[1] def p_identifier_list_keyframe(self, p): """ identifier_list : css_keyframes t_ws css_ident | css_keyframes t_ws css_ident t_ws """ p[0] = list(p)[1:] def p_identifier_list_viewport(self, p): """ identifier_list : css_viewport | css_viewport t_ws """ p[0] = list(p)[1:] def p_identifier_group_op(self, p): """ identifier_group : identifier_group child_selector ident_parts | identifier_group '+' ident_parts | identifier_group general_sibling_selector ident_parts | identifier_group '*' """ p[1].extend([p[2]]) if len(p) > 3: p[1].extend(p[3]) p[0] = p[1] def p_identifier_group(self, p): """ identifier_group : ident_parts """ p[0] = p[1] def p_ident_parts_aux(self, p): """ ident_parts : ident_parts ident_part | ident_parts filter_group """ if isinstance(p[2], list): p[1].extend(p[2]) else: p[1].append(p[2]) p[0] = p[1] def p_ident_parts(self, p): """ ident_parts : ident_part | selector | filter_group """ if not isinstance(p[1], list): p[1] = [p[1]] p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_media_query_decl(self, p): """ media_query_decl : css_media t_ws | css_media t_ws media_query_list """ p[0] = list(p)[1:] def p_media_query_list_aux(self, p): """ media_query_list : media_query_list t_comma media_query """ p[0] = list(p)[1:] def p_media_query_list(self, p): """ media_query_list : media_query """ p[0] = [p[1]] def p_media_query_a(self, p): """ media_query : media_type | media_type media_query_expression_list | not media_type | not media_type media_query_expression_list | only media_type | only media_type media_query_expression_list """ p[0] = list(p)[1:] def p_media_query_b(self, p): """ media_query : media_query_expression media_query_expression_list | media_query_expression """ p[0] = list(p)[1:] def p_media_query_expression_list_aux(self, p): """ media_query_expression_list : media_query_expression_list and media_query_expression | and media_query_expression """ p[0] = list(p)[1:] def p_media_query_expression(self, p): """ media_query_expression : t_popen css_media_feature t_pclose | t_popen css_media_feature t_colon media_query_value t_pclose """ p[0] = list(p)[1:] def p_media_query_value(self, p): """ media_query_value : number | variable | word | color | expression """ if utility.is_variable(p[1]): var = self.scope.variables(''.join(p[1])) if var: value = var.value[0] if hasattr(value, 'parse'): p[1] = value.parse(self.scope) else: p[1] = value if isinstance(p[1], Expression): p[0] = p[1].parse(self.scope) else: p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_selector(self, p): """ selector : '*' | '+' | child_selector | general_sibling_selector """ p[0] = p[1] def p_ident_part(self, p): """ ident_part : iclass | id | dom | combinator | color """ p[0] = p[1] def p_ident_part_aux(self, p): """ ident_part : combinator vendor_property """ p[0] = [p[1], p[2]] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_filter_group_aux(self, p): """ filter_group : filter_group filter """ p[1].extend(p[2]) p[0] = p[1] def p_filter_group(self, p): """ filter_group : filter """ p[0] = p[1] def p_filter(self, p): """ filter : css_filter | css_filter t_ws | t_colon word | t_colon vendor_property | t_colon vendor_property t_ws | t_colon css_property | t_colon css_property t_ws | t_colon css_filter | t_colon css_filter t_ws | t_colon t_colon word | t_colon t_colon vendor_property """ p[0] = list(p)[1:] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_ms_filter(self, p): """ ms_filter : css_ms_filter | css_ms_filter t_ws """ p[0] = tuple(list(p)[1:]) def p_fcall(self, p): """ fcall : word t_popen argument_list t_pclose | property t_popen argument_list t_pclose | vendor_property t_popen argument_list t_pclose | less_open_format argument_list t_pclose | ms_filter t_popen argument_list t_pclose """ p[0] = Call(list(p)[1:], 0) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_argument_list_empty(self, p): """ argument_list : empty """ p[0] = '' def p_argument_list_aux(self, p): """ argument_list : argument_list argument | argument_list t_comma argument """ p[1].extend(list(p)[2:]) p[0] = p[1] def p_argument_list(self, p): """ argument_list : argument """ p[0] = [p[1]] def p_argument(self, p): """ argument : expression | string | estring | word | id | css_uri | '=' | fcall """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_expression_aux(self, p): """ expression : expression '+' expression | expression '-' expression | expression '/' expression | expression '*' expression | word '/' expression """ p[0] = Expression(list(p)[1:], 0) def p_expression_p_neg(self, p): """ expression : '-' t_popen expression t_pclose """ p[0] = NegatedExpression([p[3]], 0) def p_expression_p(self, p): """ expression : t_popen expression t_pclose """ p[0] = p[2] def p_expression(self, p): """ expression : factor """ p[0] = p[1] def p_factor(self, p): """ factor : color | number | variable | css_dom | fcall """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_escaped_string(self, p): """ estring : t_eopen style_list t_eclose | t_eopen identifier_list t_eclose """ p[0] = p[2] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_string_part(self, p): """ string_part : variable | css_string """ p[0] = p[1] def p_string_part_list_aux(self, p): """ string_part_list : string_part_list string_part """ p[1].extend([p[2]]) p[0] = p[1] def p_string_part_list(self, p): """ string_part_list : string_part """ p[0] = [p[1]] def p_string_aux(self, p): """ string : t_isopen string_part_list t_isclose """ p[0] = ['"', p[2], '"'] def p_string(self, p): """ string : css_string """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_variable_neg(self, p): """ variable : '-' variable """ p[0] = ['-', p[2]] def p_variable_strange(self, p): """ variable : t_popen variable t_pclose """ p[0] = p[2] def p_variable(self, p): """ variable : less_variable | less_variable t_ws """ # p[0] = p[1] p[0] = tuple(list(p)[1:]) def p_color(self, p): """ color : css_color | css_color t_ws """ try: p[0] = Color().fmt(p[1]) if len(p) > 2: p[0] = [p[0], p[2]] except ValueError: self.handle_error( 'Illegal color value `%s`' % p[1], p.lineno(1), 'W') p[0] = p[1] def p_number(self, p): """ number : css_number | css_number t_ws """ p[0] = tuple(list(p)[1:]) def p_dom(self, p): """ dom : css_dom | css_dom t_ws """ p[0] = tuple(list(p)[1:]) def p_word(self, p): """ word : css_ident | css_ident t_ws """ p[0] = tuple(list(p)[1:]) # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_class(self, p): """ class : css_class | css_class t_ws """ p[0] = tuple(list(p)[1:]) def p_interpolated_class_part(self, p): """ iclass_part : less_variable | less_variable t_ws | class """ p[0] = list(p)[1:] def p_interpolated_class_part_list_aux(self, p): """ iclass_part_list : iclass_part_list iclass_part """ p[1].extend([p[2]]) p[0] = p[1] def p_interpolated_class_part_list(self, p): """ iclass_part_list : iclass_part """ p[0] = [p[1]] def p_interpolated_class(self, p): """ iclass : iclass_part_list """ p[0] = p[1] # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_id(self, p): """ id : css_id | css_id t_ws """ p[0] = tuple(list(p)[1:]) def p_property(self, p): """ property : css_property | css_property t_ws """ p[0] = tuple(list(p)[1:]) def p_page(self, p): """ page : css_page | css_page t_ws """ p[0] = tuple(list(p)[1:]) def p_vendor_property(self, p): """ vendor_property : css_vendor_property | css_vendor_property t_ws """ p[0] = tuple(list(p)[1:]) def p_media_type(self, p): """ media_type : css_media_type | css_media_type t_ws """ p[0] = tuple(list(p)[1:]) def p_combinator(self, p): """ combinator : '&' t_ws | '&' """ p[0] = tuple(list(p)[1:]) def p_child_selector(self, p): """ child_selector : '>' t_ws | '>' """ p[0] = tuple(list(p)[1:]) def p_general_sibling_selector(self, p): """ general_sibling_selector : t_tilde t_ws | t_tilde """ p[0] = tuple(list(p)[1:]) def p_scope_open(self, p): """ brace_open : t_bopen """ self.scope.push() p[0] = p[1] def p_scope_close(self, p): """ brace_close : t_bclose """ p[0] = p[1] def p_and(self, p): """ and : t_and t_ws | t_and """ p[0] = tuple(list(p)[1:]) def p_not(self, p): """ not : t_not t_ws | t_not """ p[0] = tuple(list(p)[1:]) def p_only(self, p): """ only : t_only t_ws | t_only """ p[0] = tuple(list(p)[1:]) def p_empty(self, p): 'empty :' pass # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # def p_error(self, t): """ Internal error handler args: t (Lex token): Error token """ if t: error_msg = "E: %s line: %d, Syntax Error, token: `%s`, `%s`" % \ (self.target, t.lineno, t.type, t.value) self.register.register(error_msg) while True: t = self.lex.token() if not t or t.value == '}': if len(self.scope) > 1: self.scope.pop() break self.parser.restart() return t def handle_error(self, e, line, t='E'): """ Custom error handler args: e (Mixed): Exception or str line (int): line number t(str): Error type """ self.register.register("%s: line: %d: %s\n" % (t, line, e)) PKPqH44lesscpy/lessc/lexer.py""" Lexer for LESSCSS. http://www.dabeaz.com/ply/ply.html http://www.w3.org/TR/CSS21/grammar.html#scanner http://lesscss.org/#docs Copyright (c) See LICENSE for details. """ import re import ply.lex as lex from six import string_types from lesscpy.lib import dom from lesscpy.lib import css from lesscpy.lib import reserved class LessLexer: states = ( ('parn', 'inclusive'), ('escapequotes', 'inclusive'), ('escapeapostrophe', 'inclusive'), ('istringquotes', 'inclusive'), ('istringapostrophe', 'inclusive'), ('iselector', 'inclusive'), ('mediaquery', 'inclusive'), ('import', 'inclusive'), ) literals = '<>=%!/*-+&' tokens = [ 'css_ident', 'css_dom', 'css_class', 'css_id', 'css_property', 'css_vendor_property', 'css_comment', 'css_string', 'css_color', 'css_filter', 'css_number', 'css_important', 'css_vendor_hack', 'css_uri', 'css_ms_filter', 'css_keyframe_selector', 'css_media_type', 'css_media_feature', 't_and', 't_not', 't_only', 'less_variable', 'less_comment', 'less_open_format', 'less_when', 'less_and', 'less_not', 't_ws', 't_popen', 't_pclose', 't_semicolon', 't_tilde', 't_colon', 't_comma', 't_eopen', 't_eclose', 't_isopen', 't_isclose', 't_bopen', 't_bclose' ] tokens += list(set(reserved.tokens.values())) # Tokens with significant following whitespace significant_ws = set([ 'css_class', 'css_id', 'css_dom', 'css_property', 'css_vendor_property', 'css_ident', 'css_number', 'css_color', 'css_media_type', 'css_filter', 'less_variable', 't_and', 't_not', 't_only', '&', ]) significant_ws.update(reserved.tokens.values()) def __init__(self): self.build(reflags=re.UNICODE | re.IGNORECASE) self.last = None self.next_ = None self.pretok = True def t_css_filter(self, t): (r'\[[^\]]*\]' '|(not|lang|nth-[a-z\-]+)\(.+\)' '|and[ \t]\([^><=\{]+\)') return t def t_css_ms_filter(self, t): r'(?:progid:|DX\.)[^;\(]*' return t def t_t_bopen(self, t): r'\{' t.lexer.in_property_decl = False return t def t_t_bclose(self, t): r'\}' return t def t_t_colon(self, t): r':' return t def t_t_comma(self, t): r',' t.lexer.in_property_decl = False return t def t_css_number(self, t): r'-?(\d*\.\d+|\d+)(s|%|in|ex|[ecm]m|p[txc]|deg|g?rad|ms?|k?hz|dpi|dpcm|dppx)?' return t def t_css_ident(self, t): (r'([\-\.\#]?' '([_a-z]' '|[\200-\377]' '|\\\[0-9a-f]{1,6}' '|\\\[^\s\r\n0-9a-f])' '([_a-z0-9\-]' '|[\200-\377]' '|\\\[0-9a-f]{1,6}' '|\\\[^\s\r\n0-9a-f])*)' '|\.') v = t.value.strip() c = v[0] if c == '.': # In some cases, only the '.' can be marked as CSS class. # # Example: .@{name} # t.type = 'css_class' if t.lexer.lexstate != "iselector": # Selector-chaining case (a.b.c), we are already in state 'iselector' t.lexer.push_state("iselector") elif c == '#': t.type = 'css_id' if len(v) in [4, 7]: try: int(v[1:], 16) t.type = 'css_color' except ValueError: pass elif v == 'when': t.type = 'less_when' elif v == 'and': t.type = 'less_and' elif v == 'not': t.type = 'less_not' elif v in ('from', 'to'): t.type = 'css_keyframe_selector' elif v in css.propertys: t.type = 'css_property' t.lexer.in_property_decl = True elif (v in dom.elements or v.lower() in dom.elements) and not t.lexer.in_property_decl: # DOM elements can't be part of property declarations, avoids ambiguity between 'rect' DOM # element and rect() CSS function. t.type = 'css_dom' elif c == '-': t.type = 'css_vendor_property' t.lexer.in_property_decl = True t.value = v return t def t_iselector_less_variable(self, t): r'@\{[^@\}]+\}' return t def t_iselector_t_eclose(self, t): r'"|\'' # Can only happen if iselector state is on top of estring state. # # Example: @item: ~".col-xs-@{index}"; # t.lexer.pop_state() return t def t_iselector_css_filter(self, t): (r'\[[^\]]*\]' '|(not|lang|nth-[a-z\-]+)\(.+\)' '|and[ \t]\([^><\{]+\)') # TODO/FIXME(saschpe): Only needs to be redifined in state 'iselector' so that # the following css_class doesn't catch everything. return t def t_iselector_css_class(self, t): r'[_a-z0-9\-]+' # The first part of CSS class was tokenized by t_css_ident() already. # Here we gather up the any LESS variable. # # Example: .span_@{num}_small # return t def t_iselector_t_ws(self, t): r'[ \t\f\v]+' # # Example: .span_@{num} # t.lexer.pop_state() t.value = ' ' return t def t_iselector_t_bopen(self, t): r'\{' t.lexer.pop_state() return t def t_iselector_t_colon(self, t): r':' t.lexer.pop_state() return t def t_mediaquery_t_not(self, t): r'not' return t def t_mediaquery_t_only(self, t): r'only' return t def t_mediaquery_t_and(self, t): r'and' return t def t_mediaquery_t_popen(self, t): r'\(' # Redefine global t_popen to avoid pushing state 'parn' return t @lex.TOKEN('|'.join(css.media_types)) def t_mediaquery_css_media_type(self, t): return t @lex.TOKEN('|'.join(css.media_features)) def t_mediaquery_css_media_feature(self, t): return t def t_mediaquery_t_bopen(self, t): r'\{' t.lexer.pop_state() return t def t_mediaquery_t_semicolon(self, t): r';' # This can happen only as part of a CSS import statement. The # "mediaquery" state is reused there. Ordinary media queries always # end at '{', i.e. when a block is opened. t.lexer.pop_state() # state mediaquery # We have to pop the 'import' state here because we already ate the # t_semicolon and won't trigger t_import_t_semicolon. t.lexer.pop_state() # state import return t @lex.TOKEN('|'.join(css.media_types)) def t_import_css_media_type(self, t): # Example: @import url("bar.css") handheld and (max-width: 500px); # Alternatively, we could use a lookahead "if not ';'" after the URL # part of the @import statement... t.lexer.push_state("mediaquery") return t def t_import_t_semicolon(self, t): r';' t.lexer.pop_state() return t def t_less_variable(self, t): r'@@?[\w-]+|@\{[^@\}]+\}' v = t.value.lower() if v in reserved.tokens: t.type = reserved.tokens[v] if t.type == "css_media": t.lexer.push_state("mediaquery") elif t.type == "css_import": t.lexer.push_state("import") return t def t_css_color(self, t): r'\#[0-9]([0-9a-f]{5}|[0-9a-f]{2})' return t def t_parn_css_uri(self, t): (r'data:[^\)]+' '|(([a-z]+://)?' '(' '(/?[\.a-z:]+[\w\.:]*[\\/][\\/]?)+' '|([a-z][\w\.\-]+(\.[a-z0-9]+))' '(\#[a-z]+)?)' ')+') return t def t_parn_css_ident(self, t): (r'(([_a-z]' '|[\200-\377]' '|\\\[0-9a-f]{1,6}' '|\\\[^\r\n\s0-9a-f])' '([_a-z0-9\-]|[\200-\377]' '|\\\[0-9a-f]{1,6}' '|\\\[^\r\n\s0-9a-f])*)') return t def t_newline(self, t): r'[\n\r]+' t.lexer.lineno += t.value.count('\n') def t_css_comment(self, t): r'(/\*(.|\n|\r)*?\*/)' t.lexer.lineno += t.value.count('\n') pass def t_less_comment(self, t): r'//.*' pass def t_css_important(self, t): r'!\s*important' t.value = '!important' return t def t_t_ws(self, t): r'[ \t\f\v]+' t.value = ' ' return t def t_t_popen(self, t): r'\(' t.lexer.push_state('parn') return t def t_less_open_format(self, t): r'%\(' t.lexer.push_state('parn') return t def t_parn_t_pclose(self, t): r'\)' t.lexer.pop_state() return t def t_t_pclose(self, t): r'\)' return t def t_t_semicolon(self, t): r';' t.lexer.in_property_decl = False return t def t_t_eopen(self, t): r'~"|~\'' if t.value[1] == '"': t.lexer.push_state('escapequotes') elif t.value[1] == '\'': t.lexer.push_state('escapeapostrophe') return t def t_t_tilde(self, t): r'~' return t def t_escapequotes_less_variable(self, t): r'@\{[^@"\}]+\}' return t def t_escapeapostrophe_less_variable(self, t): r'@\{[^@\'\}]+\}' return t def t_escapequotes_t_eclose(self, t): r'"' t.lexer.pop_state() return t def t_escapeapostrophe_t_eclose(self, t): r'\'' t.lexer.pop_state() return t def t_css_string(self, t): r'"[^"@]*"|\'[^\'@]*\'' t.lexer.lineno += t.value.count('\n') return t def t_t_isopen(self, t): r'"|\'' if t.value[0] == '"': t.lexer.push_state('istringquotes') elif t.value[0] == '\'': t.lexer.push_state('istringapostrophe') return t def t_istringquotes_less_variable(self, t): r'@\{[^@"\}]+\}' return t def t_istringapostrophe_less_variable(self, t): r'@\{[^@\'\}]+\}' return t def t_istringapostrophe_css_string(self, t): r'[^\'@]+' t.lexer.lineno += t.value.count('\n') return t def t_istringquotes_css_string(self, t): r'[^"@]+' t.lexer.lineno += t.value.count('\n') return t def t_istringquotes_t_isclose(self, t): r'"' t.lexer.pop_state() return t def t_istringapostrophe_t_isclose(self, t): r'\'' t.lexer.pop_state() return t # Error handling rule def t_error(self, t): raise SyntaxError("Illegal character '%s' line %d" % (t.value[0], t.lexer.lineno)) t.lexer.skip(1) # Build the lexer def build(self, **kwargs): self.lexer = lex.lex(module=self, **kwargs) # State-tracking variable, see http://www.dabeaz.com/ply/ply.html#ply_nn18 self.lexer.in_property_decl = False def file(self, filename): """ Lex file. """ with open(filename) as f: self.lexer.input(f.read()) return self def input(self, file): """ Load lexer with content from `file` which can be a path or a file like object. """ if isinstance(file, string_types): with open(file) as f: self.lexer.input(f.read()) else: self.lexer.input(file.read()) def token(self): """ Token function. Contains 2 hacks: 1. Injects ';' into blocks where the last property leaves out the ; 2. Strips out whitespace from nonsignificant locations to ease parsing. """ if self.next_: t = self.next_ self.next_ = None return t while True: t = self.lexer.token() if not t: return t if t.type == 't_ws' and ( self.pretok or (self.last and self.last.type not in self.significant_ws)): continue self.pretok = False if t.type == 't_bclose' and self.last and self.last.type not in ['t_bopen', 't_bclose'] and self.last.type != 't_semicolon' \ and not (hasattr(t, 'lexer') and (t.lexer.lexstate == 'escapequotes' or t.lexer.lexstate == 'escapeapostrophe')): self.next_ = t tok = lex.LexToken() tok.type = 't_semicolon' tok.value = ';' tok.lineno = t.lineno tok.lexpos = t.lexpos self.last = tok self.lexer.in_property_decl = False return tok self.last = t break return t PKzE''lesscpy/lessc/utility.py# -*- coding: utf8 -*- """ .. module:: lesscpy.lessc.utility :synopsis: various utility functions Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from __future__ import print_function import collections import itertools import math import re import sys from six import string_types def flatten(lst): """Flatten list. Args: lst (list): List to flatten Returns: generator """ for elm in lst: if isinstance(elm, collections.Iterable) and not isinstance(elm, string_types): for sub in flatten(elm): yield sub else: yield elm def pairwise(lst): """ yield item i and item i+1 in lst. e.g. (lst[0], lst[1]), (lst[1], lst[2]), ..., (lst[-1], None) Args: lst (list): List to process Returns: list """ if not lst: return length = len(lst) for i in range(length - 1): yield lst[i], lst[i + 1] yield lst[-1], None def rename(blocks, scope, stype): """ Rename all sub-blocks moved under another block. (mixins) Args: lst (list): block list scope (object): Scope object """ for p in blocks: if isinstance(p, stype): p.tokens[0].parse(scope) if p.tokens[1]: scope.push() scope.current = p.tokens[0] rename(p.tokens[1], scope, stype) scope.pop() def blocksearch(block, name): """ Recursive search for name in block (inner blocks) Args: name (str): search term Returns: Block OR False """ if hasattr(block, 'tokens'): for b in block.tokens[1]: b = (b if hasattr(b, 'raw') and b.raw() == name else blocksearch(b, name)) if b: return b return False def reverse_guard(lst): """ Reverse guard expression. not (@a > 5) -> (@a =< 5) Args: lst (list): Expression returns: list """ rev = { '<': '>=', '>': '=<', '>=': '<', '=<': '>' } return [rev[l] if l in rev else l for l in lst] def debug_print(lst, lvl=0): """ Print scope tree args: lst (list): parse result lvl (int): current nesting level """ pad = ''.join(['\t.'] * lvl) t = type(lst) if t is list: for p in lst: debug_print(p, lvl) elif hasattr(lst, 'tokens'): print(pad, t) debug_print(list(flatten(lst.tokens)), lvl + 1) def destring(value): """ Strip quotes from string args: value (str) returns: str """ return value.strip('"\'') def analyze_number(var, err=''): """ Analyse number for type and split from unit 1px -> (q, 'px') args: var (str): number string kwargs: err (str): Error message raises: SyntaxError returns: tuple """ n, u = split_unit(var) if not isinstance(var, string_types): return (var, u) if is_color(var): return (var, 'color') if is_int(n): n = int(n) elif is_float(n): n = float(n) else: raise SyntaxError('%s ´%s´' % (err, var)) return (n, u) def with_unit(number, unit=None): """ Return number with unit args: number (mixed): Number unit (str): Unit returns: str """ if isinstance(number, tuple): number, unit = number if number == 0: return '0' if unit: number = str(number) if number.startswith('.'): number = '0' + number return "%s%s" % (number, unit) return number if isinstance(number, string_types) else str(number) def is_color(value): """ Is string CSS color args: value (str): string returns: bool """ if not value or not isinstance(value, string_types): return False if value[0] == '#' and len(value) in [4, 5, 7, 9]: try: int(value[1:], 16) return True except ValueError: pass return False def is_variable(value): """ Check if string is LESS variable args: value (str): string returns: bool """ if isinstance(value, string_types): return (value.startswith('@') or value.startswith('-@')) elif isinstance(value, tuple): value = ''.join(value) return (value.startswith('@') or value.startswith('-@')) return False def is_int(value): """ Is value integer args: value (str): string returns: bool """ try: int(str(value)) return True except (ValueError, TypeError): pass return False def is_float(value): """ Is value float args: value (str): string returns: bool """ if not is_int(value): try: float(str(value)) return True except (ValueError, TypeError): pass return False def split_unit(value): """ Split a number from its unit 1px -> (q, 'px') Args: value (str): input returns: tuple """ r = re.search('^(\-?[\d\.]+)(.*)$', str(value)) return r.groups() if r else ('', '') def away_from_zero_round(value, ndigits=0): """Round half-way away from zero. Python2's round() method. """ if sys.version_info[0] >= 3: p = 10 ** ndigits return float(math.floor((value * p) + math.copysign(0.5, value))) / p else: return round(value, ndigits) def convergent_round(value, ndigits=0): """Convergent rounding. Round to neareas even, similar to Python3's round() method. """ if sys.version_info[0] < 3: if value < 0.0: return -convergent_round(-value) epsilon = 0.0000001 integral_part, _ = divmod(value, 1) if abs(value - (integral_part + 0.5)) < epsilon: if integral_part % 2.0 < epsilon: return integral_part else: nearest_even = integral_part + 0.5 return math.ceil(nearest_even) return round(value, ndigits) def pc_or_float(s): """ Utility function to process strings that contain either percentiles or floats args: str: s returns: float """ if isinstance(s, string_types) and '%' in s: return float(s.strip('%')) / 100.0 return float(s) def permutations_with_replacement(iterable, r=None): """Return successive r length permutations of elements in the iterable. Similar to itertools.permutation but withouth repeated values filtering. """ pool = tuple(iterable) n = len(pool) r = n if r is None else r for indices in itertools.product(range(n), repeat=r): yield list(pool[i] for i in indices) PKPqHjjlesscpy/plib/call.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.call :synopsis: Call parse node Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import re import math try: from urllib.parse import quote as urlquote except ImportError: from urllib import quote as urlquote import six from .node import Node import lesscpy.lessc.utility as utility import lesscpy.lessc.color as Color from lesscpy.lib.colors import lessColors class Call(Node): """Call node. Node represents a function call. All builtin none-color functions are in this node. This node attempts calls on built-ins and lets non-builtins through. increment(3px) --> 4px unknown(3px) --> unknown(3px) """ def parse(self, scope): """Parse Node within scope. the functions ~( and e( map to self.escape and %( maps to self.sformat args: scope (Scope): Current scope """ name = ''.join(self.tokens[0]) parsed = self.process(self.tokens[1:], scope) if name == '%(': name = 'sformat' elif name in ('~', 'e'): name = 'escape' color = Color.Color() args = [t for t in parsed if not isinstance(t, six.string_types) or t not in '(),'] if hasattr(self, name): try: return getattr(self, name)(*args) except ValueError: pass if hasattr(color, name): try: result = getattr(color, name)(*args) try: return result + ' ' except TypeError: return result except ValueError: pass return name + ''.join([p for p in parsed]) def escape(self, string, *args): """Less Escape. args: string (str): string to escape returns: str """ return utility.destring(string.strip('~')) def sformat(self, string, *args): """ String format. args: string (str): string to format args (list): format options returns: str """ format = string items = [] m = re.findall('(%[asdA])', format) if m and not args: raise SyntaxError('Not enough arguments...') i = 0 for n in m: v = { '%A': urlquote, '%s': utility.destring, }.get(n, str)(args[i]) items.append(v) i += 1 format = format.replace('%A', '%s') format = format.replace('%d', '%s') return format % tuple(items) def isnumber(self, string, *args): """Is number args: string (str): match returns: bool """ try: n, u = utility.analyze_number(string) except SyntaxError: return False return True def iscolor(self, string, *args): """Is color args: string (str): match returns: bool """ return (string in lessColors) def isurl(self, string, *args): """Is url args: string (str): match returns: bool """ arg = utility.destring(string) regex = re.compile(r'^(?:http|ftp)s?://' # http:// or https:// r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... # localhost... r'localhost|' r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip # optional port r'(?::\d+)?' r'(?:/?|[/?]\S+)$', re.IGNORECASE) return regex.match(arg) def isstring(self, string, *args): """Is string args: string (str): match returns: bool """ regex = re.compile(r'\'[^\']*\'|"[^"]*"') return regex.match(string) def iskeyword(self, string, *args): """Is less keyword args: string (str): match returns: bool """ return (string in ('when', 'and', 'not')) def increment(self, value, *args): """ Increment function args: value (str): target returns: str """ n, u = utility.analyze_number(value) return utility.with_unit(n + 1, u) def decrement(self, value, *args): """ Decrement function args: value (str): target returns: str """ n, u = utility.analyze_number(value) return utility.with_unit(n - 1, u) def add(self, *args): """ Add integers args: args (list): target returns: str """ if(len(args) <= 1): return 0 return sum([int(v) for v in args]) def round(self, value, *args): """ Round number args: value (str): target returns: str """ n, u = utility.analyze_number(value) return utility.with_unit(int(utility.away_from_zero_round(float(n))), u) def ceil(self, value, *args): """ Ceil number args: value (str): target returns: str """ n, u = utility.analyze_number(value) return utility.with_unit(int(math.ceil(n)), u) def floor(self, value, *args): """ Floor number args: value (str): target returns: str """ n, u = utility.analyze_number(value) return utility.with_unit(int(math.floor(n)), u) def percentage(self, value, *args): """ Return percentage value args: value (str): target returns: str """ n, u = utility.analyze_number(value) n = int(n * 100.0) u = '%' return utility.with_unit(n, u) PKPqH lesscpy/plib/expression.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.expression :synopsis: Expression node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import operator from .node import Node from lesscpy.lessc import utility from lesscpy.lessc import color class Expression(Node): """Expression node. Parses all expression except color expressions (handled in the color class) and unary negation (handled in the NegatedExpression class). """ def parse(self, scope): """ Parse Node args: scope (Scope): Scope object raises: SyntaxError returns: str """ assert(len(self.tokens) == 3) expr = self.process(self.tokens, scope) A, O, B = [e[0] if isinstance(e, tuple) else e for e in expr if str(e).strip()] try: a, ua = utility.analyze_number(A, 'Illegal element in expression') b, ub = utility.analyze_number(B, 'Illegal element in expression') except SyntaxError: return ' '.join([str(A), str(O), str(B)]) if(a is False or b is False): return ' '.join([str(A), str(O), str(B)]) if ua == 'color' or ub == 'color': return color.Color().process((A, O, B)) if a == 0 and O == '/': # NOTE(saschpe): The ugliest but valid CSS since sliced bread: 'font: 0/1 a;' return ''.join([str(A), str(O), str(B), ' ']) out = self.operate(a, b, O) if isinstance(out, bool): return out return self.with_units(out, ua, ub) def with_units(self, val, ua, ub): """Return value with unit. args: val (mixed): result ua (str): 1st unit ub (str): 2nd unit raises: SyntaxError returns: str """ if not val: return str(val) if ua or ub: if ua and ub: if ua == ub: return str(val) + ua else: # Nodejs version does not seem to mind mismatched # units within expressions. So we choose the first # as they do # raise SyntaxError("Error in expression %s != %s" % (ua, ub)) return str(val) + ua elif ua: return str(val) + ua elif ub: return str(val) + ub return repr(val) def operate(self, vala, valb, oper): """Perform operation args: vala (mixed): 1st value valb (mixed): 2nd value oper (str): operation returns: mixed """ operation = { '+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv, '=': operator.eq, '>': operator.gt, '<': operator.lt, '>=': operator.ge, '=<': operator.le, }.get(oper) if operation is None: raise SyntaxError("Unknown operation %s" % oper) ret = operation(vala, valb) if oper in '+-*/' and int(ret) == ret: ret = int(ret) return ret def expression(self): """Return str representation of expression returns: str """ return utility.flatten(self.tokens) PK4g Clesscpy/plib/statement.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.statement :synopsis: Statement node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node from lesscpy.lessc import utility class Statement(Node): """Represents CSS statement (@import, @charset...) """ def parse(self, scope): """Parse node args: scope (Scope): current scope raises: SyntaxError returns: self """ self.parsed = list(utility.flatten(self.tokens)) if self.parsed[0] == '@import': if len(self.parsed) > 4: # Media @import self.parsed.insert(3, ' ') return self def fmt(self, fills): """ Format node args: fills (dict): replacements returns: str """ return ''.join(self.parsed) + fills['eb'] PKIBDH lesscpy/plib/block.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.block :synopsis: Block parse node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node from lesscpy.lessc import utility from lesscpy.plib.identifier import Identifier class Block(Node): """ Block node. Represents one parse-block. Can contain property nodes or other block nodes. identifier { propertys inner blocks } """ def parse(self, scope): """Parse block node. args: scope (Scope): Current scope raises: SyntaxError returns: self """ if not self.parsed: scope.push() self.name, inner = self.tokens scope.current = self.name scope.real.append(self.name) if not self.name.parsed: self.name.parse(scope) if not inner: inner = [] inner = list(utility.flatten([p.parse(scope) for p in inner if p])) self.parsed = [] self.inner = [] if not hasattr(self, "inner_media_queries"): self.inner_media_queries = [] for p in inner: if p is not None: if isinstance(p, Block): if (len(scope) == 2 and p.tokens[1] is not None): p_is_mediaquery = p.name.tokens[0] == '@media' # Inner block @media ... { ... } is a nested media # query. But double-nested media queries have to be # removed and marked as well. While parsing ".foo", # both nested "@media print" and double-nested # "@media all" will be handled as we have to # re-arrange the scope and block layout quite a bit: # # .foo { # @media print { # color: blue; # @media screen { font-size: 12em; } # } # } # # Expected result: # # @media print { # .foo { color: blue; } # } # @media print and screen { # .foo { font-size: 12 em; } # } append_list = [] reparse_p = False for child in p.tokens[1]: if isinstance(child, Block) and child.name.raw().startswith("@media"): # Remove child from the nested media query, it will be re-added to # the parent with 'merged' media query (see above example). p.tokens[1].remove(child) if p_is_mediaquery: # Media query inside a & block # Double-nested media query found. We remove it from 'p' and add # it to this block with a new 'name'. reparse_p = True part_a = p.name.tokens[2:][0][0][0] part_b = child.name.tokens[2:][0][0] new_ident_tokens = ['@media', ' ', [part_a, (' ', 'and', ' '), part_b]] # Parse child again with new @media $BLA {} part child.tokens[0] = Identifier(new_ident_tokens) child.parsed = None child = child.parse(scope) else: child.block_name = p.name append_list.append(child) if reparse_p: p.parsed = None p = p.parse(scope) if not p_is_mediaquery and not append_list: self.inner.append(p) else: append_list.insert(0, p) # This media query should occur before it's children for media_query in append_list: self.inner_media_queries.append(media_query) # NOTE(saschpe): The code is not recursive but we hope that people # wont use triple-nested media queries. else: self.inner.append(p) else: self.parsed.append(p) if self.inner_media_queries: # Nested media queries, we have to remove self from scope and # push all nested @media ... {} blocks. scope.remove_block(self, index=-2) for mb in self.inner_media_queries: # New inner block with current name and media block contents if hasattr(mb, 'block_name'): cb_name = mb.block_name else: cb_name = self.tokens[0] cb = Block([cb_name, mb.tokens[1]]).parse(scope) # Replace inner block contents with new block new_mb = Block([mb.tokens[0], [cb]]).parse(scope) self.inner.append(new_mb) scope.add_block(new_mb) scope.real.pop() scope.pop() return self def raw(self, clean=False): """Raw block name args: clean (bool): clean name returns: str """ try: return self.tokens[0].raw(clean) except (AttributeError, TypeError): pass def fmt(self, fills): """Format block (CSS) args: fills (dict): Fill elements returns: str (CSS) """ f = "%(identifier)s%(ws)s{%(nl)s%(proplist)s}%(eb)s" out = [] name = self.name.fmt(fills) if self.parsed and any(p for p in self.parsed if str(type(p)) != ""): fills.update({ 'identifier': name, 'proplist': ''.join([p.fmt(fills) for p in self.parsed if p]), }) out.append(f % fills) if hasattr(self, 'inner'): if self.name.subparse and len(self.inner) > 0: # @media inner = ''.join([p.fmt(fills) for p in self.inner]) inner = inner.replace(fills['nl'], fills['nl'] + fills['tab']).rstrip(fills['tab']) if not fills['nl']: inner = inner.strip() fills.update({ 'identifier': name, 'proplist': fills['tab'] + inner }) out.append(f % fills) else: out.append(''.join([p.fmt(fills) for p in self.inner])) return ''.join(out) def copy(self): """ Return a full copy of self returns: Block object """ name, inner = self.tokens if inner: inner = [u.copy() if u else u for u in inner] if name: name = name.copy() return Block([name, inner], 0) def copy_inner(self, scope): """Copy block contents (properties, inner blocks). Renames inner block from current scope. Used for mixins. args: scope (Scope): Current scope returns: list (block contents) """ if self.tokens[1]: tokens = [u.copy() if u else u for u in self.tokens[1]] out = [p for p in tokens if p] utility.rename(out, scope, Block) return out return None PKbBD`mlesscpy/plib/identifier.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.identifier :synopsis: Identifier node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import re from .node import Node from lesscpy.lessc import utility from lesscpy.lib import reserved class Identifier(Node): """Identifier node. Represents block identifier. """ def parse(self, scope): """Parse node. Block identifiers are stored as strings with spaces replaced with ? args: scope (Scope): Current scope raises: SyntaxError returns: self """ names = [] name = [] self._subp = ( '@media', '@keyframes', '@-moz-keyframes', '@-webkit-keyframes', '@-ms-keyframes' ) if self.tokens and hasattr(self.tokens, 'parse'): self.tokens = list(utility.flatten([id.split() + [','] for id in self.tokens.parse(scope).split(',')])) self.tokens.pop() if self.tokens and any(hasattr(t, 'parse') for t in self.tokens): tmp_tokens = [] for t in self.tokens: if hasattr(t, 'parse'): tmp_tokens.append(t.parse(scope)) else: tmp_tokens.append(t) self.tokens = list(utility.flatten(tmp_tokens)) if self.tokens and self.tokens[0] in self._subp: name = list(utility.flatten(self.tokens)) self.subparse = True else: self.subparse = False for n in utility.flatten(self.tokens): if n == '*': name.append('* ') elif n in '>+~': if name and name[-1] == ' ': name.pop() name.append('?%s?' % n) elif n == ',': names.append(name) name = [] else: name.append(n) names.append(name) parsed = self.root(scope, names) if scope else names # Interpolated selectors need another step, we have to replace variables. Avoid reserved words though # # Example: '.@{var}' results in [['.', '@{var}']] # But: '@media print' results in [['@media', ' ', 'print']] # def replace_variables(tokens, scope): return [scope.swap(t) if (utility.is_variable(t) and not t in reserved.tokens) else t for t in tokens] parsed = [list(utility.flatten(replace_variables(part, scope))) for part in parsed] self.parsed = [[i for i, j in utility.pairwise(part) if i != ' ' or (j and '?' not in j)] for part in parsed] return self def root(self, scope, names): """Find root of identifier, from scope args: scope (Scope): current scope names (list): identifier name list (, separated identifiers) returns: list """ parent = scope.scopename if parent: parent = parent[-1] if parent.parsed: parsed_names = [] for name in names: ampersand_count = name.count('&') if ampersand_count: filtered_parts = [] for part in parent.parsed: if part and part[0] not in self._subp: filtered_parts.append(part) permutations = list(utility.permutations_with_replacement(filtered_parts, ampersand_count)) for permutation in permutations: parsed = [] for name_part in name: if name_part == "&": parent_part = permutation.pop(0) if parsed and parsed[-1].endswith(']'): parsed.extend(' ') if parent_part[-1] == ' ': parent_part.pop() parsed.extend(parent_part) else: parsed.append(name_part) parsed_names.append(parsed) else: # NOTE(saschpe): Maybe this code can be expressed with permutations too? for part in parent.parsed: if part and part[0] not in self._subp: parsed = [] if name[0] == "@media": parsed.extend(name) else: parsed.extend(part) if part[-1] != ' ': parsed.append(' ') parsed.extend(name) parsed_names.append(parsed) else: parsed_names.append(name) return parsed_names return names def raw(self, clean=False): """Raw identifier. args: clean (bool): clean name returns: str """ if clean: return ''.join(''.join(p) for p in self.parsed).replace('?', ' ') return '%'.join('%'.join(p) for p in self.parsed).strip().strip('%') def copy(self): """ Return copy of self Returns: Identifier object """ tokens = ([t for t in self.tokens] if isinstance(self.tokens, list) else self.tokens) return Identifier(tokens, 0) def fmt(self, fills): """Format identifier args: fills (dict): replacements returns: str (CSS) """ name = ',$$'.join(''.join(p).strip() for p in self.parsed) name = re.sub('\?(.)\?', '%(ws)s\\1%(ws)s', name) % fills return name.replace('$$', fills['nl']).replace(' ', ' ') PKIBDe e lesscpy/plib/property.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.property :synopsis: Property node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import re from .node import Node class Property(Node): """Represents CSS property declaration. """ def parse(self, scope): """Parse node args: scope (Scope): current scope raises: SyntaxError returns: self """ if not self.parsed: if len(self.tokens) > 2: property, style, _ = self.tokens self.important = True else: property, style = self.tokens self.important = False self.property = ''.join(property) self.parsed = [] if style: style = self.preprocess(style) self.parsed = self.process(style, scope) return self def preprocess(self, style): """Hackish preprocessing from font shorthand tags. Skips expression parse on certain tags. args: style (list): . returns: list """ if self.property == 'font': style = [''.join(u.expression()) if hasattr(u, 'expression') else u for u in style] else: style = [(u, ' ') if hasattr(u, 'expression') else u for u in style] return style def fmt(self, fills): """ Format node args: fills (dict): replacements returns: str """ f = "%(tab)s%(property)s:%(ws)s%(style)s%(important)s;%(nl)s" imp = ' !important' if self.important else '' if fills['nl']: self.parsed = [',%s' % fills['ws'] if p == ',' else p for p in self.parsed] style = ''.join([p.fmt(fills) if hasattr(p, 'fmt') else str(p) for p in self.parsed]) # IE cannot handle no space after url() style = re.sub("(url\([^\)]*\))([^\s,])", "\\1 \\2", style) fills.update({ 'property': self.property, 'style': style.strip(), 'important': imp }) return f % fills def copy(self): """ Return a full copy of self Returns: Property object """ return Property([t for t in self.tokens], 0) PKIBDkxxlesscpy/plib/deferred.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.deferred :synopsis: Deferred mixin call. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node class Deferred(Node): def __init__(self, mixin, args, lineno=0): """This node represents mixin calls. The calls to these mixins are deferred until the second parse cycle. lessc.js allows calls to mixins not yet defined or known. args: mixin (Mixin): Mixin object args (list): Call arguments """ self.tokens = [mixin, args] self.lineno = lineno def parse(self, scope, error=False, depth=0): """ Parse function. We search for mixins first within current scope then fallback to global scope. The special scope.deferred is used when local scope mixins are called within parent mixins. If nothing is found we fallback to block-mixin as lessc.js allows calls to blocks and mixins to be inter-changable. clx: This method is a HACK that stems from poor design elsewhere. I will fix it when I have more time. args: scope (Scope): Current scope returns: mixed """ res = False ident, args = self.tokens ident.parse(scope) mixins = scope.mixins(ident.raw()) if not mixins: ident.parse(None) mixins = scope.mixins(ident.raw()) if depth > 64: raise SyntaxError('NameError `%s`' % ident.raw(True)) if not mixins: if scope.deferred: store = [t for t in scope.deferred.parsed[-1]] i = 0 while scope.deferred.parsed[-1]: scope.current = scope.deferred ident.parse(scope) mixins = scope.mixins(ident.raw()) scope.current = None if mixins or i > 64: break scope.deferred.parsed[-1].pop() i += 1 scope.deferred.parsed[-1] = store if not mixins: # Fallback to blocks block = scope.blocks(ident.raw()) if not block: ident.parse(None) block = scope.blocks(ident.raw()) if block: scope.current = scope.real[-1] if scope.real else None res = block.copy_inner(scope) scope.current = None if mixins: for mixin in mixins: scope.current = scope.real[-1] if scope.real else None res = mixin.call(scope, args) if res: # Add variables to scope to support # closures [scope.add_variable(v) for v in mixin.vars] scope.deferred = ident break if res: store = [t for t in scope.deferred.parsed[ -1]] if scope.deferred else False tmp_res = [] for p in res: if p: if isinstance(p, Deferred): tmp_res.append(p.parse(scope, depth=depth + 1)) else: tmp_res.append(p.parse(scope)) res = tmp_res #res = [p.parse(scope, depth=depth+1) for p in res if p] while(any(t for t in res if isinstance(t, Deferred))): res = [p.parse(scope) for p in res if p] if store: scope.deferred.parsed[-1] = store if error and not res: raise SyntaxError('NameError `%s`' % ident.raw(True)) return res def copy(self): """ Returns self (used when Block objects are copy'd) returns: self """ return self PKPqH!lesscpy/plib/keyframe_selector.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.keyframe_selector :synopsis: Keyframe selector node. Copyright (c) See LICENSE for details. """ from .node import Node class KeyframeSelector(Node): """Keyframe selector node. Represents the keyframe selector in an animation sequence. Keyframes can be identified by the keywords "from" or "to", or by percentage. http://www.w3.org/TR/css3-animations/#keyframes """ def parse(self, scope): """Parse node. args: scope (Scope): Current scope raises: SyntaxError returns: self """ self.keyframe, = [e[0] if isinstance(e, tuple) else e for e in self.tokens if str(e).strip()] self.subparse = False return self def copy(self): """ Return copy of self Returns: KeyframeSelector object """ return KeyframeSelector(self.tokens, 0) def fmt(self, fills): """Format identifier args: fills (dict): replacements returns: str (CSS) """ return self.keyframe PKbBD@%lesscpy/plib/variable.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.variable :synopsis: Variable declaration Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node class Variable(Node): def parse(self, scope): """ Parse function args: scope (Scope): Scope object returns: self """ self.name, _, self.value = self.tokens if isinstance(self.name, tuple): if len(self.name) > 1: self.name, pad = self.name self.value.append(pad) else: self.name = self.name[0] scope.add_variable(self) return self def copy(self): """ Return a copy of self Returns: Variable object """ return Variable([t for t in self.tokens]) def fmt(self, fills): return '' PK4g Cp}lesscpy/plib/mixin.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.mixin :synopsis: Mixin node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ import sys import copy import itertools from .node import Node from .block import Block from .expression import Expression from .variable import Variable from lesscpy.lessc import utility class Mixin(Node): """ Mixin Node. Represents callable mixin types. """ def parse(self, scope): """Parse node args: scope (Scope): current scope raises: SyntaxError returns: self """ self.name, args, self.guards = self.tokens[0] self.args = [a for a in utility.flatten(args) if a] self.body = Block([None, self.tokens[1]], 0) self.vars = list(utility.flatten([list(v.values()) for v in [s['__variables__'] for s in scope]])) return self def raw(self): """Raw mixin name returns: str """ return self.name.raw() def parse_args(self, args, scope): """Parse arguments to mixin. Add them to scope as variables. Sets upp special variable @arguments as well. args: args (list): arguments scope (Scope): current scope raises: SyntaxError """ arguments = list(zip(args, [' '] * len(args))) if args and args[0] else None zl = itertools.zip_longest if sys.version_info[ 0] == 3 else itertools.izip_longest if self.args: parsed = [v if hasattr(v, 'parse') else v for v in copy.copy(self.args)] args = args if isinstance(args, list) else [args] vars = [self._parse_arg(var, arg, scope) for arg, var in zl([a for a in args], parsed)] for var in vars: if var: var.parse(scope) if not arguments: arguments = [v.value for v in vars if v] if not arguments: arguments = '' Variable(['@arguments', None, arguments]).parse(scope) def _parse_arg(self, var, arg, scope): """ Parse a single argument to mixin. args: var (Variable object): variable arg (mixed): argument scope (Scope object): current scope returns: Variable object or None """ if isinstance(var, Variable): # kwarg if arg: if utility.is_variable(arg[0]): tmp = scope.variables(arg[0]) if not tmp: return None val = tmp.value else: val = arg var = Variable(var.tokens[:-1] + [val]) else: # arg if utility.is_variable(var): if arg is None: raise SyntaxError('Missing argument to mixin') elif utility.is_variable(arg[0]): tmp = scope.variables(arg[0]) if not tmp: return None val = tmp.value else: val = arg var = Variable([var, None, val]) else: return None return var def parse_guards(self, scope): """Parse guards on mixin. args: scope (Scope): current scope raises: SyntaxError returns: bool (passes guards) """ if self.guards: cor = True if ',' in self.guards else False for g in self.guards: if isinstance(g, list): res = (g[0].parse(scope) if len(g) == 1 else Expression(g).parse(scope)) if cor: if res: return True elif not res: return False return True def call(self, scope, args=[]): """Call mixin. Parses a copy of the mixins body in the current scope and returns it. args: scope (Scope): current scope args (list): arguments raises: SyntaxError returns: list or False """ ret = False if args: args = [[a.parse(scope) if isinstance(a, Expression) else a for a in arg] if arg else arg for arg in args] try: self.parse_args(args, scope) except SyntaxError: pass else: if self.parse_guards(scope): body = self.body.copy() ret = body.tokens[1] if ret: utility.rename(ret, scope, Block) return ret PKPqHfIIlesscpy/plib/__init__.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib :synopsis: Parse Nodes for Lesscpy Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ __all__ = [ 'Block', 'Call', 'Deferred', 'Expression', 'Identifier', 'KeyframeSelector', 'Mixin', 'NegatedExpression', 'Node', 'Property', 'Statement', 'Variable' 'Import', ] from .block import Block from .call import Call from .deferred import Deferred from .expression import Expression from .identifier import Identifier from .keyframe_selector import KeyframeSelector from .mixin import Mixin from .negated_expression import NegatedExpression from .node import Node from .property import Property from .statement import Statement from .variable import Variable from .import_ import Import PKzE۵44lesscpy/plib/import_.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.property :synopsis: Import node. Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from .node import Node class Import(Node): """Represents CSS property declaration. """ def parse(self, scope): """Parse node args: scope (Scope): current scope raises: SyntaxError returns: parsed """ if not self.parsed: self.parsed = ''.join(self.process(self.tokens, scope)) return self.parsed def fmt(self, fills): return '' def copy(self): """ Return a full copy of self Returns: Import object """ return Import([t for t in self.tokens], 0) PKPqHf"lesscpy/plib/negated_expression.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.negated_expression :synopsis: Node for unary negated expressions. Copyright (c) See LICENSE for details. """ import six from .node import Node class NegatedExpression(Node): """Expressions preceded by unary negation.""" def parse(self, scope): val, = self.process(self.tokens, scope) if isinstance(val, six.string_types): return '-' + val return -val PKbBDd2lesscpy/plib/node.py# -*- coding: utf8 -*- """ .. module:: lesscpy.plib.node :synopsis: Base Node Copyright (c) See LICENSE for details. .. moduleauthor:: Johann T. Mariusson """ from lesscpy.lessc import utility class Node(object): def __init__(self, tokens, lineno=0): """ Base Node args: tokens (list): tokenlist lineno (int): Line number of node """ self.tokens = tokens self.lineno = lineno self.parsed = False def parse(self, scope): """ Base parse function args: scope (Scope): Current scope returns: self """ return self def process(self, tokens, scope): """ Process tokenslist, flattening and parsing it args: tokens (list): tokenlist scope (Scope): Current scope returns: list """ while True: tokens = list(utility.flatten(tokens)) done = True if any(t for t in tokens if hasattr(t, 'parse')): tokens = [t.parse(scope) if hasattr(t, 'parse') else t for t in tokens] done = False if any(t for t in tokens if (utility.is_variable(t)) or str(type(t)) == ""): tokens = self.replace_variables(tokens, scope) done = False if done: break return tokens def replace_variables(self, tokens, scope): """ Replace variables in tokenlist args: tokens (list): tokenlist scope (Scope): Current scope returns: list """ list = [] for t in tokens: if utility.is_variable(t): list.append(scope.swap(t)) elif str(type(t)) == "": list.append(scope.swap(t.name)) else: list.append(t) return list def fmt(self, fills): """ Format node args: fills (dict): replacements returns: str """ raise ValueError('No defined format') PKRqH7z||(lesscpy-0.11.1.dist-info/DESCRIPTION.rstLESSCPY ======= .. image:: https://travis-ci.org/lesscpy/lesscpy.png?branch=master :target: https://travis-ci.org/lesscpy/lesscpy .. image:: https://coveralls.io/repos/lesscpy/lesscpy/badge.png :target: https://coveralls.io/r/lesscpy/lesscpy .. image:: https://pypip.in/d/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy .. image:: https://pypip.in/v/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy .. image:: https://pypip.in/wheel/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy :alt: Wheel Status .. image:: https://pypip.in/license/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy :alt: License Python LESS Compiler. A compiler written in Python for the LESS language. For those of us not willing or able to have node.js installed in our environment. Not all features of LESS are supported (yet). Some features wil probably never be supported (JavaScript evaluation). This program uses PLY (Python Lex-Yacc) to tokenize / parse the input and is considerably slower than the NodeJS compiler. The plan is to utilize this to build in proper syntax checking and perhaps YUI compressing. This is an early version, so you are likely to find bugs. For more information on LESS: http://lesscss.org/ or https://github.com/cloudhead/less.js Development files: https://github.com/lesscpy/lesscpy Supported features ------------------ - Variables - String interpolation - Mixins (nested, calls, closures, recursive) - Guard expressions - Parametered mixins (class / id) - @arguments - Nesting - Escapes ~/e() - Expressions - Keyframe blocks - Color functions (lighten, darken, saturate, desaturate, spin, hue, mix, saturation, lightness) - Other functions (round, increment, decrement, format '%(', ...) Differences from less.js ------------------------ - All colors are auto-formatted to #nnnnnn. eg, #f7e923 - Does not preserve CSS comments Not supported ------------- - JavaScript evaluation Requirements ------------ - Python 2.6, 2.7, or 3.3 - ply (Python Lex-Yacc) (check requirements.txt) Installation ------------ To install lesscpy from the `Python Package Index`_, simply: .. code-block:: bash $ pip install lesscpy To do a local system-wide install: .. code-block:: bash python setup.py install Or simply place the package into your Python path. Or rather use packages provided by your distribution (openSUSE has them at least). Compiler script Usage --------------------- .. code-block:: text usage: lesscpy [-h] [-v] [-I INCLUDE] [-V] [-x] [-X] [-t] [-s SPACES] [-o OUT] [-r] [-f] [-m] [-D] [-g] [-S] [-L] [-N] target LessCss Compiler positional arguments: target less file or directory optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit -I INCLUDE, --include INCLUDE Included less-files (comma separated) -V, --verbose Verbose mode Formatting options: -x, --minify Minify output -X, --xminify Minify output, no end of block newlines -t, --tabs Use tabs -s SPACES, --spaces SPACES Number of startline spaces (default 2) Directory options: Compiles all \*.less files in directory that have a newer timestamp than it's css file. -o OUT, --out OUT Output directory -r, --recurse Recursive into subdirectorys -f, --force Force recompile on all files -m, --min-ending Add '.min' into output filename. eg, name.min.css -D, --dry-run Dry run, do not write files Debugging: -g, --debug Debugging information -S, --scopemap Scopemap -L, --lex-only Run lexer on target -N, --no-css No css output Python usage ------------ If you want to use the compiler from within Python, you can do it like this: .. code-block:: python import lesscpy from six import StringIO print(lesscpy.compile(StringIO(u"a { border-width: 2px * 3; }"), minify=True)) The output will be: .. code-block:: text a{border-width:6px;} License ------- See the LICENSE file .. _`Python Package Index`: https://pypi.python.org/pypi/lesscpy PKRqH&B::)lesscpy-0.11.1.dist-info/entry_points.txt[console_scripts] lesscpy = lesscpy.scripts.compiler:run PKRqH6% &lesscpy-0.11.1.dist-info/metadata.json{"classifiers": ["Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: End Users/Desktop", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Pre-processors"], "extensions": {"python.commands": {"wrap_console": {"lesscpy": "lesscpy.scripts.compiler:run"}}, "python.details": {"contacts": [{"email": "jtm@robot.is", "name": "J\u00f3hann T Mar\u00edusson", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/lesscpy/lesscpy"}}, "python.exports": {"console_scripts": {"lesscpy": "lesscpy.scripts.compiler:run"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "MIT", "metadata_version": "2.0", "name": "lesscpy", "run_requires": [{"requires": ["ply", "six"]}], "summary": "Python LESS compiler", "test_requires": [{"requires": ["coverage", "flake8", "nose"]}], "version": "0.11.1"}PKRqHfk&lesscpy-0.11.1.dist-info/top_level.txtlesscpy PKRqHndnnlesscpy-0.11.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PKRqH"tռ99!lesscpy-0.11.1.dist-info/METADATAMetadata-Version: 2.0 Name: lesscpy Version: 0.11.1 Summary: Python LESS compiler Home-page: https://github.com/lesscpy/lesscpy Author: Jóhann T Maríusson Author-email: jtm@robot.is License: MIT Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Topic :: Software Development :: Code Generators Classifier: Topic :: Software Development :: Pre-processors Requires-Dist: ply Requires-Dist: six LESSCPY ======= .. image:: https://travis-ci.org/lesscpy/lesscpy.png?branch=master :target: https://travis-ci.org/lesscpy/lesscpy .. image:: https://coveralls.io/repos/lesscpy/lesscpy/badge.png :target: https://coveralls.io/r/lesscpy/lesscpy .. image:: https://pypip.in/d/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy .. image:: https://pypip.in/v/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy .. image:: https://pypip.in/wheel/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy :alt: Wheel Status .. image:: https://pypip.in/license/lesscpy/badge.png :target: https://pypi.python.org/pypi/lesscpy :alt: License Python LESS Compiler. A compiler written in Python for the LESS language. For those of us not willing or able to have node.js installed in our environment. Not all features of LESS are supported (yet). Some features wil probably never be supported (JavaScript evaluation). This program uses PLY (Python Lex-Yacc) to tokenize / parse the input and is considerably slower than the NodeJS compiler. The plan is to utilize this to build in proper syntax checking and perhaps YUI compressing. This is an early version, so you are likely to find bugs. For more information on LESS: http://lesscss.org/ or https://github.com/cloudhead/less.js Development files: https://github.com/lesscpy/lesscpy Supported features ------------------ - Variables - String interpolation - Mixins (nested, calls, closures, recursive) - Guard expressions - Parametered mixins (class / id) - @arguments - Nesting - Escapes ~/e() - Expressions - Keyframe blocks - Color functions (lighten, darken, saturate, desaturate, spin, hue, mix, saturation, lightness) - Other functions (round, increment, decrement, format '%(', ...) Differences from less.js ------------------------ - All colors are auto-formatted to #nnnnnn. eg, #f7e923 - Does not preserve CSS comments Not supported ------------- - JavaScript evaluation Requirements ------------ - Python 2.6, 2.7, or 3.3 - ply (Python Lex-Yacc) (check requirements.txt) Installation ------------ To install lesscpy from the `Python Package Index`_, simply: .. code-block:: bash $ pip install lesscpy To do a local system-wide install: .. code-block:: bash python setup.py install Or simply place the package into your Python path. Or rather use packages provided by your distribution (openSUSE has them at least). Compiler script Usage --------------------- .. code-block:: text usage: lesscpy [-h] [-v] [-I INCLUDE] [-V] [-x] [-X] [-t] [-s SPACES] [-o OUT] [-r] [-f] [-m] [-D] [-g] [-S] [-L] [-N] target LessCss Compiler positional arguments: target less file or directory optional arguments: -h, --help show this help message and exit -v, --version show program's version number and exit -I INCLUDE, --include INCLUDE Included less-files (comma separated) -V, --verbose Verbose mode Formatting options: -x, --minify Minify output -X, --xminify Minify output, no end of block newlines -t, --tabs Use tabs -s SPACES, --spaces SPACES Number of startline spaces (default 2) Directory options: Compiles all \*.less files in directory that have a newer timestamp than it's css file. -o OUT, --out OUT Output directory -r, --recurse Recursive into subdirectorys -f, --force Force recompile on all files -m, --min-ending Add '.min' into output filename. eg, name.min.css -D, --dry-run Dry run, do not write files Debugging: -g, --debug Debugging information -S, --scopemap Scopemap -L, --lex-only Run lexer on target -N, --no-css No css output Python usage ------------ If you want to use the compiler from within Python, you can do it like this: .. code-block:: python import lesscpy from six import StringIO print(lesscpy.compile(StringIO(u"a { border-width: 2px * 3; }"), minify=True)) The output will be: .. code-block:: text a{border-width:6px;} License ------- See the LICENSE file .. _`Python Package Index`: https://pypi.python.org/pypi/lesscpy PKRqH lesscpy-0.11.1.dist-info/RECORDlesscpy/__init__.py,sha256=MzofGpwuwoVSFrhQLkYBpJPpHnQtyD3fsEIocnm6mJc,540 lesscpy/exceptions.py,sha256=ADoz49JsP05JuGT4hy0UiVhVgYHXVtGUAm-dwOkXIOY,46 lesscpy/lessc/__init__.py,sha256=7LF6u5bvP0Rz2McmxWynjIPVEOI0ZR141383OcNAGxU,98 lesscpy/lessc/color.py,sha256=M93ow4r4I6xbhLgcSUfDLgAVFYJeRsyTZw3yOvVRqmo,14089 lesscpy/lessc/formatter.py,sha256=87BiogUbVkSBqhN1cS1oPWhEMrniIQZOz7i3UJ5fVus,1090 lesscpy/lessc/lexer.py,sha256=DOw4Nf4UpCPgPLuYZIFUFlz_Zs9Xeprf9EbCwpmLNG0,13450 lesscpy/lessc/parser.py,sha256=5pwyUHsZZfcsZ5tGY8Ggt1Xbyx2cPi8-0fe6LUt5Q-s,32736 lesscpy/lessc/scope.py,sha256=QLUw93OdgMEHpwiTFh3XwRnQ3vqIaaOaDLx9I4ixdVw,5827 lesscpy/lessc/utility.py,sha256=YcToYzSf8W0CNpiBpjfaTEGuyTXBEBgQ-gOy8PKKLsc,6951 lesscpy/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 lesscpy/lib/colors.py,sha256=hnrFOmES7VC6d849USnYqmznjPhWyNk5ix8aIaOwcac,4126 lesscpy/lib/css.py,sha256=cWkO3nu4qc0nhFdmMWfiblo8QFgUTehP5aTRdoAFYyw,8272 lesscpy/lib/dom.py,sha256=k5FnCBmYPJ-1BFNMDTjvYN0KK5x-aj5M7_0nrWI6RyM,2044 lesscpy/lib/reserved.py,sha256=9lajmKwtk4cBMEHZIucuMOVTIj4nkIOgkRJdTKhbpXM,599 lesscpy/plib/__init__.py,sha256=c8QxZvyJ7UXPQTC4IPdT5eniezXhLZDyF-Sac4iG07s,841 lesscpy/plib/block.py,sha256=vEgiWbPvkOOo6pux79jb6lejtCtby7M2WIM04Qp2CR8,8437 lesscpy/plib/call.py,sha256=4aZ8XOrmIGwvPTId5w6dDwrNR6c8V_z_aTddDpWFFyI,6250 lesscpy/plib/deferred.py,sha256=S4Kb06FTYZwn0OUnFsTKXtN6kkTzre1zgZx5EOwxDiM,3960 lesscpy/plib/expression.py,sha256=dAw5vxmXc0vbBYwjEz6EDV53zUhgdR30oiFWZ8ITU-I,3550 lesscpy/plib/identifier.py,sha256=6EiTcGj5IYHi2bIMktLUe0VzkVA_8x5MnKjRW40GdoI,6414 lesscpy/plib/import_.py,sha256=bZ7AKlV7Vb-22gDmuMBVLArUE4rI3Hjifl2WPfrWwGs,820 lesscpy/plib/keyframe_selector.py,sha256=WJLQ8JG99MTN1dpiBKLarQYC6lbor4GrSGg3ikwDK-4,1173 lesscpy/plib/mixin.py,sha256=vA0Q8e_u30PdPuy-jPsQVecN-H-jgLJUOsNaOzIntlg,5100 lesscpy/plib/negated_expression.py,sha256=cq6LH8pPi2EsdwoLns3Wz15o_uleUkiQOg6LQiOxq7s,466 lesscpy/plib/node.py,sha256=LQr3H_AdAgXqLk6Tk8HNdryIDni_rD17xsAKQ2IQaKM,2302 lesscpy/plib/property.py,sha256=bP9OE-izW4fDoYdlLBIcGzWOadi6wg1Kld8OC9jt2c4,2661 lesscpy/plib/statement.py,sha256=RIOAf4febbuYnfjDiNHkLKKEfoRNXl7Lh5dUg-PBPLE,964 lesscpy/plib/variable.py,sha256=WWs2NBdaNp29htBoiTSogMr4LQmqcrBWqExqd_An2aY,929 lesscpy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 lesscpy/scripts/compiler.py,sha256=f9sTJdmRp9dOI-dCvIDpwXUtMfn-ja4K7ufD2OPI6P0,7842 lesscpy-0.11.1.dist-info/DESCRIPTION.rst,sha256=MXYhIkH3yf-JHL49Em_1vZe4dDaE_gK6FqMR0hgTAhE,4476 lesscpy-0.11.1.dist-info/METADATA,sha256=5z1lJ5ihT2CgU5ES0GzaIwJsxu2Liss2UQt0RwH5jnQ,5433 lesscpy-0.11.1.dist-info/RECORD,, lesscpy-0.11.1.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 lesscpy-0.11.1.dist-info/entry_points.txt,sha256=AA4n1ZrH-AprNWQWW3dbvVCFZePIYafHlIndvDwGcFs,58 lesscpy-0.11.1.dist-info/metadata.json,sha256=K4kbXfhkxuWq-5OwxgLsdlPYyXatXfag3186JYqpfY8,1297 lesscpy-0.11.1.dist-info/top_level.txt,sha256=XqbdBB-Gpc5ImPR2m1uOZkHLGWrtYJ4cfLDz3ddEEKo,8 PKQqH lesscpy/__init__.pyPKzE%j..Mlesscpy/exceptions.pyPKpD-Clesscpy/scripts/__init__.pyPKPqHuClesscpy/scripts/compiler.pyPKbBDjWW!lesscpy/lib/reserved.pyPKbBDqr_P P N$lesscpy/lib/css.pyPKpD-CDlesscpy/lib/__init__.pyPKiC CElesscpy/lib/colors.pyPKPqH@/ TUlesscpy/lib/dom.pyPKPqH` 7 7]lesscpy/lessc/color.pyPKPqH-U,lesscpy/lessc/scope.pyPKf C›BBlesscpy/lessc/formatter.pyPKf C$үbb.lesscpy/lessc/__init__.pyPKPqH$ǰlesscpy/lessc/parser.pyPKPqH440lesscpy/lessc/lexer.pyPKzE''elesscpy/lessc/utility.pyPKPqHjjlesscpy/plib/call.pyPKPqH lesscpy/plib/expression.pyPK4g Clesscpy/plib/statement.pyPKIBDH lesscpy/plib/block.pyPKbBD`mlesscpy/plib/identifier.pyPKIBDe e lesscpy/plib/property.pyPKIBDkxxlesscpy/plib/deferred.pyPKPqH![lesscpy/plib/keyframe_selector.pyPKbBD@%/lesscpy/plib/variable.pyPK4g Cp} lesscpy/plib/mixin.pyPKPqHfII%lesscpy/plib/__init__.pyPKzE۵44 lesscpy/plib/import_.pyPKPqHf" $lesscpy/plib/negated_expression.pyPKbBDd2&lesscpy/plib/node.pyPKRqH7z||(O/lesscpy-0.11.1.dist-info/DESCRIPTION.rstPKRqH&B::)Alesscpy-0.11.1.dist-info/entry_points.txtPKRqH6% &Alesscpy-0.11.1.dist-info/metadata.jsonPKRqHfk&Flesscpy-0.11.1.dist-info/top_level.txtPKRqHndnn3Glesscpy-0.11.1.dist-info/WHEELPKRqH"tռ99!Glesscpy-0.11.1.dist-info/METADATAPKRqH U]lesscpy-0.11.1.dist-info/RECORDPK%%c gi