Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 1803
0.00% covered (danger)
0.00%
0 / 175
CRAP
0.00% covered (danger)
0.00%
0 / 4
lessc
0.00% covered (danger)
0.00%
0 / 1068
0.00% covered (danger)
0.00%
0 / 123
263682
0.00% covered (danger)
0.00%
0 / 1
 findImport
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 fileExists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compressList
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 preg_quote
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tryImport
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
156
 compileImportedProps
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 compileBlock
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 compileCSSBlock
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 compileMedia
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 mediaParent
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 compileNestedBlock
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 compileRoot
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 compileProps
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 deduplicate
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 sortProps
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
90
 compileMediaQuery
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 multiplyMedia
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 expandParentSelectors
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 findClosestSelectors
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 multiplySelectors
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 compileSelectors
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 eq
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 patternMatch
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
702
 patternMatchAll
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 findBlocks
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
156
 zipSetArgs
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 compileProp
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
1260
 compileValue
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
420
 lib_pow
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_pi
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_mod
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_tan
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_sin
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_cos
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_atan
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_asin
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_acos
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_sqrt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_extract
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 lib_isnumber
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_isstring
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_iscolor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_iskeyword
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_ispixel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 lib_ispercentage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 lib_isem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 lib_isrem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 lib_rgbahex
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 lib_argb
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 lib_data_uri
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 lib_e
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 lib__sprintf
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 lib_floor
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_ceil
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_round
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 lib_unit
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 colorArgs
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 lib_darken
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 lib_lighten
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 lib_saturate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 lib_desaturate
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 lib_spin
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 lib_fadeout
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 lib_fadein
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 lib_hue
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_saturation
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_lightness
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_alpha
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 lib_fade
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 lib_percentage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 lib_mix
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
72
 lib_contrast
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 lib_luma
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 assertColor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 assertNumber
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 assertArgs
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 toHSL
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
90
 toRGB_helper
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
42
 toRGB
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 clamp
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 funcToColor
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
380
 reduce
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
1122
 coerceColor
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 coerceString
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 flattenList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 toBool
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 evaluate
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
 stringConcatenate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 fixColor
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 op_number_color
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 op_color_number
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 op_color_color
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
210
 lib_red
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 lib_green
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 lib_blue
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 op_number_number
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
182
 makeOutputBlock
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 pushEnv
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 popEnv
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 set
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 injectVariables
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 compile
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 compileFile
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 checkedCompile
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 cachedCompile
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
156
 parse
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 makeParser
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setFormatter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newFormatter
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 setPreserveComments
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 registerFunction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unregisterFunction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setVariables
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unsetVariable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setImportDir
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addImportDir
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 allParsedFiles
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 addParsedFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 throwError
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 ccompile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 cexecute
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
lessc_parser
0.00% covered (danger)
0.00%
0 / 697
0.00% covered (danger)
0.00%
0 / 46
124256
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 parse
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 parseChunk
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 1
2970
 isDirective
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 fixTags
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 expressionList
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 expression
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 expHelper
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
306
 propertyValue
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 parenValue
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 value
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
380
 import
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 mediaQueryList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 mediaQuery
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
182
 mediaExpression
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 openString
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
272
 string
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 interpolation
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 unit
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 color
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 argumentDef
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
600
 tags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 mixinTags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 tagBracket
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
240
 tag
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
210
 func
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
182
 variable
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 assign
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 keyword
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 end
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 guards
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 guardGroup
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 guard
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 literal
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 genericList
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 to
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 match
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 whitespace
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 peek
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 seek
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 throwError
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 pushBlock
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 pushSpecialBlock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 append
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 pop
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 removeComments
0.00% covered (danger)
0.00%
0 / 39
0.00% covered (danger)
0.00%
0 / 1
306
lessc_formatter_classic
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 5
552
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 indentStr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 property
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEmpty
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 block
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
272
lessc_formatter_compressed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 indentStr
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
lessc_formatter_lessjs
n/a
0 / 0
n/a
0 / 0
0
n/a
0 / 0
1<?php // phpcs:disable -- Copy-pasted library. Not reformatting to facilitate identification of actual changes.
2/**
3 * lessphp v0.5.0 + php 8 compatibility changes by a8c (project is unmaintained)
4 * https://leafo.net/lessphp
5 *
6 * LESS CSS compiler, adapted from http://lesscss.org
7 *
8 * Copyright 2013, Leaf Corcoran <leafot@gmail.com>
9 * Licensed under MIT or GPLv3, see LICENSE
10 */
11
12
13/**
14 * The LESS compiler and parser.
15 *
16 * Converting LESS to CSS is a three stage process. The incoming file is parsed
17 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
18 * representing the CSS structure by `lessc`. The CSS tree is fed into a
19 * formatter, like `lessc_formatter` which then outputs CSS as a string.
20 *
21 * During the first compile, all values are *reduced*, which means that their
22 * types are brought to the lowest form before being dump as strings. This
23 * handles math equations, variable dereferences, and the like.
24 *
25 * The `parse` function of `lessc` is the entry point.
26 *
27 * In summary:
28 *
29 * The `lessc` class creates an instance of the parser, feeds it LESS code,
30 * then transforms the resulting tree to a CSS tree. This class also holds the
31 * evaluation context, such as all available mixins and variables at any given
32 * time.
33 *
34 * The `lessc_parser` class is only concerned with parsing its input.
35 *
36 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
37 * handling things like indentation.
38 */
39#[\AllowDynamicProperties]
40class lessc {
41    static public $VERSION = "v0.5.0";
42
43    static public $TRUE = array("keyword", "true");
44    static public $FALSE = array("keyword", "false");
45
46    protected $libFunctions = array();
47    protected $registeredVars = array();
48    protected $preserveComments = false;
49
50    public $vPrefix = '@'; // prefix of abstract properties
51    public $mPrefix = '$'; // prefix of abstract blocks
52    public $parentSelector = '&';
53
54    public $importDisabled = false;
55    public $importDir = '';
56
57    protected $numberPrecision = null;
58
59    protected $allParsedFiles = array();
60
61    // set to the parser that generated the current line when compiling
62    // so we know how to create error messages
63    protected $sourceParser = null;
64    protected $sourceLoc = null;
65
66    static protected $nextImportId = 0; // uniquely identify imports
67
68    // attempts to find the path of an import url, returns null for css files
69    protected function findImport($url) {
70        foreach ((array)$this->importDir as $dir) {
71            $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
72            if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
73                return $file;
74            }
75        }
76
77        return null;
78    }
79
80    protected function fileExists($name) {
81        return is_file($name);
82    }
83
84    static public function compressList($items, $delim) {
85        if (!isset($items[1]) && isset($items[0])) return $items[0];
86        else return array('list', $delim, $items);
87    }
88
89    static public function preg_quote($what) {
90        return preg_quote($what, '/');
91    }
92
93    protected function tryImport($importPath, $parentBlock, $out) {
94        if ($importPath[0] == "function" && $importPath[1] == "url") {
95            $importPath = $this->flattenList($importPath[2]);
96        }
97
98        $str = $this->coerceString($importPath);
99        if ($str === null) return false;
100
101        $url = $this->compileValue($this->lib_e($str));
102
103        // don't import if it ends in css
104        if (substr_compare($url, '.css', -4, 4) === 0) return false;
105
106        $realPath = $this->findImport($url);
107
108        if ($realPath === null) return false;
109
110        if ($this->importDisabled) {
111            return array(false, "/* import disabled */");
112        }
113
114        if (isset($this->allParsedFiles[realpath($realPath)])) {
115            return array(false, null);
116        }
117
118        $this->addParsedFile($realPath);
119        $parser = $this->makeParser($realPath);
120        $root = $parser->parse(file_get_contents($realPath));
121
122        // set the parents of all the block props
123        foreach ($root->props as $prop) {
124            if ($prop[0] == "block") {
125                $prop[1]->parent = $parentBlock;
126            }
127        }
128
129        // copy mixins into scope, set their parents
130        // bring blocks from import into current block
131        // TODO: need to mark the source parser    these came from this file
132        foreach ($root->children as $childName => $child) {
133            if (isset($parentBlock->children[$childName])) {
134                $parentBlock->children[$childName] = array_merge(
135                    $parentBlock->children[$childName],
136                    $child);
137            } else {
138                $parentBlock->children[$childName] = $child;
139            }
140        }
141
142        $pi = pathinfo($realPath);
143        $dir = $pi["dirname"];
144
145        list($top, $bottom) = $this->sortProps($root->props, true);
146        $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
147
148        return array(true, $bottom, $parser, $dir);
149    }
150
151    protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
152        $oldSourceParser = $this->sourceParser;
153
154        $oldImport = $this->importDir;
155
156        // TODO: this is because the importDir api is stupid
157        $this->importDir = (array)$this->importDir;
158        array_unshift($this->importDir, $importDir);
159
160        foreach ($props as $prop) {
161            $this->compileProp($prop, $block, $out);
162        }
163
164        $this->importDir = $oldImport;
165        $this->sourceParser = $oldSourceParser;
166    }
167
168    /**
169     * Recursively compiles a block.
170     *
171     * A block is analogous to a CSS block in most cases. A single LESS document
172     * is encapsulated in a block when parsed, but it does not have parent tags
173     * so all of it's children appear on the root level when compiled.
174     *
175     * Blocks are made up of props and children.
176     *
177     * Props are property instructions, array tuples which describe an action
178     * to be taken, eg. write a property, set a variable, mixin a block.
179     *
180     * The children of a block are just all the blocks that are defined within.
181     * This is used to look up mixins when performing a mixin.
182     *
183     * Compiling the block involves pushing a fresh environment on the stack,
184     * and iterating through the props, compiling each one.
185     *
186     * See lessc::compileProp()
187     *
188     */
189    protected function compileBlock($block) {
190        switch ($block->type) {
191        case "root":
192            $this->compileRoot($block);
193            break;
194        case null:
195            $this->compileCSSBlock($block);
196            break;
197        case "media":
198            $this->compileMedia($block);
199            break;
200        case "directive":
201            $name = "@" . $block->name;
202            if (!empty($block->value)) {
203                $name .= " " . $this->compileValue($this->reduce($block->value));
204            }
205
206            $this->compileNestedBlock($block, array($name));
207            break;
208        default:
209            $this->throwError("unknown block type: $block->type\n");
210        }
211    }
212
213    protected function compileCSSBlock($block) {
214        $env = $this->pushEnv();
215
216        $selectors = $this->compileSelectors($block->tags);
217        $env->selectors = $this->multiplySelectors($selectors);
218        $out = $this->makeOutputBlock(null, $env->selectors);
219
220        $this->scope->children[] = $out;
221        $this->compileProps($block, $out);
222
223        $block->scope = $env; // mixins carry scope with them!
224        $this->popEnv();
225    }
226
227    protected function compileMedia($media) {
228        $env = $this->pushEnv($media);
229        $parentScope = $this->mediaParent($this->scope);
230
231        $query = $this->compileMediaQuery($this->multiplyMedia($env));
232
233        $this->scope = $this->makeOutputBlock($media->type, array($query));
234        $parentScope->children[] = $this->scope;
235
236        $this->compileProps($media, $this->scope);
237
238        if ( is_countable( $this->scope->lines ) && count( $this->scope->lines ) > 0 ) {
239            $orphanSelelectors = $this->findClosestSelectors();
240            if (!is_null($orphanSelelectors)) {
241                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
242                $orphan->lines = $this->scope->lines;
243                array_unshift($this->scope->children, $orphan);
244                $this->scope->lines = array();
245            }
246        }
247
248        $this->scope = $this->scope->parent;
249        $this->popEnv();
250    }
251
252    protected function mediaParent($scope) {
253        while (!empty($scope->parent)) {
254            if (!empty($scope->type) && $scope->type != "media") {
255                break;
256            }
257            $scope = $scope->parent;
258        }
259
260        return $scope;
261    }
262
263    protected function compileNestedBlock($block, $selectors) {
264        $this->pushEnv($block);
265        $this->scope = $this->makeOutputBlock($block->type, $selectors);
266        $this->scope->parent->children[] = $this->scope;
267
268        $this->compileProps($block, $this->scope);
269
270        $this->scope = $this->scope->parent;
271        $this->popEnv();
272    }
273
274    protected function compileRoot($root) {
275        $this->pushEnv();
276        $this->scope = $this->makeOutputBlock($root->type);
277        $this->compileProps($root, $this->scope);
278        $this->popEnv();
279    }
280
281    protected function compileProps($block, $out) {
282        foreach ($this->sortProps($block->props) as $prop) {
283            $this->compileProp($prop, $block, $out);
284        }
285        $out->lines = $this->deduplicate($out->lines);
286    }
287
288    /**
289     * Deduplicate lines in a block. Comments are not deduplicated. If a
290     * duplicate rule is detected, the comments immediately preceding each
291     * occurence are consolidated.
292     */
293    protected function deduplicate($lines) {
294        $unique = array();
295        $comments = array();
296
297        foreach($lines as $line) {
298            if (strpos($line, '/*') === 0) {
299                $comments[] = $line;
300                continue;
301            }
302            if (!in_array($line, $unique)) {
303                $unique[] = $line;
304            }
305            array_splice($unique, array_search($line, $unique), 0, $comments);
306            $comments = array();
307        }
308        return array_merge($unique, $comments);
309    }
310
311    protected function sortProps($props, $split = false) {
312        $vars = array();
313        $imports = array();
314        $other = array();
315        $stack = array();
316
317        foreach ($props as $prop) {
318            switch ($prop[0]) {
319            case "comment":
320                $stack[] = $prop;
321                break;
322            case "assign":
323                $stack[] = $prop;
324                if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
325                    $vars = array_merge($vars, $stack);
326                } else {
327                    $other = array_merge($other, $stack);
328                }
329                $stack = array();
330                break;
331            case "import":
332                $id = self::$nextImportId++;
333                $prop[] = $id;
334                $stack[] = $prop;
335                $imports = array_merge($imports, $stack);
336                $other[] = array("import_mixin", $id);
337                $stack = array();
338                break;
339            default:
340                $stack[] = $prop;
341                $other = array_merge($other, $stack);
342                $stack = array();
343                break;
344            }
345        }
346        $other = array_merge($other, $stack);
347
348        if ($split) {
349            return array(array_merge($imports, $vars), $other);
350        } else {
351            return array_merge($imports, $vars, $other);
352        }
353    }
354
355    protected function compileMediaQuery($queries) {
356        $compiledQueries = array();
357        foreach ($queries as $query) {
358            $parts = array();
359            foreach ($query as $q) {
360                switch ($q[0]) {
361                case "mediaType":
362                    $parts[] = implode(" ", array_slice($q, 1));
363                    break;
364                case "mediaExp":
365                    if (isset($q[2])) {
366                        $parts[] = "($q[1]" .
367                            $this->compileValue($this->reduce($q[2])) . ")";
368                    } else {
369                        $parts[] = "($q[1])";
370                    }
371                    break;
372                case "variable":
373                    $parts[] = $this->compileValue($this->reduce($q));
374                break;
375                }
376            }
377
378            if (count($parts) > 0) {
379                $compiledQueries[] =  implode(" and ", $parts);
380            }
381        }
382
383        $out = "@media";
384        if (!empty($parts)) {
385            $out .= " " .
386                implode($this->formatter->selectorSeparator, $compiledQueries);
387        }
388        return $out;
389    }
390
391    protected function multiplyMedia($env, $childQueries = null) {
392        if (is_null($env) ||
393            !empty($env->block->type) && $env->block->type != "media")
394        {
395            return $childQueries;
396        }
397
398        // plain old block, skip
399        if (empty($env->block->type)) {
400            return $this->multiplyMedia($env->parent, $childQueries);
401        }
402
403        $out = array();
404        $queries = $env->block->queries;
405        if (is_null($childQueries)) {
406            $out = $queries;
407        } else {
408            foreach ($queries as $parent) {
409                foreach ($childQueries as $child) {
410                    $out[] = array_merge($parent, $child);
411                }
412            }
413        }
414
415        return $this->multiplyMedia($env->parent, $out);
416    }
417
418    protected function expandParentSelectors(&$tag, $replace) {
419        $parts = explode("$&$", $tag);
420        $count = 0;
421        foreach ($parts as &$part) {
422            $part = str_replace($this->parentSelector, $replace, $part, $c);
423            $count += $c;
424        }
425        $tag = implode($this->parentSelector, $parts);
426        return $count;
427    }
428
429    protected function findClosestSelectors() {
430        $env = $this->env;
431        $selectors = null;
432        while ($env !== null) {
433            if (isset($env->selectors)) {
434                $selectors = $env->selectors;
435                break;
436            }
437            $env = $env->parent;
438        }
439
440        return $selectors;
441    }
442
443
444    // multiply $selectors against the nearest selectors in env
445    protected function multiplySelectors($selectors) {
446        // find parent selectors
447
448        $parentSelectors = $this->findClosestSelectors();
449        if (is_null($parentSelectors)) {
450            // kill parent reference in top level selector
451            foreach ($selectors as &$s) {
452                $this->expandParentSelectors($s, "");
453            }
454
455            return $selectors;
456        }
457
458        $out = array();
459        foreach ($parentSelectors as $parent) {
460            foreach ($selectors as $child) {
461                $count = $this->expandParentSelectors($child, $parent);
462
463                // don't prepend the parent tag if & was used
464                if ($count > 0) {
465                    $out[] = trim($child);
466                } else {
467                    $out[] = trim($parent . ' ' . $child);
468                }
469            }
470        }
471
472        return $out;
473    }
474
475    // reduces selector expressions
476    protected function compileSelectors($selectors) {
477        $out = array();
478
479        foreach ($selectors as $s) {
480            if (is_array($s)) {
481                list(, $value) = $s;
482                $out[] = trim($this->compileValue($this->reduce($value)));
483            } else {
484                $out[] = $s;
485            }
486        }
487
488        return $out;
489    }
490
491    protected function eq($left, $right) {
492        return $left == $right;
493    }
494
495    protected function patternMatch($block, $orderedArgs, $keywordArgs) {
496        // match the guards if it has them
497        // any one of the groups must have all its guards pass for a match
498        if (!empty($block->guards)) {
499            $groupPassed = false;
500            foreach ($block->guards as $guardGroup) {
501                foreach ($guardGroup as $guard) {
502                    $this->pushEnv();
503                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
504
505                    $negate = false;
506                    if ($guard[0] == "negate") {
507                        $guard = $guard[1];
508                        $negate = true;
509                    }
510
511                    $passed = $this->reduce($guard) == self::$TRUE;
512                    if ($negate) $passed = !$passed;
513
514                    $this->popEnv();
515
516                    if ($passed) {
517                        $groupPassed = true;
518                    } else {
519                        $groupPassed = false;
520                        break;
521                    }
522                }
523
524                if ($groupPassed) break;
525            }
526
527            if (!$groupPassed) {
528                return false;
529            }
530        }
531
532        if (empty($block->args)) {
533            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
534        }
535
536        $remainingArgs = $block->args;
537        if ($keywordArgs) {
538            $remainingArgs = array();
539            foreach ($block->args as $arg) {
540                if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
541                    continue;
542                }
543
544                $remainingArgs[] = $arg;
545            }
546        }
547
548        $i = -1; // no args
549        // try to match by arity or by argument literal
550        foreach ($remainingArgs as $i => $arg) {
551            switch ($arg[0]) {
552            case "lit":
553                if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
554                    return false;
555                }
556                break;
557            case "arg":
558                // no arg and no default value
559                if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
560                    return false;
561                }
562                break;
563            case "rest":
564                $i--; // rest can be empty
565                break 2;
566            }
567        }
568
569        if ($block->isVararg) {
570            return true; // not having enough is handled above
571        } else {
572            $numMatched = $i + 1;
573            // greater than becuase default values always match
574            $orderedArgsCount = is_countable( $orderedArgs ) ? count( $orderedArgs ) : 0;
575            return $numMatched >= $orderedArgsCount;
576        }
577    }
578
579    protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
580        $matches = null;
581        foreach ($blocks as $block) {
582            // skip seen blocks that don't have arguments
583            if (isset($skip[$block->id]) && !isset($block->args)) {
584                continue;
585            }
586
587            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
588                $matches[] = $block;
589            }
590        }
591
592        return $matches;
593    }
594
595    // attempt to find blocks matched by path and args
596    protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
597        if ($searchIn == null) return null;
598        if (isset($seen[$searchIn->id])) return null;
599        $seen[$searchIn->id] = true;
600
601        $name = $path[0];
602
603        if (isset($searchIn->children[$name])) {
604            $blocks = $searchIn->children[$name];
605            if ( is_countable( $path ) && count( $path ) == 1 ) {
606                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
607                if (!empty($matches)) {
608                    // This will return all blocks that match in the closest
609                    // scope that has any matching block, like lessjs
610                    return $matches;
611                }
612            } else {
613                $matches = array();
614                foreach ($blocks as $subBlock) {
615                    $subMatches = $this->findBlocks($subBlock,
616                        array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
617
618                    if (!is_null($subMatches)) {
619                        foreach ($subMatches as $sm) {
620                            $matches[] = $sm;
621                        }
622                    }
623                }
624
625                return count($matches) > 0 ? $matches : null;
626            }
627        }
628        if ($searchIn->parent === $searchIn) return null;
629        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
630    }
631
632    // sets all argument names in $args to either the default value
633    // or the one passed in through $values
634    protected function zipSetArgs($args, $orderedValues, $keywordValues) {
635        $assignedValues = array();
636
637        $i = 0;
638        foreach ($args as  $a) {
639            if ($a[0] == "arg") {
640                if (isset($keywordValues[$a[1]])) {
641                    // has keyword arg
642                    $value = $keywordValues[$a[1]];
643                } elseif (isset($orderedValues[$i])) {
644                    // has ordered arg
645                    $value = $orderedValues[$i];
646                    $i++;
647                } elseif (isset($a[2])) {
648                    // has default value
649                    $value = $a[2];
650                } else {
651                    $this->throwError("Failed to assign arg " . $a[1]);
652                    $value = null; // :(
653                }
654
655                $value = $this->reduce($value);
656                $this->set($a[1], $value);
657                $assignedValues[] = $value;
658            } else {
659                // a lit
660                $i++;
661            }
662        }
663
664        // check for a rest
665        $last = end($args);
666        if (isset($last[0]) && $last[0] == "rest") {
667            $argsCount = is_countable( $args ) ? count( $args ) : 0;
668            $rest = array_slice($orderedValues, $argsCount - 1);
669            $this->set($last[1], $this->reduce(array("list", " ", $rest)));
670        }
671
672        // wow is this the only true use of PHP's + operator for arrays?
673        $this->env->arguments = $assignedValues + $orderedValues;
674    }
675
676    // compile a prop and update $lines or $blocks appropriately
677    protected function compileProp($prop, $block, $out) {
678        // set error position context
679        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
680
681        switch ($prop[0]) {
682        case 'assign':
683            list(, $name, $value) = $prop;
684            if ($name[0] == $this->vPrefix) {
685                $this->set($name, $value);
686            } else {
687                $out->lines[] = $this->formatter->property($name,
688                        $this->compileValue($this->reduce($value)));
689            }
690            break;
691        case 'block':
692            list(, $child) = $prop;
693            $this->compileBlock($child);
694            break;
695        case 'mixin':
696            list(, $path, $args, $suffix) = $prop;
697
698            $orderedArgs = array();
699            $keywordArgs = array();
700            foreach ((array)$args as $arg) {
701                $argval = null;
702                switch ($arg[0]) {
703                case "arg":
704                    if (!isset($arg[2])) {
705                        $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
706                    } else {
707                        $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
708                    }
709                    break;
710
711                case "lit":
712                    $orderedArgs[] = $this->reduce($arg[1]);
713                    break;
714                default:
715                    $this->throwError("Unknown arg type: " . $arg[0]);
716                }
717            }
718
719            $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
720
721            if ($mixins === null) {
722                $this->throwError("{$prop[1][0]} is undefined");
723            }
724
725            foreach ($mixins as $mixin) {
726                if ($mixin === $block && !$orderedArgs) {
727                    continue;
728                }
729
730                $haveScope = false;
731                if (isset($mixin->parent->scope)) {
732                    $haveScope = true;
733                    $mixinParentEnv = $this->pushEnv();
734                    $mixinParentEnv->storeParent = $mixin->parent->scope;
735                }
736
737                $haveArgs = false;
738                if (isset($mixin->args)) {
739                    $haveArgs = true;
740                    $this->pushEnv();
741                    $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
742                }
743
744                $oldParent = $mixin->parent;
745                if ($mixin != $block) $mixin->parent = $block;
746
747                foreach ($this->sortProps($mixin->props) as $subProp) {
748                    if ($suffix !== null &&
749                        $subProp[0] == "assign" &&
750                        is_string($subProp[1]) &&
751                        $subProp[1][0] != $this->vPrefix)
752                    {
753                        $subProp[2] = array(
754                            'list', ' ',
755                            array($subProp[2], array('keyword', $suffix))
756                        );
757                    }
758
759                    $this->compileProp($subProp, $mixin, $out);
760                }
761
762                $mixin->parent = $oldParent;
763
764                if ($haveArgs) $this->popEnv();
765                if ($haveScope) $this->popEnv();
766            }
767
768            break;
769        case 'raw':
770            $out->lines[] = $prop[1];
771            break;
772        case "directive":
773            list(, $name, $value) = $prop;
774            $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
775            break;
776        case "comment":
777            $out->lines[] = $prop[1];
778            break;
779        case "import";
780            list(, $importPath, $importId) = $prop;
781            $importPath = $this->reduce($importPath);
782
783            if (!isset($this->env->imports)) {
784                $this->env->imports = array();
785            }
786
787            $result = $this->tryImport($importPath, $block, $out);
788
789            $this->env->imports[$importId] = $result === false ?
790                array(false, "@import " . $this->compileValue($importPath).";") :
791                $result;
792
793            break;
794        case "import_mixin":
795            list(,$importId) = $prop;
796            $import = $this->env->imports[$importId];
797            if ($import[0] === false) {
798                if (isset($import[1])) {
799                    $out->lines[] = $import[1];
800                }
801            } else {
802                list(, $bottom, $parser, $importDir) = $import;
803                $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
804            }
805
806            break;
807        default:
808            $this->throwError("unknown op: {$prop[0]}\n");
809        }
810    }
811
812
813    /**
814     * Compiles a primitive value into a CSS property value.
815     *
816     * Values in lessphp are typed by being wrapped in arrays, their format is
817     * typically:
818     *
819     *     array(type, contents [, additional_contents]*)
820     *
821     * The input is expected to be reduced. This function will not work on
822     * things like expressions and variables.
823     */
824    public function compileValue($value) {
825        switch ($value[0]) {
826        case 'list':
827            // [1] - delimiter
828            // [2] - array of values
829            return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
830        case 'raw_color':
831            if (!empty($this->formatter->compressColors)) {
832                return $this->compileValue($this->coerceColor($value));
833            }
834            return $value[1];
835        case 'keyword':
836            // [1] - the keyword
837            return $value[1];
838        case 'number':
839            list(, $num, $unit) = $value;
840            // [1] - the number
841            // [2] - the unit
842            if ($this->numberPrecision !== null) {
843                $num = round($num, $this->numberPrecision);
844            }
845            return $num . $unit;
846        case 'string':
847            // [1] - contents of string (includes quotes)
848            list(, $delim, $content) = $value;
849            foreach ($content as &$part) {
850                if (is_array($part)) {
851                    $part = $this->compileValue($part);
852                }
853            }
854            return $delim . implode($content) . $delim;
855        case 'color':
856            // [1] - red component (either number or a %)
857            // [2] - green component
858            // [3] - blue component
859            // [4] - optional alpha component
860            list(, $r, $g, $b) = $value;
861            $r = round($r);
862            $g = round($g);
863            $b = round($b);
864
865            if ( is_countable( $value ) && count( $value ) == 5 && $value[4] != 1 ) { // rgba
866                return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
867            }
868
869            $h = sprintf("#%02x%02x%02x", $r, $g, $b);
870
871            if (!empty($this->formatter->compressColors)) {
872                // Converting hex color to short notation (e.g. #003399 to #039)
873                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
874                    $h = '#' . $h[1] . $h[3] . $h[5];
875                }
876            }
877
878            return $h;
879
880        case 'function':
881            list(, $name, $args) = $value;
882            return $name.'('.$this->compileValue($args).')';
883        default: // assumed to be unit
884            $this->throwError("unknown value type: $value[0]");
885        }
886    }
887
888    protected function lib_pow($args) {
889        list($base, $exp) = $this->assertArgs($args, 2, "pow");
890        return pow($this->assertNumber($base), $this->assertNumber($exp));
891    }
892
893    protected function lib_pi() {
894        return pi();
895    }
896
897    protected function lib_mod($args) {
898        list($a, $b) = $this->assertArgs($args, 2, "mod");
899        return $this->assertNumber($a) % $this->assertNumber($b);
900    }
901
902    protected function lib_tan($num) {
903        return tan($this->assertNumber($num));
904    }
905
906    protected function lib_sin($num) {
907        return sin($this->assertNumber($num));
908    }
909
910    protected function lib_cos($num) {
911        return cos($this->assertNumber($num));
912    }
913
914    protected function lib_atan($num) {
915        $num = atan($this->assertNumber($num));
916        return array("number", $num, "rad");
917    }
918
919    protected function lib_asin($num) {
920        $num = asin($this->assertNumber($num));
921        return array("number", $num, "rad");
922    }
923
924    protected function lib_acos($num) {
925        $num = acos($this->assertNumber($num));
926        return array("number", $num, "rad");
927    }
928
929    protected function lib_sqrt($num) {
930        return sqrt($this->assertNumber($num));
931    }
932
933    protected function lib_extract($value) {
934        list($list, $idx) = $this->assertArgs($value, 2, "extract");
935        $idx = $this->assertNumber($idx);
936        // 1 indexed
937        if ($list[0] == "list" && isset($list[2][$idx - 1])) {
938            return $list[2][$idx - 1];
939        }
940    }
941
942    protected function lib_isnumber($value) {
943        return $this->toBool($value[0] == "number");
944    }
945
946    protected function lib_isstring($value) {
947        return $this->toBool($value[0] == "string");
948    }
949
950    protected function lib_iscolor($value) {
951        return $this->toBool($this->coerceColor($value));
952    }
953
954    protected function lib_iskeyword($value) {
955        return $this->toBool($value[0] == "keyword");
956    }
957
958    protected function lib_ispixel($value) {
959        return $this->toBool($value[0] == "number" && $value[2] == "px");
960    }
961
962    protected function lib_ispercentage($value) {
963        return $this->toBool($value[0] == "number" && $value[2] == "%");
964    }
965
966    protected function lib_isem($value) {
967        return $this->toBool($value[0] == "number" && $value[2] == "em");
968    }
969
970    protected function lib_isrem($value) {
971        return $this->toBool($value[0] == "number" && $value[2] == "rem");
972    }
973
974    protected function lib_rgbahex($color) {
975        $color = $this->coerceColor($color);
976        if (is_null($color))
977            $this->throwError("color expected for rgbahex");
978
979        return sprintf("#%02x%02x%02x%02x",
980            isset($color[4]) ? $color[4]*255 : 255,
981            $color[1],$color[2], $color[3]);
982    }
983
984    protected function lib_argb($color){
985        return $this->lib_rgbahex($color);
986    }
987
988    /**
989     * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
990     *
991     * @param  array  $value either an argument list (two strings) or a single string
992     * @return string        formatted url(), either as a link or base64-encoded
993     */
994    protected function lib_data_uri($value) {
995        $mime = ($value[0] === 'list') ? $value[2][0][2] : null;
996        $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
997
998        $fullpath = $this->findImport($url);
999
1000        if($fullpath && ($fsize = filesize($fullpath)) !== false) {
1001            // IE8 can't handle data uris larger than 32KB
1002            if($fsize/1024 < 32) {
1003                if(is_null($mime)) {
1004                    $mime = jetpack_mime_content_type( $fullpath );
1005                }
1006
1007                if(!is_null($mime)) // fallback if the mime type is still unknown
1008                    $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
1009            }
1010        }
1011
1012        return 'url("'.$url.'")';
1013    }
1014
1015    // utility func to unquote a string
1016    protected function lib_e($arg) {
1017        switch ($arg[0]) {
1018            case "list":
1019                $items = $arg[2];
1020                if (isset($items[0])) {
1021                    return $this->lib_e($items[0]);
1022                }
1023                $this->throwError("unrecognised input");
1024            case "string":
1025                $arg[1] = "";
1026                return $arg;
1027            case "keyword":
1028                return $arg;
1029            default:
1030                return array("keyword", $this->compileValue($arg));
1031        }
1032    }
1033
1034    protected function lib__sprintf($args) {
1035        if ($args[0] != "list") return $args;
1036        $values = $args[2];
1037        $string = array_shift($values);
1038        $template = $this->compileValue($this->lib_e($string));
1039
1040        $i = 0;
1041        if (preg_match_all('/%[dsa]/', $template, $m)) {
1042            foreach ($m[0] as $match) {
1043                $val = isset($values[$i]) ?
1044                    $this->reduce($values[$i]) : array('keyword', '');
1045
1046                // lessjs compat, renders fully expanded color, not raw color
1047                if ($color = $this->coerceColor($val)) {
1048                    $val = $color;
1049                }
1050
1051                $i++;
1052                $rep = $this->compileValue($this->lib_e($val));
1053                $template = preg_replace('/'.self::preg_quote($match).'/',
1054                    $rep, $template, 1);
1055            }
1056        }
1057
1058        $d = $string[0] == "string" ? $string[1] : '"';
1059        return array("string", $d, array($template));
1060    }
1061
1062    protected function lib_floor($arg) {
1063        $value = $this->assertNumber($arg);
1064        return array("number", floor($value), $arg[2]);
1065    }
1066
1067    protected function lib_ceil($arg) {
1068        $value = $this->assertNumber($arg);
1069        return array("number", ceil($value), $arg[2]);
1070    }
1071
1072    protected function lib_round($arg) {
1073        if($arg[0] != "list") {
1074            $value = $this->assertNumber($arg);
1075            return array("number", round($value), $arg[2]);
1076        } else {
1077            $value = $this->assertNumber($arg[2][0]);
1078            $precision = $this->assertNumber($arg[2][1]);
1079            return array("number", round($value, $precision), $arg[2][0][2]);
1080        }
1081    }
1082
1083    protected function lib_unit($arg) {
1084        if ($arg[0] == "list") {
1085            list($number, $newUnit) = $arg[2];
1086            return array("number", $this->assertNumber($number),
1087                $this->compileValue($this->lib_e($newUnit)));
1088        } else {
1089            return array("number", $this->assertNumber($arg), "");
1090        }
1091    }
1092
1093    /**
1094     * Helper function to get arguments for color manipulation functions.
1095     * takes a list that contains a color like thing and a percentage
1096     */
1097    public function colorArgs($args) {
1098        if ( $args[0] != 'list' || ! is_countable( $args[2] ) || count( $args[2] ) < 2 ) {
1099            return array(array('color', 0, 0, 0), 0);
1100        }
1101        list($color, $delta) = $args[2];
1102        $color = $this->assertColor($color);
1103        $delta = (float) $delta[1];
1104
1105        return array($color, $delta);
1106    }
1107
1108    protected function lib_darken($args) {
1109        list($color, $delta) = $this->colorArgs($args);
1110
1111        $hsl = $this->toHSL($color);
1112        $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1113        return $this->toRGB($hsl);
1114    }
1115
1116    protected function lib_lighten($args) {
1117        list($color, $delta) = $this->colorArgs($args);
1118
1119        $hsl = $this->toHSL($color);
1120        $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1121        return $this->toRGB($hsl);
1122    }
1123
1124    protected function lib_saturate($args) {
1125        list($color, $delta) = $this->colorArgs($args);
1126
1127        $hsl = $this->toHSL($color);
1128        $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1129        return $this->toRGB($hsl);
1130    }
1131
1132    protected function lib_desaturate($args) {
1133        list($color, $delta) = $this->colorArgs($args);
1134
1135        $hsl = $this->toHSL($color);
1136        $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1137        return $this->toRGB($hsl);
1138    }
1139
1140    protected function lib_spin($args) {
1141        list($color, $delta) = $this->colorArgs($args);
1142
1143        $hsl = $this->toHSL($color);
1144
1145        $hsl[1] = $hsl[1] + $delta % 360;
1146        if ($hsl[1] < 0) $hsl[1] += 360;
1147
1148        return $this->toRGB($hsl);
1149    }
1150
1151    protected function lib_fadeout($args) {
1152        list($color, $delta) = $this->colorArgs($args);
1153        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
1154        return $color;
1155    }
1156
1157    protected function lib_fadein($args) {
1158        list($color, $delta) = $this->colorArgs($args);
1159        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
1160        return $color;
1161    }
1162
1163    protected function lib_hue($color) {
1164        $hsl = $this->toHSL($this->assertColor($color));
1165        return round($hsl[1]);
1166    }
1167
1168    protected function lib_saturation($color) {
1169        $hsl = $this->toHSL($this->assertColor($color));
1170        return round($hsl[2]);
1171    }
1172
1173    protected function lib_lightness($color) {
1174        $hsl = $this->toHSL($this->assertColor($color));
1175        return round($hsl[3]);
1176    }
1177
1178    // get the alpha of a color
1179    // defaults to 1 for non-colors or colors without an alpha
1180    protected function lib_alpha($value) {
1181        if (!is_null($color = $this->coerceColor($value))) {
1182            return isset($color[4]) ? $color[4] : 1;
1183        }
1184    }
1185
1186    // set the alpha of the color
1187    protected function lib_fade($args) {
1188        list($color, $alpha) = $this->colorArgs($args);
1189        $color[4] = $this->clamp($alpha / 100.0);
1190        return $color;
1191    }
1192
1193    protected function lib_percentage($arg) {
1194        $num = $this->assertNumber($arg);
1195        return array("number", $num*100, "%");
1196    }
1197
1198    // mixes two colors by weight
1199    // mix(@color1, @color2, [@weight: 50%]);
1200    // https://sass-lang.com/documentation/functions/color#mix
1201    protected function lib_mix($args) {
1202        if ( $args[0] != "list" || ! is_countable( $args[2] ) || count( $args[2] ) < 2 ) {
1203            $this->throwError("mix expects (color1, color2, weight)");
1204        }
1205
1206        list($first, $second) = $args[2];
1207        $first = $this->assertColor($first);
1208        $second = $this->assertColor($second);
1209
1210        $first_a = $this->lib_alpha($first);
1211        $second_a = $this->lib_alpha($second);
1212
1213        if (isset($args[2][2])) {
1214            $weight = $args[2][2][1] / 100.0;
1215        } else {
1216            $weight = 0.5;
1217        }
1218
1219        $w = $weight * 2 - 1;
1220        $a = $first_a - $second_a;
1221
1222        $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1223        $w2 = 1.0 - $w1;
1224
1225        $new = array('color',
1226            $w1 * $first[1] + $w2 * $second[1],
1227            $w1 * $first[2] + $w2 * $second[2],
1228            $w1 * $first[3] + $w2 * $second[3],
1229        );
1230
1231        if ($first_a != 1.0 || $second_a != 1.0) {
1232            $new[] = $first_a * $weight + $second_a * ($weight - 1);
1233        }
1234
1235        return $this->fixColor($new);
1236    }
1237
1238    protected function lib_contrast($args) {
1239        $darkColor  = array('color', 0, 0, 0);
1240        $lightColor = array('color', 255, 255, 255);
1241        $threshold  = 0.43;
1242
1243        if ( $args[0] == 'list' ) {
1244            $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0])  : $lightColor;
1245            $darkColor  = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1])  : $darkColor;
1246            $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2])  : $lightColor;
1247            $threshold  = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
1248        }
1249        else {
1250            $inputColor  = $this->assertColor($args);
1251        }
1252
1253        $inputColor = $this->coerceColor($inputColor);
1254        $darkColor  = $this->coerceColor($darkColor);
1255        $lightColor = $this->coerceColor($lightColor);
1256
1257        //Figure out which is actually light and dark!
1258        if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) {
1259            $t  = $lightColor;
1260            $lightColor = $darkColor;
1261            $darkColor  = $t;
1262        }
1263
1264        $inputColor_alpha = $this->lib_alpha($inputColor);
1265        if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) {
1266            return $lightColor;
1267        }
1268        return $darkColor;
1269    }
1270
1271    protected function lib_luma($color) {
1272        $color = $this->coerceColor($color);
1273        return (0.2126 * $color[1] / 255) + (0.7152 * $color[2] / 255) + (0.0722 * $color[3] / 255);
1274    }
1275
1276
1277    public function assertColor($value, $error = "expected color value") {
1278        $color = $this->coerceColor($value);
1279        if (is_null($color)) $this->throwError($error);
1280        return $color;
1281    }
1282
1283    public function assertNumber($value, $error = "expecting number") {
1284        if ($value[0] == "number") return $value[1];
1285        $this->throwError($error);
1286    }
1287
1288    public function assertArgs($value, $expectedArgs, $name="") {
1289        if ($expectedArgs == 1) {
1290            return $value;
1291        } else {
1292            if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
1293            $values = $value[2];
1294            $numValues = is_countable( $values ) ? count( $values ) : 0;
1295            if ($expectedArgs != $numValues) {
1296                if ($name) {
1297                    $name = $name . ": ";
1298                }
1299
1300                $this->throwError("{$name}expecting $expectedArgs arguments, got $numValues");
1301            }
1302
1303            return $values;
1304        }
1305    }
1306
1307    protected function toHSL($color) {
1308        if ($color[0] == 'hsl') return $color;
1309
1310        $r = $color[1] / 255;
1311        $g = $color[2] / 255;
1312        $b = $color[3] / 255;
1313
1314        $min = min($r, $g, $b);
1315        $max = max($r, $g, $b);
1316
1317        $L = ($min + $max) / 2;
1318        if ($min == $max) {
1319            $S = $H = 0;
1320        } else {
1321            if ($L < 0.5)
1322                $S = ($max - $min)/($max + $min);
1323            else
1324                $S = ($max - $min)/(2.0 - $max - $min);
1325
1326            if ($r == $max) $H = ($g - $b)/($max - $min);
1327            elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
1328            else $H = 4.0 + ($r - $g)/($max - $min);
1329
1330        }
1331
1332        $out = array('hsl',
1333            ($H < 0 ? $H + 6 : $H)*60,
1334            $S*100,
1335            $L*100,
1336        );
1337
1338        if ( is_countable( $color ) && count( $color ) > 4 ) {
1339            $out[] = $color[4]; // copy alpha
1340        }
1341        return $out;
1342    }
1343
1344    protected function toRGB_helper($comp, $temp1, $temp2) {
1345        if ($comp < 0) $comp += 1.0;
1346        elseif ($comp > 1) $comp -= 1.0;
1347
1348        if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1349        if (2 * $comp < 1) return $temp2;
1350        if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
1351
1352        return $temp1;
1353    }
1354
1355    /**
1356     * Converts a hsl array into a color value in rgb.
1357     * Expects H to be in range of 0 to 360, S and L in 0 to 100
1358     */
1359    protected function toRGB($color) {
1360        if ($color[0] == 'color') return $color;
1361
1362        $H = $color[1] / 360;
1363        $S = $color[2] / 100;
1364        $L = $color[3] / 100;
1365
1366        if ($S == 0) {
1367            $r = $g = $b = $L;
1368        } else {
1369            $temp2 = $L < 0.5 ?
1370                $L*(1.0 + $S) :
1371                $L + $S - $L * $S;
1372
1373            $temp1 = 2.0 * $L - $temp2;
1374
1375            $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
1376            $g = $this->toRGB_helper($H, $temp1, $temp2);
1377            $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
1378        }
1379
1380        // $out = array('color', round($r*255), round($g*255), round($b*255));
1381        $out = array('color', $r*255, $g*255, $b*255);
1382        if ( is_countable( $color ) && count( $color ) > 4 ) {
1383            $out[] = $color[4]; // copy alpha
1384        }
1385        return $out;
1386    }
1387
1388    protected function clamp($v, $max = 1, $min = 0) {
1389        return min($max, max($min, $v));
1390    }
1391
1392    /**
1393     * Convert the rgb, rgba, hsl color literals of function type
1394     * as returned by the parser into values of color type.
1395     */
1396    protected function funcToColor($func) {
1397        $fname = $func[1];
1398        if ($func[2][0] != 'list') return false; // need a list of arguments
1399        $rawComponents = $func[2][2];
1400
1401        if ($fname == 'hsl' || $fname == 'hsla') {
1402            $hsl = array('hsl');
1403            $i = 0;
1404            foreach ($rawComponents as $c) {
1405                $val = $this->reduce($c);
1406                $val = isset( $val[1] ) ? (float) $val[1] : 0;
1407
1408                if ($i == 0) $clamp = 360;
1409                elseif ($i < 3) $clamp = 100;
1410                else $clamp = 1;
1411
1412                $hsl[] = $this->clamp($val, $clamp);
1413                $i++;
1414            }
1415
1416            while (count($hsl) < 4) $hsl[] = 0;
1417            return $this->toRGB($hsl);
1418
1419        } elseif ($fname == 'rgb' || $fname == 'rgba') {
1420            $components = array();
1421            $i = 1;
1422            foreach    ($rawComponents as $c) {
1423                $c = $this->reduce($c);
1424                if ($i < 4) {
1425                    if ($c[0] == "number" && $c[2] == "%") {
1426                        $components[] = 255 * ($c[1] / 100);
1427                    } else {
1428                        $components[] = (float) $c[1];
1429                    }
1430                } elseif ($i == 4) {
1431                    if ($c[0] == "number" && $c[2] == "%") {
1432                        $components[] = 1.0 * ($c[1] / 100);
1433                    } else {
1434                        $components[] = (float) $c[1];
1435                    }
1436                } else break;
1437
1438                $i++;
1439            }
1440            while (count($components) < 3) $components[] = 0;
1441            array_unshift($components, 'color');
1442            return $this->fixColor($components);
1443        }
1444
1445        return false;
1446    }
1447
1448    protected function reduce($value, $forExpression = false) {
1449        switch ($value[0]) {
1450        case "interpolate":
1451            $reduced = $this->reduce($value[1]);
1452            $var = $this->compileValue($reduced);
1453            $res = $this->reduce(array("variable", $this->vPrefix . $var));
1454
1455            if ($res[0] == "raw_color") {
1456                $res = $this->coerceColor($res);
1457            }
1458
1459            if (empty($value[2])) $res = $this->lib_e($res);
1460
1461            return $res;
1462        case "variable":
1463            $key = $value[1];
1464            if (is_array($key)) {
1465                $key = $this->reduce($key);
1466                $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
1467            }
1468
1469            // Memoization: return cached value if already reduced.
1470            if (isset($this->env->reducedVars[$key])) {
1471                return $this->env->reducedVars[$key];
1472            }
1473
1474            $seen =& $this->env->seenNames;
1475
1476            if (!empty($seen[$key])) {
1477                $this->throwError("infinite loop detected: $key");
1478            }
1479
1480            $seen[$key] = true;
1481            $out = $this->reduce($this->get($key));
1482            $seen[$key] = false;
1483
1484            // Cache the reduced value for future lookups.
1485            $this->env->reducedVars[$key] = $out;
1486
1487            return $out;
1488        case "list":
1489            foreach ($value[2] as &$item) {
1490                $item = $this->reduce($item, $forExpression);
1491            }
1492            return $value;
1493        case "expression":
1494            return $this->evaluate($value);
1495        case "string":
1496            foreach ($value[2] as &$part) {
1497                if (is_array($part)) {
1498                    $strip = $part[0] == "variable";
1499                    $part = $this->reduce($part);
1500                    if ($strip) $part = $this->lib_e($part);
1501                }
1502            }
1503            return $value;
1504        case "escape":
1505            list(,$inner) = $value;
1506            return $this->lib_e($this->reduce($inner));
1507        case "function":
1508            $color = $this->funcToColor($value);
1509            if ($color) return $color;
1510
1511            list(, $name, $args) = $value;
1512            if ($name == "%") $name = "_sprintf";
1513
1514            $f = isset($this->libFunctions[$name]) ?
1515                $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
1516
1517            if (is_callable($f)) {
1518                if ($args[0] == 'list')
1519                    $args = self::compressList($args[2], $args[1]);
1520
1521                $ret = call_user_func($f, $this->reduce($args, true), $this);
1522
1523                if (is_null($ret)) {
1524                    return array("string", "", array(
1525                        $name, "(", $args, ")"
1526                    ));
1527                }
1528
1529                // convert to a typed value if the result is a php primitive
1530                if (is_numeric($ret)) $ret = array('number', $ret, "");
1531                elseif (!is_array($ret)) $ret = array('keyword', $ret);
1532
1533                return $ret;
1534            }
1535
1536            // plain function, reduce args
1537            $value[2] = $this->reduce($value[2]);
1538            return $value;
1539        case "unary":
1540            list(, $op, $exp) = $value;
1541            $exp = $this->reduce($exp);
1542
1543            if ($exp[0] == "number") {
1544                switch ($op) {
1545                case "+":
1546                    return $exp;
1547                case "-":
1548                    $exp[1] *= -1;
1549                    return $exp;
1550                }
1551            }
1552            return array("string", "", array($op, $exp));
1553        }
1554
1555        if ($forExpression) {
1556            switch ($value[0]) {
1557            case "keyword":
1558                if ($color = $this->coerceColor($value)) {
1559                    return $color;
1560                }
1561                break;
1562            case "raw_color":
1563                return $this->coerceColor($value);
1564            }
1565        }
1566
1567        return $value;
1568    }
1569
1570
1571    // coerce a value for use in color operation
1572    protected function coerceColor($value) {
1573        switch($value[0]) {
1574            case 'color': return $value;
1575            case 'raw_color':
1576                $c = array("color", 0, 0, 0);
1577                $colorStr = substr($value[1], 1);
1578                $num = hexdec($colorStr);
1579                $width = strlen($colorStr) == 3 ? 16 : 256;
1580
1581                for ($i = 3; $i > 0; $i--) { // 3 2 1
1582                    $t = (int) $num % $width;
1583                    $num /= $width;
1584
1585                    $c[$i] = $t * (256/$width) + $t * floor(16/$width);
1586                }
1587
1588                return $c;
1589            case 'keyword':
1590                $name = $value[1];
1591                if (isset(self::$cssColors[$name])) {
1592                    $rgba = explode(',', self::$cssColors[$name]);
1593
1594                    if(isset($rgba[3]))
1595                        return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
1596
1597                    return array('color', $rgba[0], $rgba[1], $rgba[2]);
1598                }
1599                return null;
1600        }
1601    }
1602
1603    // make something string like into a string
1604    protected function coerceString($value) {
1605        switch ($value[0]) {
1606        case "string":
1607            return $value;
1608        case "keyword":
1609            return array("string", "", array($value[1]));
1610        }
1611        return null;
1612    }
1613
1614    // turn list of length 1 into value type
1615    protected function flattenList($value) {
1616        if ( $value[0] == "list" && is_countable( $value[2] ) && count( $value[2] ) == 1 ) {
1617            return $this->flattenList($value[2][0]);
1618        }
1619        return $value;
1620    }
1621
1622    public function toBool($a) {
1623        if ($a) return self::$TRUE;
1624        else return self::$FALSE;
1625    }
1626
1627    // evaluate an expression
1628    protected function evaluate($exp) {
1629        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1630
1631        $left = $this->reduce($left, true);
1632        $right = $this->reduce($right, true);
1633
1634        if ($leftColor = $this->coerceColor($left)) {
1635            $left = $leftColor;
1636        }
1637
1638        if ($rightColor = $this->coerceColor($right)) {
1639            $right = $rightColor;
1640        }
1641
1642        $ltype = $left[0];
1643        $rtype = $right[0];
1644
1645        // operators that work on all types
1646        if ($op == "and") {
1647            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1648        }
1649
1650        if ($op == "=") {
1651            return $this->toBool($this->eq($left, $right) );
1652        }
1653
1654        if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
1655            return $str;
1656        }
1657
1658        // type based operators
1659        $fname = "op_{$ltype}_{$rtype}";
1660        if (is_callable(array($this, $fname))) {
1661            $out = $this->$fname($op, $left, $right);
1662            if (!is_null($out)) return $out;
1663        }
1664
1665        // make the expression look it did before being parsed
1666        $paddedOp = $op;
1667        if ($whiteBefore) $paddedOp = " " . $paddedOp;
1668        if ($whiteAfter) $paddedOp .= " ";
1669
1670        return array("string", "", array($left, $paddedOp, $right));
1671    }
1672
1673    protected function stringConcatenate($left, $right) {
1674        if ($strLeft = $this->coerceString($left)) {
1675            if ($right[0] == "string") {
1676                $right[1] = "";
1677            }
1678            $strLeft[2][] = $right;
1679            return $strLeft;
1680        }
1681
1682        if ($strRight = $this->coerceString($right)) {
1683            array_unshift($strRight[2], $left);
1684            return $strRight;
1685        }
1686    }
1687
1688
1689    // make sure a color's components don't go out of bounds
1690    protected function fixColor($c) {
1691        foreach (range(1, 3) as $i) {
1692            if ($c[$i] < 0) $c[$i] = 0;
1693            if ($c[$i] > 255) $c[$i] = 255;
1694        }
1695
1696        return $c;
1697    }
1698
1699    protected function op_number_color($op, $lft, $rgt) {
1700        if ($op == '+' || $op == '*') {
1701            return $this->op_color_number($op, $rgt, $lft);
1702        }
1703    }
1704
1705    protected function op_color_number($op, $lft, $rgt) {
1706        if ($rgt[0] == '%') $rgt[1] /= 100;
1707
1708        $lftCount = is_countable( $lft ) ? count( $lft ) : 1;
1709        return $this->op_color_color($op, $lft,
1710            array_fill(1, $lftCount - 1, $rgt[1]));
1711    }
1712
1713    protected function op_color_color($op, $left, $right) {
1714        $out = array('color');
1715        $leftCount  = is_countable( $left ) ? count( $left ) : 0;
1716        $rightCount = is_countable( $right ) ? count( $right) : 0;
1717        $max = $leftCount > $rightCount ? $leftCount : $rightCount;
1718        foreach (range(1, $max - 1) as $i) {
1719            $lval = isset($left[$i]) ? $left[$i] : 0;
1720            $rval = isset($right[$i]) ? $right[$i] : 0;
1721            switch ($op) {
1722            case '+':
1723                $out[] = $lval + $rval;
1724                break;
1725            case '-':
1726                $out[] = $lval - $rval;
1727                break;
1728            case '*':
1729                $out[] = $lval * $rval;
1730                break;
1731            case '%':
1732                $out[] = $lval % $rval;
1733                break;
1734            case '/':
1735                if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
1736                $out[] = $lval / $rval;
1737                break;
1738            default:
1739                $this->throwError('evaluate error: color op number failed on op '.$op);
1740            }
1741        }
1742        return $this->fixColor($out);
1743    }
1744
1745    function lib_red($color){
1746        $color = $this->coerceColor($color);
1747        if (is_null($color)) {
1748            $this->throwError('color expected for red()');
1749        }
1750
1751        return $color[1];
1752    }
1753
1754    function lib_green($color){
1755        $color = $this->coerceColor($color);
1756        if (is_null($color)) {
1757            $this->throwError('color expected for green()');
1758        }
1759
1760        return $color[2];
1761    }
1762
1763    function lib_blue($color){
1764        $color = $this->coerceColor($color);
1765        if (is_null($color)) {
1766            $this->throwError('color expected for blue()');
1767        }
1768
1769        return $color[3];
1770    }
1771
1772
1773    // operator on two numbers
1774    protected function op_number_number($op, $left, $right) {
1775        $unit = empty($left[2]) ? $right[2] : $left[2];
1776
1777        $value = 0;
1778        switch ($op) {
1779        case '+':
1780            $value = $left[1] + $right[1];
1781            break;
1782        case '*':
1783            $value = $left[1] * $right[1];
1784            break;
1785        case '-':
1786            $value = $left[1] - $right[1];
1787            break;
1788        case '%':
1789            $value = $left[1] % $right[1];
1790            break;
1791        case '/':
1792            if ($right[1] == 0) $this->throwError('parse error: divide by zero');
1793            $value = $left[1] / $right[1];
1794            break;
1795        case '<':
1796            return $this->toBool($left[1] < $right[1]);
1797        case '>':
1798            return $this->toBool($left[1] > $right[1]);
1799        case '>=':
1800            return $this->toBool($left[1] >= $right[1]);
1801        case '=<':
1802            return $this->toBool($left[1] <= $right[1]);
1803        default:
1804            $this->throwError('parse error: unknown number operator: '.$op);
1805        }
1806
1807        return array("number", $value, $unit);
1808    }
1809
1810
1811    /* environment functions */
1812
1813    protected function makeOutputBlock($type, $selectors = null) {
1814        $b = new stdclass;
1815        $b->lines = array();
1816        $b->children = array();
1817        $b->selectors = $selectors;
1818        $b->type = $type;
1819        $b->parent = $this->scope;
1820        return $b;
1821    }
1822
1823    // the state of execution
1824    protected function pushEnv($block = null) {
1825        $e = new stdclass;
1826        $e->parent = $this->env;
1827        $e->store = array();
1828        $e->block = $block;
1829
1830        $this->env = $e;
1831        return $e;
1832    }
1833
1834    // pop something off the stack
1835    protected function popEnv() {
1836        $old = $this->env;
1837        $this->env = $this->env->parent;
1838        return $old;
1839    }
1840
1841    // set something in the current env
1842    protected function set($name, $value) {
1843        $this->env->store[$name] = $value;
1844    }
1845
1846
1847    // get the highest occurrence entry for a name
1848    protected function get($name) {
1849        $current = $this->env;
1850
1851        $isArguments = $name == $this->vPrefix . 'arguments';
1852        while ($current) {
1853            if ($isArguments && isset($current->arguments)) {
1854                return array('list', ' ', $current->arguments);
1855            }
1856
1857            if (isset($current->store[$name]))
1858                return $current->store[$name];
1859            else {
1860                $current = isset($current->storeParent) ?
1861                    $current->storeParent : $current->parent;
1862            }
1863        }
1864
1865        $this->throwError("variable $name is undefined");
1866    }
1867
1868    // inject array of unparsed strings into environment as variables
1869    protected function injectVariables($args) {
1870        $this->pushEnv();
1871        $parser = new lessc_parser($this, __METHOD__);
1872        foreach ($args as $name => $strValue) {
1873            if ($name[0] != '@') $name = '@'.$name;
1874            $parser->count = 0;
1875            $parser->buffer = (string)$strValue;
1876            if (!$parser->propertyValue($value)) {
1877                throw new Exception("failed to parse passed in variable $name$strValue");
1878            }
1879
1880            $this->set($name, $value);
1881        }
1882    }
1883
1884    /**
1885     * Initialize any static state, can initialize parser for a file
1886     * $opts isn't used yet
1887     */
1888    public function __construct($fname = null) {
1889        if ($fname !== null) {
1890            // used for deprecated parse method
1891            $this->_parseFile = $fname;
1892        }
1893    }
1894
1895    public function compile($string, $name = null) {
1896        $locale = setlocale(LC_NUMERIC, 0);
1897        setlocale(LC_NUMERIC, "C");
1898
1899        $this->parser = $this->makeParser($name);
1900        $root = $this->parser->parse($string);
1901
1902        $this->env = null;
1903        $this->scope = null;
1904
1905        $this->formatter = $this->newFormatter();
1906
1907        if (!empty($this->registeredVars)) {
1908            $this->injectVariables($this->registeredVars);
1909        }
1910
1911        $this->sourceParser = $this->parser; // used for error messages
1912        $this->compileBlock($root);
1913
1914        ob_start();
1915        $this->formatter->block($this->scope);
1916        $out = ob_get_clean();
1917        setlocale(LC_NUMERIC, $locale);
1918        return $out;
1919    }
1920
1921    public function compileFile($fname, $outFname = null) {
1922        if (!is_readable($fname)) {
1923            throw new Exception('load error: failed to find '.$fname);
1924        }
1925
1926        $pi = pathinfo($fname);
1927
1928        $oldImport = $this->importDir;
1929
1930        $this->importDir = (array)$this->importDir;
1931        $this->importDir[] = $pi['dirname'].'/';
1932
1933        $this->addParsedFile($fname);
1934
1935        $out = $this->compile(file_get_contents($fname), $fname);
1936
1937        $this->importDir = $oldImport;
1938
1939        if ($outFname !== null) {
1940            return file_put_contents($outFname, $out);
1941        }
1942
1943        return $out;
1944    }
1945
1946    // compile only if changed input has changed or output doesn't exist
1947    public function checkedCompile($in, $out) {
1948        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1949            $this->compileFile($in, $out);
1950            return true;
1951        }
1952        return false;
1953    }
1954
1955    /**
1956     * Execute lessphp on a .less file or a lessphp cache structure
1957     *
1958     * The lessphp cache structure contains information about a specific
1959     * less file having been parsed. It can be used as a hint for future
1960     * calls to determine whether or not a rebuild is required.
1961     *
1962     * The cache structure contains two important keys that may be used
1963     * externally:
1964     *
1965     * compiled: The final compiled CSS
1966     * updated: The time (in seconds) the CSS was last compiled
1967     *
1968     * The cache structure is a plain-ol' PHP associative array and can
1969     * be serialized and unserialized without a hitch.
1970     *
1971     * @param mixed $in Input
1972     * @param bool $force Force rebuild?
1973     * @return array lessphp cache structure
1974     */
1975    public function cachedCompile($in, $force = false) {
1976        // assume no root
1977        $root = null;
1978
1979        if (is_string($in)) {
1980            $root = $in;
1981        } elseif (is_array($in) and isset($in['root'])) {
1982            if ($force or ! isset($in['files'])) {
1983                // If we are forcing a recompile or if for some reason the
1984                // structure does not contain any file information we should
1985                // specify the root to trigger a rebuild.
1986                $root = $in['root'];
1987            } elseif (isset($in['files']) and is_array($in['files'])) {
1988                foreach ($in['files'] as $fname => $ftime ) {
1989                    if (!file_exists($fname) or filemtime($fname) > $ftime) {
1990                        // One of the files we knew about previously has changed
1991                        // so we should look at our incoming root again.
1992                        $root = $in['root'];
1993                        break;
1994                    }
1995                }
1996            }
1997        } else {
1998            // TODO: Throw an exception? We got neither a string nor something
1999            // that looks like a compatible lessphp cache structure.
2000            return null;
2001        }
2002
2003        if ($root !== null) {
2004            // If we have a root value which means we should rebuild.
2005            $out = array();
2006            $out['root'] = $root;
2007            $out['compiled'] = $this->compileFile($root);
2008            $out['files'] = $this->allParsedFiles();
2009            $out['updated'] = time();
2010            return $out;
2011        } else {
2012            // No changes, pass back the structure
2013            // we were given initially.
2014            return $in;
2015        }
2016
2017    }
2018
2019    // parse and compile buffer
2020    // This is deprecated
2021    public function parse($str = null, $initialVariables = null) {
2022        if (is_array($str)) {
2023            $initialVariables = $str;
2024            $str = null;
2025        }
2026
2027        $oldVars = $this->registeredVars;
2028        if ($initialVariables !== null) {
2029            $this->setVariables($initialVariables);
2030        }
2031
2032        if ($str == null) {
2033            if (empty($this->_parseFile)) {
2034                throw new exception("nothing to parse");
2035            }
2036
2037            $out = $this->compileFile($this->_parseFile);
2038        } else {
2039            $out = $this->compile($str);
2040        }
2041
2042        $this->registeredVars = $oldVars;
2043        return $out;
2044    }
2045
2046    protected function makeParser($name) {
2047        $parser = new lessc_parser($this, $name);
2048        $parser->writeComments = $this->preserveComments;
2049
2050        return $parser;
2051    }
2052
2053    public function setFormatter($name) {
2054        $this->formatterName = $name;
2055    }
2056
2057    protected function newFormatter() {
2058        $className = "lessc_formatter_lessjs";
2059        if (!empty($this->formatterName)) {
2060            if (!is_string($this->formatterName))
2061                return $this->formatterName;
2062            $className = "lessc_formatter_$this->formatterName";
2063        }
2064
2065        return new $className;
2066    }
2067
2068    public function setPreserveComments($preserve) {
2069        $this->preserveComments = $preserve;
2070    }
2071
2072    public function registerFunction($name, $func) {
2073        $this->libFunctions[$name] = $func;
2074    }
2075
2076    public function unregisterFunction($name) {
2077        unset($this->libFunctions[$name]);
2078    }
2079
2080    public function setVariables($variables) {
2081        $this->registeredVars = array_merge($this->registeredVars, $variables);
2082    }
2083
2084    public function unsetVariable($name) {
2085        unset($this->registeredVars[$name]);
2086    }
2087
2088    public function setImportDir($dirs) {
2089        $this->importDir = (array)$dirs;
2090    }
2091
2092    public function addImportDir($dir) {
2093        $this->importDir = (array)$this->importDir;
2094        $this->importDir[] = $dir;
2095    }
2096
2097    public function allParsedFiles() {
2098        return $this->allParsedFiles;
2099    }
2100
2101    public function addParsedFile($file) {
2102        $this->allParsedFiles[realpath($file)] = filemtime($file);
2103    }
2104
2105    /**
2106     * Uses the current value of $this->count to show line and line number
2107     */
2108    public function throwError($msg = null) {
2109        if ($this->sourceLoc >= 0) {
2110            $this->sourceParser->throwError($msg, $this->sourceLoc);
2111        }
2112        throw new exception($msg);
2113    }
2114
2115    // compile file $in to file $out if $in is newer than $out
2116    // returns true when it compiles, false otherwise
2117    public static function ccompile($in, $out, $less = null) {
2118        if ($less === null) {
2119            $less = new self;
2120        }
2121        return $less->checkedCompile($in, $out);
2122    }
2123
2124    public static function cexecute($in, $force = false, $less = null) {
2125        if ($less === null) {
2126            $less = new self;
2127        }
2128        return $less->cachedCompile($in, $force);
2129    }
2130
2131    static protected $cssColors = array(
2132        'aliceblue' => '240,248,255',
2133        'antiquewhite' => '250,235,215',
2134        'aqua' => '0,255,255',
2135        'aquamarine' => '127,255,212',
2136        'azure' => '240,255,255',
2137        'beige' => '245,245,220',
2138        'bisque' => '255,228,196',
2139        'black' => '0,0,0',
2140        'blanchedalmond' => '255,235,205',
2141        'blue' => '0,0,255',
2142        'blueviolet' => '138,43,226',
2143        'brown' => '165,42,42',
2144        'burlywood' => '222,184,135',
2145        'cadetblue' => '95,158,160',
2146        'chartreuse' => '127,255,0',
2147        'chocolate' => '210,105,30',
2148        'coral' => '255,127,80',
2149        'cornflowerblue' => '100,149,237',
2150        'cornsilk' => '255,248,220',
2151        'crimson' => '220,20,60',
2152        'cyan' => '0,255,255',
2153        'darkblue' => '0,0,139',
2154        'darkcyan' => '0,139,139',
2155        'darkgoldenrod' => '184,134,11',
2156        'darkgray' => '169,169,169',
2157        'darkgreen' => '0,100,0',
2158        'darkgrey' => '169,169,169',
2159        'darkkhaki' => '189,183,107',
2160        'darkmagenta' => '139,0,139',
2161        'darkolivegreen' => '85,107,47',
2162        'darkorange' => '255,140,0',
2163        'darkorchid' => '153,50,204',
2164        'darkred' => '139,0,0',
2165        'darksalmon' => '233,150,122',
2166        'darkseagreen' => '143,188,143',
2167        'darkslateblue' => '72,61,139',
2168        'darkslategray' => '47,79,79',
2169        'darkslategrey' => '47,79,79',
2170        'darkturquoise' => '0,206,209',
2171        'darkviolet' => '148,0,211',
2172        'deeppink' => '255,20,147',
2173        'deepskyblue' => '0,191,255',
2174        'dimgray' => '105,105,105',
2175        'dimgrey' => '105,105,105',
2176        'dodgerblue' => '30,144,255',
2177        'firebrick' => '178,34,34',
2178        'floralwhite' => '255,250,240',
2179        'forestgreen' => '34,139,34',
2180        'fuchsia' => '255,0,255',
2181        'gainsboro' => '220,220,220',
2182        'ghostwhite' => '248,248,255',
2183        'gold' => '255,215,0',
2184        'goldenrod' => '218,165,32',
2185        'gray' => '128,128,128',
2186        'green' => '0,128,0',
2187        'greenyellow' => '173,255,47',
2188        'grey' => '128,128,128',
2189        'honeydew' => '240,255,240',
2190        'hotpink' => '255,105,180',
2191        'indianred' => '205,92,92',
2192        'indigo' => '75,0,130',
2193        'ivory' => '255,255,240',
2194        'khaki' => '240,230,140',
2195        'lavender' => '230,230,250',
2196        'lavenderblush' => '255,240,245',
2197        'lawngreen' => '124,252,0',
2198        'lemonchiffon' => '255,250,205',
2199        'lightblue' => '173,216,230',
2200        'lightcoral' => '240,128,128',
2201        'lightcyan' => '224,255,255',
2202        'lightgoldenrodyellow' => '250,250,210',
2203        'lightgray' => '211,211,211',
2204        'lightgreen' => '144,238,144',
2205        'lightgrey' => '211,211,211',
2206        'lightpink' => '255,182,193',
2207        'lightsalmon' => '255,160,122',
2208        'lightseagreen' => '32,178,170',
2209        'lightskyblue' => '135,206,250',
2210        'lightslategray' => '119,136,153',
2211        'lightslategrey' => '119,136,153',
2212        'lightsteelblue' => '176,196,222',
2213        'lightyellow' => '255,255,224',
2214        'lime' => '0,255,0',
2215        'limegreen' => '50,205,50',
2216        'linen' => '250,240,230',
2217        'magenta' => '255,0,255',
2218        'maroon' => '128,0,0',
2219        'mediumaquamarine' => '102,205,170',
2220        'mediumblue' => '0,0,205',
2221        'mediumorchid' => '186,85,211',
2222        'mediumpurple' => '147,112,219',
2223        'mediumseagreen' => '60,179,113',
2224        'mediumslateblue' => '123,104,238',
2225        'mediumspringgreen' => '0,250,154',
2226        'mediumturquoise' => '72,209,204',
2227        'mediumvioletred' => '199,21,133',
2228        'midnightblue' => '25,25,112',
2229        'mintcream' => '245,255,250',
2230        'mistyrose' => '255,228,225',
2231        'moccasin' => '255,228,181',
2232        'navajowhite' => '255,222,173',
2233        'navy' => '0,0,128',
2234        'oldlace' => '253,245,230',
2235        'olive' => '128,128,0',
2236        'olivedrab' => '107,142,35',
2237        'orange' => '255,165,0',
2238        'orangered' => '255,69,0',
2239        'orchid' => '218,112,214',
2240        'palegoldenrod' => '238,232,170',
2241        'palegreen' => '152,251,152',
2242        'paleturquoise' => '175,238,238',
2243        'palevioletred' => '219,112,147',
2244        'papayawhip' => '255,239,213',
2245        'peachpuff' => '255,218,185',
2246        'peru' => '205,133,63',
2247        'pink' => '255,192,203',
2248        'plum' => '221,160,221',
2249        'powderblue' => '176,224,230',
2250        'purple' => '128,0,128',
2251        'rebeccapurple' => '102,51,153',
2252        'red' => '255,0,0',
2253        'rosybrown' => '188,143,143',
2254        'royalblue' => '65,105,225',
2255        'saddlebrown' => '139,69,19',
2256        'salmon' => '250,128,114',
2257        'sandybrown' => '244,164,96',
2258        'seagreen' => '46,139,87',
2259        'seashell' => '255,245,238',
2260        'sienna' => '160,82,45',
2261        'silver' => '192,192,192',
2262        'skyblue' => '135,206,235',
2263        'slateblue' => '106,90,205',
2264        'slategray' => '112,128,144',
2265        'slategrey' => '112,128,144',
2266        'snow' => '255,250,250',
2267        'springgreen' => '0,255,127',
2268        'steelblue' => '70,130,180',
2269        'tan' => '210,180,140',
2270        'teal' => '0,128,128',
2271        'thistle' => '216,191,216',
2272        'tomato' => '255,99,71',
2273        'transparent' => '0,0,0,0',
2274        'turquoise' => '64,224,208',
2275        'violet' => '238,130,238',
2276        'wheat' => '245,222,179',
2277        'white' => '255,255,255',
2278        'whitesmoke' => '245,245,245',
2279        'yellow' => '255,255,0',
2280        'yellowgreen' => '154,205,50'
2281    );
2282}
2283
2284// responsible for taking a string of LESS code and converting it into a
2285// syntax tree
2286#[\AllowDynamicProperties]
2287class lessc_parser {
2288    static protected $nextBlockId = 0; // used to uniquely identify blocks
2289
2290    static protected $precedence = array(
2291        '=<' => 0,
2292        '>=' => 0,
2293        '=' => 0,
2294        '<' => 0,
2295        '>' => 0,
2296
2297        '+' => 1,
2298        '-' => 1,
2299        '*' => 2,
2300        '/' => 2,
2301        '%' => 2,
2302    );
2303
2304    static protected $whitePattern;
2305    static protected $commentMulti;
2306
2307    static protected $commentSingle = "//";
2308    static protected $commentMultiLeft = "/*";
2309    static protected $commentMultiRight = "*/";
2310
2311    // regex string to match any of the operators
2312    static protected $operatorString;
2313
2314    // these properties will supress division unless it's inside parenthases
2315    static protected $supressDivisionProps =
2316        array('/border-radius$/i', '/^font$/i');
2317
2318    protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
2319    protected $lineDirectives = array("charset");
2320
2321    /**
2322     * if we are in parens we can be more liberal with whitespace around
2323     * operators because it must evaluate to a single value and thus is less
2324     * ambiguous.
2325     *
2326     * Consider:
2327     *     property1: 10 -5; // is two numbers, 10 and -5
2328     *     property2: (10 -5); // should evaluate to 5
2329     */
2330    protected $inParens = false;
2331
2332    // caches preg escaped literals
2333    static protected $literalCache = array();
2334
2335    public function __construct($lessc, $sourceName = null) {
2336        $this->eatWhiteDefault = true;
2337        // reference to less needed for vPrefix, mPrefix, and parentSelector
2338        $this->lessc = $lessc;
2339
2340        $this->sourceName = $sourceName; // name used for error messages
2341
2342        $this->writeComments = false;
2343
2344        if (!self::$operatorString) {
2345            self::$operatorString =
2346                '('.implode('|', array_map(array('lessc', 'preg_quote'),
2347                    array_keys(self::$precedence))).')';
2348
2349            $commentSingle = lessc::preg_quote(self::$commentSingle);
2350            $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
2351            $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
2352
2353            self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2354            self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2355        }
2356    }
2357
2358    public function parse($buffer) {
2359        $this->count = 0;
2360        $this->line = 1;
2361
2362        $this->env = null; // block stack
2363        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
2364        $this->pushSpecialBlock("root");
2365        $this->eatWhiteDefault = true;
2366        $this->seenComments = array();
2367
2368        // trim whitespace on head
2369        // if (preg_match('/^\s+/', $this->buffer, $m)) {
2370        //     $this->line += substr_count($m[0], "\n");
2371        //     $this->buffer = ltrim($this->buffer);
2372        // }
2373        $this->whitespace();
2374
2375        // parse the entire file
2376        while (false !== $this->parseChunk());
2377
2378        if ($this->count != strlen($this->buffer))
2379            $this->throwError();
2380
2381        // TODO report where the block was opened
2382        if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
2383            throw new exception('parse error: unclosed block');
2384
2385        return $this->env;
2386    }
2387
2388    /**
2389     * Parse a single chunk off the head of the buffer and append it to the
2390     * current parse environment.
2391     * Returns false when the buffer is empty, or when there is an error.
2392     *
2393     * This function is called repeatedly until the entire document is
2394     * parsed.
2395     *
2396     * This parser is most similar to a recursive descent parser. Single
2397     * functions represent discrete grammatical rules for the language, and
2398     * they are able to capture the text that represents those rules.
2399     *
2400     * Consider the function lessc::keyword(). (all parse functions are
2401     * structured the same)
2402     *
2403     * The function takes a single reference argument. When calling the
2404     * function it will attempt to match a keyword on the head of the buffer.
2405     * If it is successful, it will place the keyword in the referenced
2406     * argument, advance the position in the buffer, and return true. If it
2407     * fails then it won't advance the buffer and it will return false.
2408     *
2409     * All of these parse functions are powered by lessc::match(), which behaves
2410     * the same way, but takes a literal regular expression. Sometimes it is
2411     * more convenient to use match instead of creating a new function.
2412     *
2413     * Because of the format of the functions, to parse an entire string of
2414     * grammatical rules, you can chain them together using &&.
2415     *
2416     * But, if some of the rules in the chain succeed before one fails, then
2417     * the buffer position will be left at an invalid state. In order to
2418     * avoid this, lessc::seek() is used to remember and set buffer positions.
2419     *
2420     * Before parsing a chain, use $s = $this->seek() to remember the current
2421     * position into $s. Then if a chain fails, use $this->seek($s) to
2422     * go back where we started.
2423     */
2424    protected function parseChunk() {
2425        if (empty($this->buffer)) return false;
2426        $s = $this->seek();
2427
2428        if ($this->whitespace()) {
2429            return true;
2430        }
2431
2432        // setting a property
2433        if ($this->keyword($key) && $this->assign() &&
2434            $this->propertyValue($value, $key) && $this->end())
2435        {
2436            $this->append(array('assign', $key, $value), $s);
2437            return true;
2438        } else {
2439            $this->seek($s);
2440        }
2441
2442
2443        // look for special css blocks
2444        if ($this->literal('@', false)) {
2445            $this->count--;
2446
2447            // media
2448            if ($this->literal('@media')) {
2449                if (($this->mediaQueryList($mediaQueries) || true)
2450                    && $this->literal('{'))
2451                {
2452                    $media = $this->pushSpecialBlock("media");
2453                    $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
2454                    return true;
2455                } else {
2456                    $this->seek($s);
2457                    return false;
2458                }
2459            }
2460
2461            if ($this->literal("@", false) && $this->keyword($dirName)) {
2462                if ($this->isDirective($dirName, $this->blockDirectives)) {
2463                    if (($this->openString("{", $dirValue, null, array(";")) || true) &&
2464                        $this->literal("{"))
2465                    {
2466                        $dir = $this->pushSpecialBlock("directive");
2467                        $dir->name = $dirName;
2468                        if (isset($dirValue)) $dir->value = $dirValue;
2469                        return true;
2470                    }
2471                } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
2472                    if ($this->propertyValue($dirValue) && $this->end()) {
2473                        $this->append(array("directive", $dirName, $dirValue));
2474                        return true;
2475                    }
2476                }
2477            }
2478
2479            $this->seek($s);
2480        }
2481
2482        // setting a variable
2483        if ($this->variable($var) && $this->assign() &&
2484            $this->propertyValue($value) && $this->end())
2485        {
2486            $this->append(array('assign', $var, $value), $s);
2487            return true;
2488        } else {
2489            $this->seek($s);
2490        }
2491
2492        if ($this->import($importValue)) {
2493            $this->append($importValue, $s);
2494            return true;
2495        }
2496
2497        // opening parametric mixin
2498        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
2499            ($this->guards($guards) || true) &&
2500            $this->literal('{'))
2501        {
2502            $block = $this->pushBlock($this->fixTags(array($tag)));
2503            $block->args = $args;
2504            $block->isVararg = $isVararg;
2505            if (!empty($guards)) $block->guards = $guards;
2506            return true;
2507        } else {
2508            $this->seek($s);
2509        }
2510
2511        // opening a simple block
2512        if ($this->tags($tags) && $this->literal('{', false)) {
2513            $tags = $this->fixTags($tags);
2514            $this->pushBlock($tags);
2515            return true;
2516        } else {
2517            $this->seek($s);
2518        }
2519
2520        // closing a block
2521        if ($this->literal('}', false)) {
2522            try {
2523                $block = $this->pop();
2524            } catch (exception $e) {
2525                $this->seek($s);
2526                $this->throwError($e->getMessage());
2527                $block = null; // Rector.
2528            }
2529
2530            if ($this->env === null) {
2531                $this->seek($s);
2532                $this->throwError("unexpected '}'");
2533            }
2534
2535            $hidden = false;
2536            if (is_null($block->type)) {
2537                $hidden = true;
2538                if (!isset($block->args)) {
2539                    foreach ($block->tags as $tag) {
2540                        if (!is_string($tag) || $tag[0] != $this->lessc->mPrefix) {
2541                            $hidden = false;
2542                            break;
2543                        }
2544                    }
2545                }
2546
2547                foreach ($block->tags as $tag) {
2548                    if (is_string($tag)) {
2549                        $this->env->children[$tag][] = $block;
2550                    }
2551                }
2552            }
2553
2554            if (!$hidden) {
2555                $this->append(array('block', $block), $s);
2556            }
2557
2558            // this is done here so comments aren't bundled into he block that
2559            // was just closed
2560            $this->whitespace();
2561            return true;
2562        }
2563
2564        // mixin
2565        if ($this->mixinTags($tags) &&
2566            ($this->argumentDef($argv, $isVararg) || true) &&
2567            ($this->keyword($suffix) || true) && $this->end())
2568        {
2569            $tags = $this->fixTags($tags);
2570            $this->append(array('mixin', $tags, $argv, $suffix), $s);
2571            return true;
2572        } else {
2573            $this->seek($s);
2574        }
2575
2576        // spare ;
2577        if ($this->literal(';')) return true;
2578
2579        return false; // got nothing, throw error
2580    }
2581
2582    protected function isDirective($dirname, $directives) {
2583        // TODO: cache pattern in parser
2584        $pattern = implode("|",
2585            array_map(array("lessc", "preg_quote"), $directives));
2586        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
2587
2588        return preg_match($pattern, $dirname);
2589    }
2590
2591    protected function fixTags($tags) {
2592        // move @ tags out of variable namespace
2593        foreach ($tags as &$tag) {
2594            if ($tag[0] == $this->lessc->vPrefix)
2595                $tag[0] = $this->lessc->mPrefix;
2596        }
2597        return $tags;
2598    }
2599
2600    // a list of expressions
2601    protected function expressionList(&$exps) {
2602        $values = array();
2603
2604        while ($this->expression($exp)) {
2605            $values[] = $exp;
2606        }
2607
2608        if (count($values) == 0) return false;
2609
2610        $exps = lessc::compressList($values, ' ');
2611        return true;
2612    }
2613
2614    /**
2615     * Attempt to consume an expression.
2616     * @link https://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
2617     */
2618    protected function expression(&$out) {
2619        if ($this->value($lhs)) {
2620            $out = $this->expHelper($lhs, 0);
2621
2622            // look for / shorthand
2623            if (!empty($this->env->supressedDivision)) {
2624                unset($this->env->supressedDivision);
2625                $s = $this->seek();
2626                if ($this->literal("/") && $this->value($rhs)) {
2627                    $out = array("list", "",
2628                        array($out, array("keyword", "/"), $rhs));
2629                } else {
2630                    $this->seek($s);
2631                }
2632            }
2633
2634            return true;
2635        }
2636        return false;
2637    }
2638
2639    /**
2640     * recursively parse infix equation with $lhs at precedence $minP
2641     */
2642    protected function expHelper($lhs, $minP) {
2643        $this->inExp = true;
2644        $ss = $this->seek();
2645
2646        while (true) {
2647            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2648                ctype_space($this->buffer[$this->count - 1]);
2649
2650            // If there is whitespace before the operator, then we require
2651            // whitespace after the operator for it to be an expression
2652            $needWhite = $whiteBefore && !$this->inParens;
2653
2654            if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
2655                if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
2656                    foreach (self::$supressDivisionProps as $pattern) {
2657                        if (preg_match($pattern, $this->env->currentProperty)) {
2658                            $this->env->supressedDivision = true;
2659                            break 2;
2660                        }
2661                    }
2662                }
2663
2664
2665                $whiteAfter = isset($this->buffer[$this->count - 1]) &&
2666                    ctype_space($this->buffer[$this->count - 1]);
2667
2668                if (!$this->value($rhs)) break;
2669
2670                // peek for next operator to see what to do with rhs
2671                if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
2672                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
2673                }
2674
2675                $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
2676                $ss = $this->seek();
2677
2678                continue;
2679            }
2680
2681            break;
2682        }
2683
2684        $this->seek($ss);
2685
2686        return $lhs;
2687    }
2688
2689    // consume a list of values for a property
2690    public function propertyValue(&$value, $keyName = null) {
2691        $values = array();
2692
2693        if ($keyName !== null) $this->env->currentProperty = $keyName;
2694
2695        $s = null;
2696        while ($this->expressionList($v)) {
2697            $values[] = $v;
2698            $s = $this->seek();
2699            if (!$this->literal(',')) break;
2700        }
2701
2702        if ($s) $this->seek($s);
2703
2704        if ($keyName !== null) unset($this->env->currentProperty);
2705
2706        if (count($values) == 0) return false;
2707
2708        $value = lessc::compressList($values, ', ');
2709        return true;
2710    }
2711
2712    protected function parenValue(&$out) {
2713        $s = $this->seek();
2714
2715        // speed shortcut
2716        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
2717            return false;
2718        }
2719
2720        $inParens = $this->inParens;
2721        if ($this->literal("(") &&
2722            ($this->inParens = true) && $this->expression($exp) &&
2723            $this->literal(")"))
2724        {
2725            $out = $exp;
2726            $this->inParens = $inParens;
2727            return true;
2728        } else {
2729            $this->inParens = $inParens;
2730            $this->seek($s);
2731        }
2732
2733        return false;
2734    }
2735
2736    // a single value
2737    protected function value(&$value) {
2738        $s = $this->seek();
2739
2740        // speed shortcut
2741        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
2742            // negation
2743            if ($this->literal("-", false) &&
2744                (($this->variable($inner) && $inner = array("variable", $inner)) ||
2745                $this->unit($inner) ||
2746                $this->parenValue($inner)))
2747            {
2748                $value = array("unary", "-", $inner);
2749                return true;
2750            } else {
2751                $this->seek($s);
2752            }
2753        }
2754
2755        if ($this->parenValue($value)) return true;
2756        if ($this->unit($value)) return true;
2757        if ($this->color($value)) return true;
2758        if ($this->func($value)) return true;
2759        if ($this->string($value)) return true;
2760
2761        if ($this->keyword($word)) {
2762            $value = array('keyword', $word);
2763            return true;
2764        }
2765
2766        // try a variable
2767        if ($this->variable($var)) {
2768            $value = array('variable', $var);
2769            return true;
2770        }
2771
2772        // unquote string (should this work on any type?
2773        if ($this->literal("~") && $this->string($str)) {
2774            $value = array("escape", $str);
2775            return true;
2776        } else {
2777            $this->seek($s);
2778        }
2779
2780        // css hack: \0
2781        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
2782            $value = array('keyword', '\\'.$m[1]);
2783            return true;
2784        } else {
2785            $this->seek($s);
2786        }
2787
2788        return false;
2789    }
2790
2791    // an import statement
2792    protected function import(&$out) {
2793        if (!$this->literal('@import')) return false;
2794
2795        // @import "something.css" media;
2796        // @import url("something.css") media;
2797        // @import url(something.css) media;
2798
2799        if ($this->propertyValue($value)) {
2800            $out = array("import", $value);
2801            return true;
2802        }
2803    }
2804
2805    protected function mediaQueryList(&$out) {
2806        if ($this->genericList($list, "mediaQuery", ",", false)) {
2807            $out = $list[2];
2808            return true;
2809        }
2810        return false;
2811    }
2812
2813    protected function mediaQuery(&$out) {
2814        $s = $this->seek();
2815
2816        $expressions = null;
2817        $parts = array();
2818
2819        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
2820            $prop = array("mediaType");
2821            if (isset($only)) $prop[] = "only";
2822            if (isset($not)) $prop[] = "not";
2823            $prop[] = $mediaType;
2824            $parts[] = $prop;
2825        } else {
2826            $this->seek($s);
2827        }
2828
2829
2830        if (!empty($mediaType) && !$this->literal("and")) {
2831            // ~
2832        } else {
2833            $this->genericList($expressions, "mediaExpression", "and", false);
2834            if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
2835        }
2836
2837        if (count($parts) == 0) {
2838            $this->seek($s);
2839            return false;
2840        }
2841
2842        $out = $parts;
2843        return true;
2844    }
2845
2846    protected function mediaExpression(&$out) {
2847        $s = $this->seek();
2848        $value = null;
2849        if ($this->literal("(") &&
2850            $this->keyword($feature) &&
2851            ($this->literal(":") && $this->expression($value) || true) &&
2852            $this->literal(")"))
2853        {
2854            $out = array("mediaExp", $feature);
2855            if ($value) $out[] = $value;
2856            return true;
2857        } elseif ($this->variable($variable)) {
2858            $out = array('variable', $variable);
2859            return true;
2860        }
2861
2862        $this->seek($s);
2863        return false;
2864    }
2865
2866    // an unbounded string stopped by $end
2867    protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
2868        $oldWhite = $this->eatWhiteDefault;
2869        $this->eatWhiteDefault = false;
2870
2871        $stop = array("'", '"', "@{", $end);
2872        $stop = array_map(array("lessc", "preg_quote"), $stop);
2873        // $stop[] = self::$commentMulti;
2874
2875        if (!is_null($rejectStrs)) {
2876            $stop = array_merge($stop, $rejectStrs);
2877        }
2878
2879        $patt = '(.*?)('.implode("|", $stop).')';
2880
2881        $nestingLevel = 0;
2882
2883        $content = array();
2884        while ($this->match($patt, $m, false)) {
2885            if (!empty($m[1])) {
2886                $content[] = $m[1];
2887                if ($nestingOpen) {
2888                    $nestingLevel += substr_count($m[1], $nestingOpen);
2889                }
2890            }
2891
2892            $tok = $m[2];
2893
2894            $this->count-= strlen($tok);
2895            if ($tok == $end) {
2896                if ($nestingLevel == 0) {
2897                    break;
2898                } else {
2899                    $nestingLevel--;
2900                }
2901            }
2902
2903            if (($tok == "'" || $tok == '"') && $this->string($str)) {
2904                $content[] = $str;
2905                continue;
2906            }
2907
2908            if ($tok == "@{" && $this->interpolation($inter)) {
2909                $content[] = $inter;
2910                continue;
2911            }
2912
2913            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
2914                break;
2915            }
2916
2917            $content[] = $tok;
2918            $this->count+= strlen($tok);
2919        }
2920
2921        $this->eatWhiteDefault = $oldWhite;
2922
2923        if (count($content) == 0) return false;
2924
2925        // trim the end
2926        if (is_string(end($content))) {
2927            $content[count($content) - 1] = rtrim(end($content));
2928        }
2929
2930        $out = array("string", "", $content);
2931        return true;
2932    }
2933
2934    protected function string(&$out) {
2935        $s = $this->seek();
2936        if ($this->literal('"', false)) {
2937            $delim = '"';
2938        } elseif ($this->literal("'", false)) {
2939            $delim = "'";
2940        } else {
2941            return false;
2942        }
2943
2944        $content = array();
2945
2946        // look for either ending delim , escape, or string interpolation
2947        $patt = '([^\n]*?)(@\{|\\\\|' .
2948            lessc::preg_quote($delim).')';
2949
2950        $oldWhite = $this->eatWhiteDefault;
2951        $this->eatWhiteDefault = false;
2952
2953        while ($this->match($patt, $m, false)) {
2954            $content[] = $m[1];
2955            if ($m[2] == "@{") {
2956                $this->count -= strlen($m[2]);
2957                if ($this->interpolation($inter, false)) {
2958                    $content[] = $inter;
2959                } else {
2960                    $this->count += strlen($m[2]);
2961                    $content[] = "@{"; // ignore it
2962                }
2963            } elseif ($m[2] == '\\') {
2964                $content[] = $m[2];
2965                if ($this->literal($delim, false)) {
2966                    $content[] = $delim;
2967                }
2968            } else {
2969                $this->count -= strlen($delim);
2970                break; // delim
2971            }
2972        }
2973
2974        $this->eatWhiteDefault = $oldWhite;
2975
2976        if ($this->literal($delim)) {
2977            $out = array("string", $delim, $content);
2978            return true;
2979        }
2980
2981        $this->seek($s);
2982        return false;
2983    }
2984
2985    protected function interpolation(&$out) {
2986        $oldWhite = $this->eatWhiteDefault;
2987        $this->eatWhiteDefault = true;
2988
2989        $s = $this->seek();
2990        if ($this->literal("@{") &&
2991            $this->openString("}", $interp, null, array("'", '"', ";")) &&
2992            $this->literal("}", false))
2993        {
2994            $out = array("interpolate", $interp);
2995            $this->eatWhiteDefault = $oldWhite;
2996            if ($this->eatWhiteDefault) $this->whitespace();
2997            return true;
2998        }
2999
3000        $this->eatWhiteDefault = $oldWhite;
3001        $this->seek($s);
3002        return false;
3003    }
3004
3005    protected function unit(&$unit) {
3006        // speed shortcut
3007        if (isset($this->buffer[$this->count])) {
3008            $char = $this->buffer[$this->count];
3009            if (!ctype_digit($char) && $char != ".") return false;
3010        }
3011
3012        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
3013            $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
3014            return true;
3015        }
3016        return false;
3017    }
3018
3019    // a # color
3020    protected function color(&$out) {
3021        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
3022            if (strlen($m[1]) > 7) {
3023                $out = array("string", "", array($m[1]));
3024            } else {
3025                $out = array("raw_color", $m[1]);
3026            }
3027            return true;
3028        }
3029
3030        return false;
3031    }
3032
3033    // consume an argument definition list surrounded by ()
3034    // each argument is a variable name with optional value
3035    // or at the end a ... or a variable named followed by ...
3036    // arguments are separated by , unless a ; is in the list, then ; is the
3037    // delimiter.
3038    protected function argumentDef(&$args, &$isVararg) {
3039        $value  = null; // Initialize output variable to make wpcom's rector happy.
3040        $rhs    = null; // Initialize output variable to make wpcom's rector happy.
3041        $newArg = null; // Initialize variable for rector.
3042        $s = $this->seek();
3043        if (!$this->literal('(')) return false;
3044
3045        $values = array();
3046        $delim = ",";
3047        $method = "expressionList";
3048
3049        $isVararg = false;
3050        while (true) {
3051            if ($this->literal("...")) {
3052                $isVararg = true;
3053                break;
3054            }
3055
3056            if ($this->$method($value)) {
3057                if ($value[0] == "variable") {
3058                    $arg = array("arg", $value[1]);
3059                    $ss = $this->seek();
3060
3061                    if ($this->assign() && $this->$method($rhs)) {
3062                        $arg[] = $rhs;
3063                    } else {
3064                        $this->seek($ss);
3065                        if ($this->literal("...")) {
3066                            $arg[0] = "rest";
3067                            $isVararg = true;
3068                        }
3069                    }
3070
3071                    $values[] = $arg;
3072                    if ($isVararg) break;
3073                    continue;
3074                } else {
3075                    $values[] = array("lit", $value);
3076                }
3077            }
3078
3079
3080            if (!$this->literal($delim)) {
3081                if ($delim == "," && $this->literal(";")) {
3082                    // found new delim, convert existing args
3083                    $delim = ";";
3084                    $method = "propertyValue";
3085
3086                    // transform arg list
3087                    if (isset($values[1])) { // 2 items
3088                        $newList = array();
3089                        foreach ($values as $i => $arg) {
3090                            switch($arg[0]) {
3091                            case "arg":
3092                                if ($i) {
3093                                    $this->throwError("Cannot mix ; and , as delimiter types");
3094                                }
3095                                $newList[] = $arg[2];
3096                                break;
3097                            case "lit":
3098                                $newList[] = $arg[1];
3099                                break;
3100                            case "rest":
3101                                $this->throwError("Unexpected rest before semicolon");
3102                            }
3103                        }
3104
3105                        $newList = array("list", ", ", $newList);
3106
3107                        switch ($values[0][0]) {
3108                        case "arg":
3109                            $newArg = array("arg", $values[0][1], $newList);
3110                            break;
3111                        case "lit":
3112                            $newArg = array("lit", $newList);
3113                            break;
3114                        }
3115
3116                    } elseif ($values) { // 1 item
3117                        $newArg = $values[0];
3118                    }
3119
3120                    if ($newArg) {
3121                        $values = array($newArg);
3122                    }
3123                } else {
3124                    break;
3125                }
3126            }
3127        }
3128
3129        if (!$this->literal(')')) {
3130            $this->seek($s);
3131            return false;
3132        }
3133
3134        $args = $values;
3135
3136        return true;
3137    }
3138
3139    // consume a list of tags
3140    // this accepts a hanging delimiter
3141    protected function tags(&$tags, $simple = false, $delim = ',') {
3142        $tags = array();
3143        while ($this->tag($tt, $simple)) {
3144            $tags[] = $tt;
3145            if (!$this->literal($delim)) break;
3146        }
3147        if (count($tags) == 0) return false;
3148
3149        return true;
3150    }
3151
3152    // list of tags of specifying mixin path
3153    // optionally separated by > (lazy, accepts extra >)
3154    protected function mixinTags(&$tags) {
3155        $tags = array();
3156        while ($this->tag($tt, true)) {
3157            $tags[] = $tt;
3158            $this->literal(">");
3159        }
3160
3161        if (count($tags) == 0) return false;
3162
3163        return true;
3164    }
3165
3166    // a bracketed value (contained within in a tag definition)
3167    protected function tagBracket(&$parts, &$hasExpression) {
3168        // speed shortcut
3169        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
3170            return false;
3171        }
3172
3173        $s = $this->seek();
3174
3175        $hasInterpolation = false;
3176
3177        if ($this->literal("[", false)) {
3178            $attrParts = array("[");
3179            // keyword, string, operator
3180            while (true) {
3181                if ($this->literal("]", false)) {
3182                    $this->count--;
3183                    break; // get out early
3184                }
3185
3186                if ($this->match('\s+', $m)) {
3187                    $attrParts[] = " ";
3188                    continue;
3189                }
3190                if ($this->string($str)) {
3191                    // escape parent selector, (yuck)
3192                    foreach ($str[2] as &$chunk) {
3193                        $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
3194                    }
3195
3196                    $attrParts[] = $str;
3197                    $hasInterpolation = true;
3198                    continue;
3199                }
3200
3201                if ($this->keyword($word)) {
3202                    $attrParts[] = $word;
3203                    continue;
3204                }
3205
3206                if ($this->interpolation($inter, false)) {
3207                    $attrParts[] = $inter;
3208                    $hasInterpolation = true;
3209                    continue;
3210                }
3211
3212                // operator, handles attr namespace too
3213                if ($this->match('[|-~\$\*\^=]+', $m)) {
3214                    $attrParts[] = $m[0];
3215                    continue;
3216                }
3217
3218                break;
3219            }
3220
3221            if ($this->literal("]", false)) {
3222                $attrParts[] = "]";
3223                foreach ($attrParts as $part) {
3224                    $parts[] = $part;
3225                }
3226                $hasExpression = $hasExpression || $hasInterpolation;
3227                return true;
3228            }
3229            $this->seek($s);
3230        }
3231
3232        $this->seek($s);
3233        return false;
3234    }
3235
3236    // a space separated list of selectors
3237    protected function tag(&$tag, $simple = false) {
3238        if ($simple)
3239            $chars = '^@,:;{}\][>\(\) "\'';
3240        else
3241            $chars = '^@,;{}["\'';
3242
3243        $s = $this->seek();
3244
3245        $hasExpression = false;
3246        $parts = array();
3247        while ($this->tagBracket($parts, $hasExpression));
3248
3249        $oldWhite = $this->eatWhiteDefault;
3250        $this->eatWhiteDefault = false;
3251
3252        while (true) {
3253            if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
3254                $parts[] = $m[1];
3255                if ($simple) break;
3256
3257                while ($this->tagBracket($parts, $hasExpression));
3258                continue;
3259            }
3260
3261            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
3262                if ($this->interpolation($interp)) {
3263                    $hasExpression = true;
3264                    $interp[2] = true; // don't unescape
3265                    $parts[] = $interp;
3266                    continue;
3267                }
3268
3269                if ($this->literal("@")) {
3270                    $parts[] = "@";
3271                    continue;
3272                }
3273            }
3274
3275            if ($this->unit($unit)) { // for keyframes
3276                $parts[] = $unit[1];
3277                $parts[] = $unit[2];
3278                continue;
3279            }
3280
3281            break;
3282        }
3283
3284        $this->eatWhiteDefault = $oldWhite;
3285        if (!$parts) {
3286            $this->seek($s);
3287            return false;
3288        }
3289
3290        if ($hasExpression) {
3291            $tag = array("exp", array("string", "", $parts));
3292        } else {
3293            $tag = trim(implode($parts));
3294        }
3295
3296        $this->whitespace();
3297        return true;
3298    }
3299
3300    // a css function
3301    protected function func(&$func) {
3302        $s = $this->seek();
3303
3304        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
3305            $fname = $m[1];
3306
3307            $sPreArgs = $this->seek();
3308
3309            $args = array();
3310            while (true) {
3311                $ss = $this->seek();
3312                // this ugly nonsense is for ie filter properties
3313                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
3314                    $args[] = array("string", "", array($name, "=", $value));
3315                } else {
3316                    $this->seek($ss);
3317                    if ($this->expressionList($value)) {
3318                        $args[] = $value;
3319                    }
3320                }
3321
3322                if (!$this->literal(',')) break;
3323            }
3324            $args = array('list', ',', $args);
3325
3326            if ($this->literal(')')) {
3327                $func = array('function', $fname, $args);
3328                return true;
3329            } elseif ($fname == 'url') {
3330                // couldn't parse and in url? treat as string
3331                $this->seek($sPreArgs);
3332                if ($this->openString(")", $string) && $this->literal(")")) {
3333                    $func = array('function', $fname, $string);
3334                    return true;
3335                }
3336            }
3337        }
3338
3339        $this->seek($s);
3340        return false;
3341    }
3342
3343    // consume a less variable
3344    protected function variable(&$name) {
3345        $s = $this->seek();
3346        if ($this->literal($this->lessc->vPrefix, false) &&
3347            ($this->variable($sub) || $this->keyword($name)))
3348        {
3349            if (!empty($sub)) {
3350                $name = array('variable', $sub);
3351            } else {
3352                $name = $this->lessc->vPrefix.$name;
3353            }
3354            return true;
3355        }
3356
3357        $name = null;
3358        $this->seek($s);
3359        return false;
3360    }
3361
3362    /**
3363     * Consume an assignment operator
3364     * Can optionally take a name that will be set to the current property name
3365     */
3366    protected function assign($name = null) {
3367        if ($name) $this->currentProperty = $name;
3368        return $this->literal(':') || $this->literal('=');
3369    }
3370
3371    // consume a keyword
3372    protected function keyword(&$word) {
3373        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
3374            $word = $m[1];
3375            return true;
3376        }
3377        return false;
3378    }
3379
3380    // consume an end of statement delimiter
3381    protected function end() {
3382        if ($this->literal(';', false)) {
3383            return true;
3384        } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3385            // if there is end of file or a closing block next then we don't need a ;
3386            return true;
3387        }
3388        return false;
3389    }
3390
3391    protected function guards(&$guards) {
3392        $s = $this->seek();
3393
3394        if (!$this->literal("when")) {
3395            $this->seek($s);
3396            return false;
3397        }
3398
3399        $guards = array();
3400
3401        while ($this->guardGroup($g)) {
3402            $guards[] = $g;
3403            if (!$this->literal(",")) break;
3404        }
3405
3406        if (count($guards) == 0) {
3407            $guards = null;
3408            $this->seek($s);
3409            return false;
3410        }
3411
3412        return true;
3413    }
3414
3415    // a bunch of guards that are and'd together
3416    // TODO rename to guardGroup
3417    protected function guardGroup(&$guardGroup) {
3418        $s = $this->seek();
3419        $guardGroup = array();
3420        while ($this->guard($guard)) {
3421            $guardGroup[] = $guard;
3422            if (!$this->literal("and")) break;
3423        }
3424
3425        if (count($guardGroup) == 0) {
3426            $guardGroup = null;
3427            $this->seek($s);
3428            return false;
3429        }
3430
3431        return true;
3432    }
3433
3434    protected function guard(&$guard) {
3435        $s = $this->seek();
3436        $negate = $this->literal("not");
3437
3438        if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
3439            $guard = $exp;
3440            if ($negate) $guard = array("negate", $guard);
3441            return true;
3442        }
3443
3444        $this->seek($s);
3445        return false;
3446    }
3447
3448    /* raw parsing functions */
3449
3450    protected function literal($what, $eatWhitespace = null) {
3451        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3452
3453        // shortcut on single letter
3454        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3455            if ($this->buffer[$this->count] == $what) {
3456                if (!$eatWhitespace) {
3457                    $this->count++;
3458                    return true;
3459                }
3460                // goes below...
3461            } else {
3462                return false;
3463            }
3464        }
3465
3466        if (!isset(self::$literalCache[$what])) {
3467            self::$literalCache[$what] = lessc::preg_quote($what);
3468        }
3469
3470        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
3471    }
3472
3473    protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3474        $value = null; // Initialize output parameter to make wpcom's rector happy.
3475        $s = $this->seek();
3476        $items = array();
3477        while ($this->$parseItem($value)) {
3478            $items[] = $value;
3479            if ($delim) {
3480                if (!$this->literal($delim)) break;
3481            }
3482        }
3483
3484        if (count($items) == 0) {
3485            $this->seek($s);
3486            return false;
3487        }
3488
3489        if ($flatten && count($items) == 1) {
3490            $out = $items[0];
3491        } else {
3492            $out = array("list", $delim, $items);
3493        }
3494
3495        return true;
3496    }
3497
3498
3499    // advance counter to next occurrence of $what
3500    // $until - don't include $what in advance
3501    // $allowNewline, if string, will be used as valid char set
3502    protected function to($what, &$out, $until = false, $allowNewline = false) {
3503        if (is_string($allowNewline)) {
3504            $validChars = $allowNewline;
3505        } else {
3506            $validChars = $allowNewline ? "." : "[^\n]";
3507        }
3508        if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
3509        if ($until) $this->count -= strlen($what); // give back $what
3510        $out = $m[1];
3511        return true;
3512    }
3513
3514    // try to match something on head of buffer
3515    protected function match($regex, &$out, $eatWhitespace = null) {
3516        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3517
3518        $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
3519        if (preg_match($r, $this->buffer, $out, 0, $this->count)) {
3520            $this->count += strlen($out[0]);
3521            if ($eatWhitespace && $this->writeComments) $this->whitespace();
3522            return true;
3523        }
3524        return false;
3525    }
3526
3527    // match some whitespace
3528    protected function whitespace() {
3529        if ($this->writeComments) {
3530            $gotWhite = false;
3531            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3532                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
3533                    $this->append(array("comment", $m[1]));
3534                    $this->seenComments[$this->count] = true;
3535                }
3536                $this->count += strlen($m[0]);
3537                $gotWhite = true;
3538            }
3539            return $gotWhite;
3540        } else {
3541            $this->match("", $m);
3542            return strlen($m[0]) > 0;
3543        }
3544    }
3545
3546    // match something without consuming it
3547    protected function peek($regex, &$out = null, $from=null) {
3548        if (is_null($from)) $from = $this->count;
3549        $r = '/'.$regex.'/Ais';
3550        $result = preg_match($r, $this->buffer, $out, 0, $from);
3551
3552        return $result;
3553    }
3554
3555    // seek to a spot in the buffer or return where we are on no argument
3556    protected function seek($where = null) {
3557        if ($where === null) return $this->count;
3558        else $this->count = $where;
3559        return true;
3560    }
3561
3562    /* misc functions */
3563
3564    public function throwError($msg = "parse error", $count = null) {
3565        $count = is_null($count) ? $this->count : $count;
3566
3567        $line = $this->line +
3568            substr_count(substr($this->buffer, 0, $count), "\n");
3569
3570        if (!empty($this->sourceName)) {
3571            $loc = "$this->sourceName on line $line";
3572        } else {
3573            $loc = "line: $line";
3574        }
3575
3576        // TODO this depends on $this->count
3577        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3578            throw new exception("$msg: failed at `$m[1]$loc");
3579        } else {
3580            throw new exception("$msg$loc");
3581        }
3582    }
3583
3584    protected function pushBlock($selectors=null, $type=null) {
3585        $b = new stdclass;
3586        $b->parent = $this->env;
3587
3588        $b->type = $type;
3589        $b->id = self::$nextBlockId++;
3590
3591        $b->isVararg = false; // TODO: kill me from here
3592        $b->tags = $selectors;
3593
3594        $b->props = array();
3595        $b->children = array();
3596
3597        $this->env = $b;
3598        return $b;
3599    }
3600
3601    // push a block that doesn't multiply tags
3602    protected function pushSpecialBlock($type) {
3603        return $this->pushBlock(null, $type);
3604    }
3605
3606    // append a property to the current block
3607    protected function append($prop, $pos = null) {
3608        if ($pos !== null) $prop[-1] = $pos;
3609        $this->env->props[] = $prop;
3610    }
3611
3612    // pop something off the stack
3613    protected function pop() {
3614        $old = $this->env;
3615        $this->env = $this->env->parent;
3616        return $old;
3617    }
3618
3619    // remove comments from $text
3620    // todo: make it work for all functions, not just url
3621    protected function removeComments($text) {
3622        $look = array(
3623            'url(', '//', '/*', '"', "'"
3624        );
3625
3626        $out = '';
3627        $min = null;
3628        while (true) {
3629            // find the next item
3630            foreach ($look as $token) {
3631                $pos = strpos($text, $token);
3632                if ($pos !== false) {
3633                    if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
3634                }
3635            }
3636
3637            if (is_null($min)) break;
3638
3639            $count = $min[1];
3640            $skip = 0;
3641            $newlines = 0;
3642            switch ($min[0]) {
3643            case 'url(':
3644                if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
3645                    $count += strlen($m[0]) - strlen($min[0]);
3646                break;
3647            case '"':
3648            case "'":
3649                if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
3650                    $count += strlen($m[0]) - 1;
3651                break;
3652            case '//':
3653                $skip = strpos($text, "\n", $count);
3654                if ($skip === false) $skip = strlen($text) - $count;
3655                else $skip -= $count;
3656                break;
3657            case '/*':
3658                if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
3659                    $skip = strlen($m[0]);
3660                    $newlines = substr_count($m[0], "\n");
3661                }
3662                break;
3663            }
3664
3665            if ($skip == 0) $count += strlen($min[0]);
3666
3667            $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
3668            $text = substr($text, $count + $skip);
3669
3670            $min = null;
3671        }
3672
3673        return $out.$text;
3674    }
3675
3676}
3677
3678class lessc_formatter_classic {
3679    public $indentChar = "  ";
3680
3681    public $break = "\n";
3682    public $open = " {";
3683    public $close = "}";
3684    public $selectorSeparator = ", ";
3685    public $assignSeparator = ":";
3686
3687    public $openSingle = " { ";
3688    public $closeSingle = " }";
3689
3690    public $disableSingle = false;
3691    public $breakSelectors = false;
3692
3693    public $compressColors = false;
3694
3695    public function __construct() {
3696        $this->indentLevel = 0;
3697    }
3698
3699    public function indentStr($n = 0) {
3700        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
3701    }
3702
3703    public function property($name, $value) {
3704        return $name . $this->assignSeparator . $value . ";";
3705    }
3706
3707    protected function isEmpty($block) {
3708        if (empty($block->lines)) {
3709            foreach ($block->children as $child) {
3710                if (!$this->isEmpty($child)) return false;
3711            }
3712
3713            return true;
3714        }
3715        return false;
3716    }
3717
3718    public function block($block) {
3719        if ($this->isEmpty($block)) return;
3720
3721        $inner = $pre = $this->indentStr();
3722
3723        $isSingle = !$this->disableSingle &&
3724            is_null( $block->type ) && is_countable( $block->lines ) && count( $block->lines ) == 1;
3725
3726        if (!empty($block->selectors)) {
3727            $this->indentLevel++;
3728
3729            if ($this->breakSelectors) {
3730                $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
3731            } else {
3732                $selectorSeparator = $this->selectorSeparator;
3733            }
3734
3735            echo $pre .
3736                implode($selectorSeparator, $block->selectors);
3737            if ($isSingle) {
3738                echo $this->openSingle;
3739                $inner = "";
3740            } else {
3741                echo $this->open . $this->break;
3742                $inner = $this->indentStr();
3743            }
3744
3745        }
3746
3747        if (!empty($block->lines)) {
3748            $glue = $this->break.$inner;
3749            echo $inner . implode($glue, $block->lines);
3750            if (!$isSingle && !empty($block->children)) {
3751                echo $this->break;
3752            }
3753        }
3754
3755        foreach ($block->children as $child) {
3756            $this->block($child);
3757        }
3758
3759        if (!empty($block->selectors)) {
3760            if (!$isSingle && empty($block->children)) echo $this->break;
3761
3762            if ($isSingle) {
3763                echo $this->closeSingle . $this->break;
3764            } else {
3765                echo $pre . $this->close . $this->break;
3766            }
3767
3768            $this->indentLevel--;
3769        }
3770    }
3771}
3772
3773#[\AllowDynamicProperties]
3774class lessc_formatter_compressed extends lessc_formatter_classic {
3775    public $disableSingle = true;
3776    public $open = "{";
3777    public $selectorSeparator = ",";
3778    public $assignSeparator = ":";
3779    public $break = "";
3780    public $compressColors = true;
3781
3782    public function indentStr($n = 0) {
3783        return "";
3784    }
3785}
3786
3787class lessc_formatter_lessjs extends lessc_formatter_classic {
3788    public $disableSingle = true;
3789    public $breakSelectors = true;
3790    public $assignSeparator = ": ";
3791    public $selectorSeparator = ",";
3792}