require'pathname'require'fileutils'require'tempfile'require'middleman-core/rack'require'middleman-core/callback_manager'require'middleman-core/contracts'moduleMiddlemanclassBuilderextendForwardableincludeContracts# Make app & events available to `after_build` callbacks.attr_reader:app,:events# Reference to the Thor class.attr_accessor:thor# Logger comes from App.def_delegator:@app,:logger# Sort order, images, fonts, js/css and finally everything else.SORT_ORDER=%w(.png .jpeg .jpg .gif .bmp .svg .svgz .webp .ico .woff .woff2 .otf .ttf .eot .js .css)# Create a new Builder instance.# @param [Middleman::Application] app The app to build.# @param [Hash] opts The builder optionsdefinitialize(app,opts={})@app=app@source_dir=Pathname(File.join(@app.root,@app.config[:source]))@build_dir=Pathname(@app.config[:build_dir])if@build_dir.expand_path.relative_path_from(@source_dir).to_s=~/\A[.\/]+\Z/raise":build_dir (#{@build_dir}) cannot be a parent of :source_dir (#{@source_dir})"end@glob=opts.fetch(:glob)@cleaning=opts.fetch(:clean)rack_app=::Middleman::Rack.new(@app).to_app@rack=::Rack::MockRequest.new(rack_app)@callbacks=::Middleman::CallbackManager.new@callbacks.install_methods!(self,[:on_build_event])end# Run the build phase.# @return [Boolean] Whether the build was successful.ContractBooldefrun!@has_error=false@events={}@app.execute_callbacks(:before_build,[self])queue_current_pathsif@cleaningprerender_cssoutput_filescleanif@cleaning::Middleman::Profiling.report('build')@app.execute_callbacks(:after_build,[self])!@has_errorend# Pre-request CSS to give Compass a chance to build sprites# @return [Array<Resource>] List of css resources that were output.ContractResourceListdefprerender_csslogger.debug'== Prerendering CSS'css_files=@app.sitemap.resources.selectdo|resource|resource.ext=='.css'end.each(&method(:output_resource))logger.debug'== Checking for Compass sprites'# Double-check for compass sprites@app.files.find_new_files!@app.sitemap.ensure_resource_list_updated!css_filesend# Find all the files we need to output and do so.# @return [Array<Resource>] List of resources that were output.ContractResourceListdefoutput_fileslogger.debug'== Building files'@app.sitemap.resources.sort_by{|resource|SORT_ORDER.index(resource.ext)||100}.reject{|resource|resource.ext=='.css'}.select{|resource|!@glob||File.fnmatch(@glob,resource.destination_path)}.each(&method(:output_resource))end# Figure out the correct event mode.# @param [Pathname] output_file The output file path.# @param [String] source The source file path.# @return [Symbol]ContractPathname,String=>Symboldefwhich_mode(output_file,source)if!output_file.exist?:createdelseFileUtils.compare_file(source.to_s,output_file.to_s)?:identical::updatedendend# Create a tempfile for a given output with contents.# @param [Pathname] output_file The output path.# @param [String] contents The file contents.# @return [Tempfile]ContractPathname,String=>Tempfiledefwrite_tempfile(output_file,contents)file=Tempfile.new([File.basename(output_file),File.extname(output_file)])file.binmodefile.write(contents)file.closeFile.chmod(0644,file)fileend# Actually export the file.# @param [Pathname] output_file The path to output to.# @param [String|Pathname] source The source path or contents.# @return [void]ContractPathname,Or[String,Pathname]=>Anydefexport_file!(output_file,source)source=write_tempfile(output_file,source.to_s)ifsource.is_a?Stringmethod,source_path=ifsource.is_a?Tempfile[FileUtils.method(:mv),source.path]else[FileUtils.method(:cp),source.to_s]endmode=which_mode(output_file,source_path)ifmode==:created||mode==:updatedFileUtils.mkdir_p(output_file.dirname)method.call(source_path,output_file.to_s)endsource.unlinkifsource.is_a?Tempfiletrigger(mode,output_file)end# Try to output a resource and capture errors.# @param [Middleman::Sitemap::Resource] resource The resource.# @return [void]ContractIsA['Middleman::Sitemap::Resource']=>Anydefoutput_resource(resource)output_file=@build_dir+resource.destination_path.gsub('%20',' ')beginifresource.binary?export_file!(output_file,resource.file_descriptor[:full_path])elseresponse=@rack.get(URI.escape(resource.request_path))# If we get a response, save it to a tempfile.ifresponse.status==200export_file!(output_file,binary_encode(response.body))else@has_error=truetrigger(:error,output_file,response.body)endendrescue=>e@has_error=truetrigger(:error,output_file,"#{e}\n#{e.backtrace.join("\n")}")endreturnunless@cleaningreturnunlessoutput_file.exist?# handle UTF-8-MAC filename on MacOScleaned_name=ifRUBY_PLATFORM=~/darwin/output_file.to_s.encode('UTF-8','UTF-8-MAC')elseoutput_fileend@to_clean.delete(Pathname(cleaned_name))end# Get a list of all the paths in the destination folder and save them# for comparison against the files we build in this cycle# @return [void]ContractAnydefqueue_current_paths@to_clean=[]returnunlessFile.exist?(@app.config[:build_dir])paths=::Middleman::Util.all_files_under(@app.config[:build_dir]).mapdo|path|Pathname(path)end@to_clean=paths.selectdo|path|path.realpath.relative_path_from(@build_dir.realpath).to_s!~/\/\./||path.to_s=~/\.(htaccess|htpasswd)/end# handle UTF-8-MAC filename on MacOS@to_clean=@to_clean.mapdo|path|ifRUBY_PLATFORM=~/darwin/Pathname(path.to_s.encode('UTF-8','UTF-8-MAC'))elsePathname(path)endendend# Remove files which were not built in this cycleContractArrayOf[Pathname]defclean@to_clean.eachdo|f|FileUtils.rm(f)trigger(:deleted,f)endendContractString=>Stringdefbinary_encode(string)string.force_encoding('ascii-8bit')ifstring.respond_to?(:force_encoding)stringendContractSymbol,Or[String,Pathname],Maybe[String]=>Anydeftrigger(event_type,target,extra=nil)@events[event_type]||=[]@events[event_type]<<targetexecute_callbacks(:on_build_event,[event_type,target,extra])endendend