--- /dev/null
+<?php\r
+\r
+/**\r
+ * Source map generator\r
+ *\r
+ * @package Less\r
+ * @subpackage Output\r
+ */\r
+class Less_SourceMap_Generator extends Less_Configurable {\r
+\r
+ /**\r
+ * What version of source map does the generator generate?\r
+ */\r
+ const VERSION = 3;\r
+\r
+ /**\r
+ * Array of default options\r
+ *\r
+ * @var array\r
+ */\r
+ protected $defaultOptions = array(\r
+ // an optional source root, useful for relocating source files\r
+ // on a server or removing repeated values in the 'sources' entry.\r
+ // This value is prepended to the individual entries in the 'source' field.\r
+ 'sourceRoot' => '',\r
+\r
+ // an optional name of the generated code that this source map is associated with.\r
+ 'sourceMapFilename' => null,\r
+\r
+ // url of the map\r
+ 'sourceMapURL' => null,\r
+\r
+ // absolute path to a file to write the map to\r
+ 'sourceMapWriteTo' => null,\r
+\r
+ // output source contents?\r
+ 'outputSourceFiles' => false,\r
+\r
+ // base path for filename normalization\r
+ 'sourceMapRootpath' => '',\r
+\r
+ // base path for filename normalization\r
+ 'sourceMapBasepath' => ''\r
+ );\r
+\r
+ /**\r
+ * The base64 VLQ encoder\r
+ *\r
+ * @var Less_SourceMap_Base64VLQ\r
+ */\r
+ protected $encoder;\r
+\r
+ /**\r
+ * Array of mappings\r
+ *\r
+ * @var array\r
+ */\r
+ protected $mappings = array();\r
+\r
+ /**\r
+ * The root node\r
+ *\r
+ * @var Less_Tree_Ruleset\r
+ */\r
+ protected $root;\r
+\r
+ /**\r
+ * Array of contents map\r
+ *\r
+ * @var array\r
+ */\r
+ protected $contentsMap = array();\r
+\r
+ /**\r
+ * File to content map\r
+ *\r
+ * @var array\r
+ */\r
+ protected $sources = array();\r
+ protected $source_keys = array();\r
+\r
+ /**\r
+ * Constructor\r
+ *\r
+ * @param Less_Tree_Ruleset $root The root node\r
+ * @param array $options Array of options\r
+ */\r
+ public function __construct(Less_Tree_Ruleset $root, $contentsMap, $options = array()){\r
+ $this->root = $root;\r
+ $this->contentsMap = $contentsMap;\r
+ $this->encoder = new Less_SourceMap_Base64VLQ();\r
+\r
+ $this->SetOptions($options);\r
+ \r
+ $this->options['sourceMapRootpath'] = $this->fixWindowsPath($this->options['sourceMapRootpath'], true);\r
+ $this->options['sourceMapBasepath'] = $this->fixWindowsPath($this->options['sourceMapBasepath'], true);\r
+ }\r
+\r
+ /**\r
+ * Generates the CSS\r
+ *\r
+ * @return string\r
+ */\r
+ public function generateCSS(){\r
+ $output = new Less_Output_Mapped($this->contentsMap, $this);\r
+\r
+ // catch the output\r
+ $this->root->genCSS($output);\r
+\r
+\r
+ $sourceMapUrl = $this->getOption('sourceMapURL');\r
+ $sourceMapFilename = $this->getOption('sourceMapFilename');\r
+ $sourceMapContent = $this->generateJson();\r
+ $sourceMapWriteTo = $this->getOption('sourceMapWriteTo');\r
+\r
+ if( !$sourceMapUrl && $sourceMapFilename ){\r
+ $sourceMapUrl = $this->normalizeFilename($sourceMapFilename);\r
+ }\r
+\r
+ // write map to a file\r
+ if( $sourceMapWriteTo ){\r
+ $this->saveMap($sourceMapWriteTo, $sourceMapContent);\r
+ }\r
+\r
+ // inline the map\r
+ if( !$sourceMapUrl ){\r
+ $sourceMapUrl = sprintf('data:application/json,%s', Less_Functions::encodeURIComponent($sourceMapContent));\r
+ }\r
+\r
+ if( $sourceMapUrl ){\r
+ $output->add( sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl) );\r
+ }\r
+\r
+ return $output->toString();\r
+ }\r
+\r
+ /**\r
+ * Saves the source map to a file\r
+ *\r
+ * @param string $file The absolute path to a file\r
+ * @param string $content The content to write\r
+ * @throws Exception If the file could not be saved\r
+ */\r
+ protected function saveMap($file, $content){\r
+ $dir = dirname($file);\r
+ // directory does not exist\r
+ if( !is_dir($dir) ){\r
+ // FIXME: create the dir automatically?\r
+ throw new Exception(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));\r
+ }\r
+ // FIXME: proper saving, with dir write check!\r
+ if(file_put_contents($file, $content) === false){\r
+ throw new Exception(sprintf('Cannot save the source map to "%s"', $file));\r
+ }\r
+ return true;\r
+ }\r
+\r
+ /**\r
+ * Normalizes the filename\r
+ *\r
+ * @param string $filename\r
+ * @return string\r
+ */\r
+ protected function normalizeFilename($filename){\r
+\r
+ $filename = $this->fixWindowsPath($filename);\r
+\r
+ $rootpath = $this->getOption('sourceMapRootpath');\r
+ $basePath = $this->getOption('sourceMapBasepath');\r
+\r
+ // "Trim" the 'sourceMapBasepath' from the output filename.\r
+ if (strpos($filename, $basePath) === 0) {\r
+ $filename = substr($filename, strlen($basePath));\r
+ }\r
+\r
+ // Remove extra leading path separators.\r
+ if(strpos($filename, '\\') === 0 || strpos($filename, '/') === 0){\r
+ $filename = substr($filename, 1);\r
+ }\r
+\r
+ return $rootpath . $filename;\r
+ }\r
+\r
+ /**\r
+ * Adds a mapping\r
+ *\r
+ * @param integer $generatedLine The line number in generated file\r
+ * @param integer $generatedColumn The column number in generated file\r
+ * @param integer $originalLine The line number in original file\r
+ * @param integer $originalColumn The column number in original file\r
+ * @param string $sourceFile The original source file\r
+ */\r
+ public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $fileInfo ){\r
+\r
+ $this->mappings[] = array(\r
+ 'generated_line' => $generatedLine,\r
+ 'generated_column' => $generatedColumn,\r
+ 'original_line' => $originalLine,\r
+ 'original_column' => $originalColumn,\r
+ 'source_file' => $fileInfo['currentUri']\r
+ );\r
+\r
+ $this->sources[$fileInfo['currentUri']] = $fileInfo['filename'];\r
+ }\r
+\r
+\r
+ /**\r
+ * Generates the JSON source map\r
+ *\r
+ * @return string\r
+ * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#\r
+ */\r
+ protected function generateJson(){\r
+\r
+ $sourceMap = array();\r
+ $mappings = $this->generateMappings();\r
+\r
+ // File version (always the first entry in the object) and must be a positive integer.\r
+ $sourceMap['version'] = self::VERSION;\r
+\r
+\r
+ // An optional name of the generated code that this source map is associated with.\r
+ $file = $this->getOption('sourceMapFilename');\r
+ if( $file ){\r
+ $sourceMap['file'] = $file;\r
+ }\r
+\r
+\r
+ // An optional source root, useful for relocating source files on a server or removing repeated values in the 'sources' entry. This value is prepended to the individual entries in the 'source' field.\r
+ $root = $this->getOption('sourceRoot');\r
+ if( $root ){\r
+ $sourceMap['sourceRoot'] = $root;\r
+ }\r
+\r
+\r
+ // A list of original sources used by the 'mappings' entry.\r
+ $sourceMap['sources'] = array();\r
+ foreach($this->sources as $source_uri => $source_filename){\r
+ $sourceMap['sources'][] = $this->normalizeFilename($source_filename);\r
+ }\r
+\r
+\r
+ // A list of symbol names used by the 'mappings' entry.\r
+ $sourceMap['names'] = array();\r
+\r
+ // A string with the encoded mapping data.\r
+ $sourceMap['mappings'] = $mappings;\r
+\r
+ if( $this->getOption('outputSourceFiles') ){\r
+ // An optional list of source content, useful when the 'source' can't be hosted.\r
+ // The contents are listed in the same order as the sources above.\r
+ // 'null' may be used if some original sources should be retrieved by name.\r
+ $sourceMap['sourcesContent'] = $this->getSourcesContent();\r
+ }\r
+\r
+ // less.js compat fixes\r
+ if( count($sourceMap['sources']) && empty($sourceMap['sourceRoot']) ){\r
+ unset($sourceMap['sourceRoot']);\r
+ }\r
+\r
+ return json_encode($sourceMap);\r
+ }\r
+\r
+ /**\r
+ * Returns the sources contents\r
+ *\r
+ * @return array|null\r
+ */\r
+ protected function getSourcesContent(){\r
+ if(empty($this->sources)){\r
+ return;\r
+ }\r
+ $content = array();\r
+ foreach($this->sources as $sourceFile){\r
+ $content[] = file_get_contents($sourceFile);\r
+ }\r
+ return $content;\r
+ }\r
+\r
+ /**\r
+ * Generates the mappings string\r
+ *\r
+ * @return string\r
+ */\r
+ public function generateMappings(){\r
+\r
+ if( !count($this->mappings) ){\r
+ return '';\r
+ }\r
+\r
+ $this->source_keys = array_flip(array_keys($this->sources));\r
+\r
+\r
+ // group mappings by generated line number.\r
+ $groupedMap = $groupedMapEncoded = array();\r
+ foreach($this->mappings as $m){\r
+ $groupedMap[$m['generated_line']][] = $m;\r
+ }\r
+ ksort($groupedMap);\r
+\r
+ $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;\r
+\r
+ foreach($groupedMap as $lineNumber => $line_map){\r
+ while(++$lastGeneratedLine < $lineNumber){\r
+ $groupedMapEncoded[] = ';';\r
+ }\r
+\r
+ $lineMapEncoded = array();\r
+ $lastGeneratedColumn = 0;\r
+\r
+ foreach($line_map as $m){\r
+ $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);\r
+ $lastGeneratedColumn = $m['generated_column'];\r
+\r
+ // find the index\r
+ if( $m['source_file'] ){\r
+ $index = $this->findFileIndex($m['source_file']);\r
+ if( $index !== false ){\r
+ $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);\r
+ $lastOriginalIndex = $index;\r
+\r
+ // lines are stored 0-based in SourceMap spec version 3\r
+ $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);\r
+ $lastOriginalLine = $m['original_line'] - 1;\r
+\r
+ $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);\r
+ $lastOriginalColumn = $m['original_column'];\r
+ }\r
+ }\r
+\r
+ $lineMapEncoded[] = $mapEncoded;\r
+ }\r
+\r
+ $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';\r
+ }\r
+\r
+ return rtrim(implode($groupedMapEncoded), ';');\r
+ }\r
+\r
+ /**\r
+ * Finds the index for the filename\r
+ *\r
+ * @param string $filename\r
+ * @return integer|false\r
+ */\r
+ protected function findFileIndex($filename){\r
+ return $this->source_keys[$filename];\r
+ }\r
+\r
+ /**\r
+ * fix windows paths\r
+ * @param string $path\r
+ * @return string \r
+ */\r
+ public function fixWindowsPath($path, $addEndSlash = false){\r
+ $slash = ($addEndSlash) ? '/' : '';\r
+ if( !empty($path) ){\r
+ $path = str_replace('\\', '/', $path);\r
+ $path = rtrim($path,'/') . $slash;\r
+ }\r
+\r
+ return $path;\r
+ }\r
+\r
+}
\ No newline at end of file