1<?php
2/**
3 *
4 *  General utilities.
5 *
6 */
7namespace CssCrush;
8
9class Util
10{
11    public static function htmlAttributes(array $attributes, array $sort_order = null)
12    {
13        // Optionally sort attributes (for better readability).
14        if ($sort_order) {
15            uksort($attributes, function ($a, $b) use ($sort_order) {
16                $a_index = array_search($a, $sort_order);
17                $b_index = array_search($b, $sort_order);
18                $a_found = is_int($a_index);
19                $b_found = is_int($b_index);
20
21                if ($a_found && $b_found) {
22                    if ($a_index == $b_index) {
23                        return 0;
24                    }
25                    return $a_index > $b_index ? 1 : -1;
26                }
27                elseif ($a_found && ! $b_found) {
28                    return -1;
29                }
30                elseif ($b_found && ! $a_found) {
31                    return 1;
32                }
33
34                return strcmp($a, $b);
35            });
36        }
37
38        $str = '';
39        foreach ($attributes as $name => $value) {
40            $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8', false);
41            $str .= " $name=\"$value\"";
42        }
43        return $str;
44    }
45
46    public static function normalizePath($path, $strip_drive_letter = false)
47    {
48        if (! $path) {
49            return '';
50        }
51
52        if ($strip_drive_letter) {
53            $path = preg_replace('~^[a-z]\:~i', '', $path);
54        }
55
56        // Backslashes and repeat slashes to a single forward slash.
57        $path = rtrim(preg_replace('~[\\\\/]+~', '/', $path), '/');
58
59        // Removing redundant './'.
60        $path = str_replace('/./', '/', $path);
61        if (strpos($path, './') === 0) {
62            $path = substr($path, 2);
63        }
64
65        return Util::simplifyPath($path);
66    }
67
68    public static function simplifyPath($path)
69    {
70        // Reduce redundant path segments. e.g 'foo/../bar' => 'bar'
71        $patt = '~[^/.]+/\.\./~S';
72        while (preg_match($patt, $path)) {
73            $path = preg_replace($patt, '', $path);
74        }
75        return $path;
76    }
77
78    public static function resolveUserPath($path, $recovery = null, $docRoot = null)
79    {
80        // System path.
81        if ($realpath = realpath($path)) {
82            $path = $realpath;
83        }
84        else {
85            if (! $docRoot) {
86                $docRoot = isset(Crush::$process->docRoot) ? Crush::$process->docRoot : Crush::$config->docRoot;
87            }
88
89            // Absolute path.
90            if (strpos($path, '/') === 0) {
91                // If $path is not doc_root based assume it's doc_root relative and prepend doc_root.
92                if (strpos($path, $docRoot) !== 0) {
93                    $path = $docRoot . $path;
94                }
95            }
96            // Relative path. Try resolving based on the directory of the executing script.
97            else {
98                $path = Crush::$config->scriptDir . '/' . $path;
99            }
100
101            if (! file_exists($path) && is_callable($recovery)) {
102                $path = $recovery($path);
103            }
104            $path = realpath($path);
105        }
106
107        return $path ? Util::normalizePath($path) : false;
108    }
109
110    public static function stripCommentTokens($str)
111    {
112        return preg_replace(Regex::$patt->c_token, '', $str);
113    }
114
115    public static function normalizeWhiteSpace($str)
116    {
117        static $find, $replace;
118        if (! $find) {
119            $replacements = array(
120                // Convert all whitespace sequences to a single space.
121                '~\s+~S' => ' ',
122                // Trim bracket whitespace where it's safe to do it.
123                '~([\[(]) | ([\])])| ?([{}]) ?~S' => '${1}${2}${3}',
124                // Trim whitespace around delimiters and special characters.
125                '~ ?([;,]) ?~S' => '$1',
126            );
127            $find = array_keys($replacements);
128            $replace = array_values($replacements);
129        }
130
131        return preg_replace($find, $replace, $str);
132    }
133
134    public static function splitDelimList($str, $options = array())
135    {
136        extract($options + array(
137            'delim' => ',',
138            'regex' => false,
139            'allow_empty_strings' => false,
140        ));
141
142        $str = trim($str);
143
144        if (! $regex && strpos($str, $delim) === false) {
145            return ! $allow_empty_strings && ! strlen($str) ? array() : array($str);
146        }
147
148        if ($match_count = preg_match_all(Regex::$patt->parens, $str, $matches)) {
149            $keys = array();
150            foreach ($matches[0] as $index => &$value) {
151                $keys[] = "?$index?";
152            }
153            $str = str_replace($matches[0], $keys, $str);
154        }
155
156        $list = $regex ? preg_split($regex, $str) : explode($delim, $str);
157
158        if ($match_count) {
159            foreach ($list as &$value) {
160                $value = str_replace($keys, $matches[0], $value);
161            }
162        }
163
164        $list = array_map('trim', $list);
165
166        return ! $allow_empty_strings ? array_filter($list, 'strlen') : $list;
167    }
168
169    public static function getLinkBetweenPaths($path1, $path2, $directories = true)
170    {
171        $path1 = trim(Util::normalizePath($path1, true), '/');
172        $path2 = trim(Util::normalizePath($path2, true), '/');
173
174        $link = '';
175
176        if ($path1 != $path2) {
177
178            // Split the directory paths into arrays so we can compare segment by segment.
179            $path1_segs = explode('/', $path1);
180            $path2_segs = explode('/', $path2);
181
182            // Shift the segments until they are on different branches.
183            while (isset($path1_segs[0]) && isset($path2_segs[0]) && ($path1_segs[0] === $path2_segs[0])) {
184                array_shift($path1_segs);
185                array_shift($path2_segs);
186            }
187
188            $link = str_repeat('../', count($path1_segs)) . implode('/', $path2_segs);
189        }
190
191        $link = $link !== '' ? rtrim($link, '/') : '';
192
193        // Append end slash if getting a link between directories.
194        if ($link && $directories) {
195            $link .= '/';
196        }
197
198        return $link;
199    }
200
201    public static function filePutContents($file, $str)
202    {
203        if ($stream = fopen($file, 'w')) {
204            fwrite($stream, $str);
205            fclose($stream);
206
207            return true;
208        }
209
210        warning("Could not write file '$file'.");
211
212        return false;
213    }
214
215    public static function parseIni($path, $sections = false)
216    {
217        if (! ($result = @parse_ini_file($path, $sections))) {
218            notice("Ini file '$path' could not be parsed.");
219
220            return false;
221        }
222        return $result;
223    }
224
225    public static function readConfigFile($path)
226    {
227        require_once $path;
228        return Options::filter(get_defined_vars());
229    }
230
231    /*
232     * Get raw value (useful if testing values that may or may not be a token).
233     */
234    public static function rawValue($value)
235    {
236        if ($tokenType = Tokens::test($value)) {
237            if ($tokenType == 'u') {
238                $value = Crush::$process->tokens->get($value)->value;
239            }
240            elseif ($tokenType == 's') {
241                $value = Crush::$process->tokens->get($value);
242            }
243        }
244
245        return $value;
246    }
247
248    /*
249     * Encode integer to Base64 VLQ.
250     */
251    public static function vlqEncode($value)
252    {
253        static $VLQ_BASE_SHIFT, $VLQ_BASE, $VLQ_BASE_MASK, $VLQ_CONTINUATION_BIT, $BASE64_MAP;
254        if (! $VLQ_BASE_SHIFT) {
255            $VLQ_BASE_SHIFT = 5;
256            $VLQ_BASE = 1 << $VLQ_BASE_SHIFT;
257            $VLQ_BASE_MASK = $VLQ_BASE - 1;
258            $VLQ_CONTINUATION_BIT = $VLQ_BASE;
259            $BASE64_MAP = str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/');
260        }
261
262        $vlq = $value < 0 ? ((-$value) << 1) + 1 : ($value << 1) + 0;
263
264        $encoded = "";
265        do {
266          $digit = $vlq & $VLQ_BASE_MASK;
267          $vlq >>= $VLQ_BASE_SHIFT;
268          if ($vlq > 0) {
269            $digit |= $VLQ_CONTINUATION_BIT;
270          }
271          $encoded .= $BASE64_MAP[$digit];
272
273        } while ($vlq > 0);
274
275        return $encoded;
276    }
277}
278