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