MDL-49746 course: sorting users by last access
[moodle.git] / lib / lessphp / SourceMap / Generator.php
blobac5f2c927762856fadb434bfeeb7291c3c860e78
1 <?php
3 /**
4 * Source map generator
6 * @package Less
7 * @subpackage Output
8 */
9 class Less_SourceMap_Generator extends Less_Configurable {
11 /**
12 * What version of source map does the generator generate?
14 const VERSION = 3;
16 /**
17 * Array of default options
19 * @var array
21 protected $defaultOptions = array(
22 // an optional source root, useful for relocating source files
23 // on a server or removing repeated values in the 'sources' entry.
24 // This value is prepended to the individual entries in the 'source' field.
25 'sourceRoot' => '',
27 // an optional name of the generated code that this source map is associated with.
28 'sourceMapFilename' => null,
30 // url of the map
31 'sourceMapURL' => null,
33 // absolute path to a file to write the map to
34 'sourceMapWriteTo' => null,
36 // output source contents?
37 'outputSourceFiles' => false,
39 // base path for filename normalization
40 'sourceMapRootpath' => '',
42 // base path for filename normalization
43 'sourceMapBasepath' => ''
46 /**
47 * The base64 VLQ encoder
49 * @var Less_SourceMap_Base64VLQ
51 protected $encoder;
53 /**
54 * Array of mappings
56 * @var array
58 protected $mappings = array();
60 /**
61 * The root node
63 * @var Less_Tree_Ruleset
65 protected $root;
67 /**
68 * Array of contents map
70 * @var array
72 protected $contentsMap = array();
74 /**
75 * File to content map
77 * @var array
79 protected $sources = array();
80 protected $source_keys = array();
82 /**
83 * Constructor
85 * @param Less_Tree_Ruleset $root The root node
86 * @param array $options Array of options
88 public function __construct(Less_Tree_Ruleset $root, $contentsMap, $options = array()){
89 $this->root = $root;
90 $this->contentsMap = $contentsMap;
91 $this->encoder = new Less_SourceMap_Base64VLQ();
93 $this->SetOptions($options);
96 // fix windows paths
97 if( !empty($this->options['sourceMapRootpath']) ){
98 $this->options['sourceMapRootpath'] = str_replace('\\', '/', $this->options['sourceMapRootpath']);
99 $this->options['sourceMapRootpath'] = rtrim($this->options['sourceMapRootpath'],'/').'/';
104 * Generates the CSS
106 * @return string
108 public function generateCSS(){
109 $output = new Less_Output_Mapped($this->contentsMap, $this);
111 // catch the output
112 $this->root->genCSS($output);
115 $sourceMapUrl = $this->getOption('sourceMapURL');
116 $sourceMapFilename = $this->getOption('sourceMapFilename');
117 $sourceMapContent = $this->generateJson();
118 $sourceMapWriteTo = $this->getOption('sourceMapWriteTo');
120 if( !$sourceMapUrl && $sourceMapFilename ){
121 $sourceMapUrl = $this->normalizeFilename($sourceMapFilename);
124 // write map to a file
125 if( $sourceMapWriteTo ){
126 $this->saveMap($sourceMapWriteTo, $sourceMapContent);
129 // inline the map
130 if( !$sourceMapUrl ){
131 $sourceMapUrl = sprintf('data:application/json,%s', Less_Functions::encodeURIComponent($sourceMapContent));
134 if( $sourceMapUrl ){
135 $output->add( sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl) );
138 return $output->toString();
142 * Saves the source map to a file
144 * @param string $file The absolute path to a file
145 * @param string $content The content to write
146 * @throws Exception If the file could not be saved
148 protected function saveMap($file, $content){
149 $dir = dirname($file);
150 // directory does not exist
151 if( !is_dir($dir) ){
152 // FIXME: create the dir automatically?
153 throw new Exception(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));
155 // FIXME: proper saving, with dir write check!
156 if(file_put_contents($file, $content) === false){
157 throw new Exception(sprintf('Cannot save the source map to "%s"', $file));
159 return true;
163 * Normalizes the filename
165 * @param string $filename
166 * @return string
168 protected function normalizeFilename($filename){
170 $filename = str_replace('\\', '/', $filename);
171 $rootpath = $this->getOption('sourceMapRootpath');
172 $basePath = $this->getOption('sourceMapBasepath');
174 // "Trim" the 'sourceMapBasepath' from the output filename.
175 if (strpos($filename, $basePath) === 0) {
176 $filename = substr($filename, strlen($basePath));
179 // Remove extra leading path separators.
180 if(strpos($filename, '\\') === 0 || strpos($filename, '/') === 0){
181 $filename = substr($filename, 1);
184 return $rootpath . $filename;
188 * Adds a mapping
190 * @param integer $generatedLine The line number in generated file
191 * @param integer $generatedColumn The column number in generated file
192 * @param integer $originalLine The line number in original file
193 * @param integer $originalColumn The column number in original file
194 * @param string $sourceFile The original source file
196 public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $fileInfo ){
198 $this->mappings[] = array(
199 'generated_line' => $generatedLine,
200 'generated_column' => $generatedColumn,
201 'original_line' => $originalLine,
202 'original_column' => $originalColumn,
203 'source_file' => $fileInfo['currentUri']
206 $this->sources[$fileInfo['currentUri']] = $fileInfo['filename'];
211 * Generates the JSON source map
213 * @return string
214 * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
216 protected function generateJson(){
218 $sourceMap = array();
219 $mappings = $this->generateMappings();
221 // File version (always the first entry in the object) and must be a positive integer.
222 $sourceMap['version'] = self::VERSION;
225 // An optional name of the generated code that this source map is associated with.
226 $file = $this->getOption('sourceMapFilename');
227 if( $file ){
228 $sourceMap['file'] = $file;
232 // 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.
233 $root = $this->getOption('sourceRoot');
234 if( $root ){
235 $sourceMap['sourceRoot'] = $root;
239 // A list of original sources used by the 'mappings' entry.
240 $sourceMap['sources'] = array();
241 foreach($this->sources as $source_uri => $source_filename){
242 $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
246 // A list of symbol names used by the 'mappings' entry.
247 $sourceMap['names'] = array();
249 // A string with the encoded mapping data.
250 $sourceMap['mappings'] = $mappings;
252 if( $this->getOption('outputSourceFiles') ){
253 // An optional list of source content, useful when the 'source' can't be hosted.
254 // The contents are listed in the same order as the sources above.
255 // 'null' may be used if some original sources should be retrieved by name.
256 $sourceMap['sourcesContent'] = $this->getSourcesContent();
259 // less.js compat fixes
260 if( count($sourceMap['sources']) && empty($sourceMap['sourceRoot']) ){
261 unset($sourceMap['sourceRoot']);
264 return json_encode($sourceMap);
268 * Returns the sources contents
270 * @return array|null
272 protected function getSourcesContent(){
273 if(empty($this->sources)){
274 return;
276 $content = array();
277 foreach($this->sources as $sourceFile){
278 $content[] = file_get_contents($sourceFile);
280 return $content;
284 * Generates the mappings string
286 * @return string
288 public function generateMappings(){
290 if( !count($this->mappings) ){
291 return '';
294 $this->source_keys = array_flip(array_keys($this->sources));
297 // group mappings by generated line number.
298 $groupedMap = $groupedMapEncoded = array();
299 foreach($this->mappings as $m){
300 $groupedMap[$m['generated_line']][] = $m;
302 ksort($groupedMap);
304 $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
306 foreach($groupedMap as $lineNumber => $line_map){
307 while(++$lastGeneratedLine < $lineNumber){
308 $groupedMapEncoded[] = ';';
311 $lineMapEncoded = array();
312 $lastGeneratedColumn = 0;
314 foreach($line_map as $m){
315 $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
316 $lastGeneratedColumn = $m['generated_column'];
318 // find the index
319 if( $m['source_file'] ){
320 $index = $this->findFileIndex($m['source_file']);
321 if( $index !== false ){
322 $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
323 $lastOriginalIndex = $index;
325 // lines are stored 0-based in SourceMap spec version 3
326 $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
327 $lastOriginalLine = $m['original_line'] - 1;
329 $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
330 $lastOriginalColumn = $m['original_column'];
334 $lineMapEncoded[] = $mapEncoded;
337 $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
340 return rtrim(implode($groupedMapEncoded), ';');
344 * Finds the index for the filename
346 * @param string $filename
347 * @return integer|false
349 protected function findFileIndex($filename){
350 return $this->source_keys[$filename];