* @author Adam Pedersen (Issue 55 fix) */ class Minify_Lines { /** * Add line numbers in C-style comments * * This uses a very basic parser easily fooled by comment tokens inside * strings or regexes, but, otherwise, generally clean code will not be * mangled. URI rewriting can also be performed. * * @param string $content * * @param array $options available options: * * 'id': (optional) string to identify file. E.g. file name/path * * 'currentDir': (default null) if given, this is assumed to be the * directory of the current CSS file. Using this, minify will rewrite * all relative URIs in import/url declarations to correctly point to * the desired files, and prepend a comment with debugging information about * this process. * * @return string */ public static function minify($content, $options = array()) { $id = (isset($options['id']) && $options['id']) ? $options['id'] : ''; $content = str_replace("\r\n", "\n", $content); $lines = explode("\n", $content); $numLines = count($lines); // determine left padding $padTo = strlen((string) $numLines); // e.g. 103 lines = 3 digits $inComment = false; $i = 0; $newLines = array(); while (null !== ($line = array_shift($lines))) { if (('' !== $id) && (0 === $i % 50)) { if ($inComment) { array_push($newLines, '', "/* {$id} *|", ''); } else { array_push($newLines, '', "/* {$id} */", ''); } } ++$i; $newLines[] = self::_addNote($line, $i, $inComment, $padTo); $inComment = self::_eolInComment($line, $inComment); } $content = implode("\n", $newLines) . "\n"; // check for desired URI rewriting if (isset($options['currentDir'])) { Minify_CSS_UriRewriter::$debugText = ''; $docRoot = isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT']; $symlinks = isset($options['symlinks']) ? $options['symlinks'] : array(); $content = Minify_CSS_UriRewriter::rewrite($content, $options['currentDir'], $docRoot, $symlinks); $content = "/* Minify_CSS_UriRewriter::\$debugText\n\n" . Minify_CSS_UriRewriter::$debugText . "*/\n" . $content; } return $content; } /** * Is the parser within a C-style comment at the end of this line? * * @param string $line current line of code * * @param bool $inComment was the parser in a C-style comment at the * beginning of the previous line? * * @return bool */ private static function _eolInComment($line, $inComment) { while (strlen($line)) { if ($inComment) { // only "*/" can end the comment $index = self::_find($line, '*/'); if ($index === false) { return true; } // stop comment and keep walking line $inComment = false; @$line = (string)substr($line, $index + 2); continue; } // look for "//" and "/*" $single = self::_find($line, '//'); $multi = self::_find($line, '/*'); if ($multi === false) { return false; } if ($single === false || $multi < $single) { // start comment and keep walking line $inComment = true; @$line = (string)substr($line, $multi + 2); continue; } // a single-line comment preceeded it return false; } return $inComment; } /** * Prepend a comment (or note) to the given line * * @param string $line current line of code * * @param string $note content of note/comment * * @param bool $inComment was the parser in a comment at the * beginning of the line? * * @param int $padTo minimum width of comment * * @return string */ private static function _addNote($line, $note, $inComment, $padTo) { if ($inComment) { $line = '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' *| ' . $line; } else { $line = '/* ' . str_pad($note, $padTo, ' ', STR_PAD_RIGHT) . ' */ ' . $line; } return rtrim($line); } /** * Find a token trying to avoid false positives * * @param string $str String containing the token * @param string $token Token being checked * @return bool */ private static function _find($str, $token) { switch ($token) { case '//': $fakes = array( '://' => 1, '"//' => 1, '\'//' => 1, '".//' => 2, '\'.//' => 2, ); break; case '/*': $fakes = array( '"/*' => 1, '\'/*' => 1, '"//*' => 2, '\'//*' => 2, '".//*' => 3, '\'.//*' => 3, '*/*' => 1, '\\/*' => 1, ); break; default: $fakes = array(); } $index = strpos($str, $token); $offset = 0; while ($index !== false) { foreach ($fakes as $fake => $skip) { $check = substr($str, $index - $skip, strlen($fake)); if ($check === $fake) { // move offset and scan again $offset += $index + strlen($token); $index = strpos($str, $token, $offset); break; } } // legitimate find return $index; } return $index; } }