# frozen_string_literal: true require "webrick" module Jekyll module Commands class Serve # This class is used to determine if the Servlet should modify a served file # to insert the LiveReload script tags class SkipAnalyzer BAD_USER_AGENTS = [%r!MSIE!].freeze def self.skip_processing?(request, response, options) new(request, response, options).skip_processing? end def initialize(request, response, options) @options = options @request = request @response = response end def skip_processing? !html? || chunked? || inline? || bad_browser? end def chunked? @response["Transfer-Encoding"] == "chunked" end def inline? @response["Content-Disposition"].to_s.start_with?("inline") end def bad_browser? BAD_USER_AGENTS.any? { |pattern| pattern.match?(@request["User-Agent"]) } end def html? @response["Content-Type"].to_s.include?("text/html") end end # This class inserts the LiveReload script tags into HTML as it is served class BodyProcessor HEAD_TAG_REGEX = %r!
|!.freeze attr_reader :content_length, :new_body, :livereload_added def initialize(body, options) @body = body @options = options @processed = false end def processed? @processed end # rubocop:disable Metrics/MethodLength def process! @new_body = [] # @body will usually be a File object but Strings occur in rare cases if @body.respond_to?(:each) begin @body.each { |line| @new_body << line.to_s } ensure @body.close end else @new_body = @body.lines end @content_length = 0 @livereload_added = false @new_body.each do |line| if !@livereload_added && line[" document.write( ' TEMPLATE end def livereload_args # XHTML standard requires ampersands to be encoded as entities when in # attributes. See http://stackoverflow.com/a/2190292 src = "" if @options["livereload_min_delay"] src += "&mindelay=#{@options["livereload_min_delay"]}" end if @options["livereload_max_delay"] src += "&maxdelay=#{@options["livereload_max_delay"]}" end src += "&port=#{@options["livereload_port"]}" if @options["livereload_port"] src end end class Servlet < WEBrick::HTTPServlet::FileHandler DEFAULTS = { "Cache-Control" => "private, max-age=0, proxy-revalidate, " \ "no-store, no-cache, must-revalidate", }.freeze def initialize(server, root, callbacks) # So we can access them easily. @jekyll_opts = server.config[:JekyllOptions] set_defaults super end def search_index_file(req, res) super || search_file(req, res, ".html") || search_file(req, res, ".xhtml") end # Add the ability to tap file.html the same way that Nginx does on our # Docker images (or on GitHub Pages.) The difference is that we might end # up with a different preference on which comes first. def search_file(req, res, basename) # /file.* > /file/index.html > /file.html super || super(req, res, "#{basename}.html") || super(req, res, "#{basename}.xhtml") end # rubocop:disable Naming/MethodName def do_GET(req, res) rtn = super if @jekyll_opts["livereload"] return rtn if SkipAnalyzer.skip_processing?(req, res, @jekyll_opts) processor = BodyProcessor.new(res.body, @jekyll_opts) processor.process! res.body = processor.new_body res.content_length = processor.content_length.to_s if processor.livereload_added # Add a header to indicate that the page content has been modified res["X-Rack-LiveReload"] = "1" end end validate_and_ensure_charset(req, res) res.header.merge!(@headers) rtn end # rubocop:enable Naming/MethodName private def validate_and_ensure_charset(_req, res) key = res.header.keys.grep(%r!content-type!i).first typ = res.header[key] unless %r!;\s*charset=!.match?(typ) res.header[key] = "#{typ}; charset=#{@jekyll_opts["encoding"]}" end end def set_defaults hash_ = @jekyll_opts.fetch("webrick", {}).fetch("headers", {}) DEFAULTS.each_with_object(@headers = hash_) do |(key, val), hash| hash[key] = val unless hash.key?(key) end end end end end end