# frozen_string_literal: true
require "optparse"
module HTMLProofer
class Configuration
DEFAULT_TESTS = ["Links", "Images", "Scripts"].freeze
PROOFER_DEFAULTS = {
allow_hash_href: true,
allow_missing_href: false,
assume_extension: ".html",
check_external_hash: true,
check_internal_hash: true,
checks: DEFAULT_TESTS,
directory_index_file: "index.html",
disable_external: false,
ignore_empty_alt: true,
ignore_empty_mailto: false,
ignore_files: [],
ignore_missing_alt: false,
ignore_status_codes: [],
ignore_urls: [],
enforce_https: true,
extensions: [".html"],
log_level: :info,
only_4xx: false,
swap_attributes: {},
swap_urls: {},
}.freeze
TYPHOEUS_DEFAULTS = {
followlocation: true,
headers: {
"User-Agent" => "Mozilla/5.0 (compatible; HTML Proofer/#{HTMLProofer::VERSION}; +https://github.com/gjtorikian/html-proofer)",
"Accept" => "application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5",
},
connecttimeout: 10,
timeout: 30,
}.freeze
HYDRA_DEFAULTS = {
max_concurrency: 50,
}.freeze
CACHE_DEFAULTS = {}.freeze
class << self
def generate_defaults(opts)
options = PROOFER_DEFAULTS.merge(opts)
options[:typhoeus] = HTMLProofer::Configuration::TYPHOEUS_DEFAULTS.merge(opts[:typhoeus] || {})
options[:hydra] = HTMLProofer::Configuration::HYDRA_DEFAULTS.merge(opts[:hydra] || {})
options[:cache] = HTMLProofer::Configuration::CACHE_DEFAULTS.merge(opts[:cache] || {})
options.delete(:src)
options
end
end
def initialize
@options = {}
end
def parse_cli_options(args)
define_options.parse!(args)
input = ARGV.empty? ? "." : ARGV.join(",")
[@options, input]
end
private def define_options
OptionParser.new do |opts|
opts.banner = "Usage: htmlproofer [options] PATH/LINK"
section(opts, "Input Options") do
set_option(opts, "--as-links") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--assume-extension EXT") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--directory-index-file FILENAME") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--extensions [EXT1,EXT2,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = list.nil? ? [] : list.split(",")
end
end
section(opts, "Check Configuration") do
set_option(opts, "--[no-]allow-hash-href") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]allow-missing-href") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--checks [CHECK1,CHECK2,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = list.nil? ? [] : list.split(",")
end
set_option(opts, "--[no-]check-external-hash") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]check-internal-hash") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]check-sri") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]disable-external") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]enforce-https") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--root-dir
") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
end
section(opts, "Ignore Configuration") do
set_option(opts, "--ignore-files [FILE1,FILE2,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = if list.nil?
[]
else
list.split(",").map.each do |l|
if l.start_with?("/") && l.end_with?("/")
Regexp.new(l[1...-1])
else
l
end
end
end
end
set_option(opts, "--[no-]ignore-empty-alt") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]ignore-empty-mailto") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--[no-]ignore-missing-alt") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
set_option(opts, "--ignore-status-codes [500,401,420,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = list.nil? ? [] : list.split(",").map(&:to_i)
end
set_option(opts, "--ignore-urls [URL1, URL2,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = if list.nil?
[]
else
list.split(",").each_with_object([]) do |url, arr|
arr << to_regex?(url)
end
end
end
set_option(opts, "--only-status-codes [404,451,...]") do |long_opt_symbol, list|
@options[long_opt_symbol] = list.nil? ? [] : list.split(",")
end
set_option(opts, "--only-4xx") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg
end
end
section(opts, "Transforms Configuration") do
set_option(opts, "--swap-attributes ") do |long_opt_symbol, arg|
@options[long_opt_symbol] = parse_json_option("swap_attributes", arg, symbolize_names: false)
end
set_option(opts, "--swap-urls [re:string,re:string,...]") do |long_opt_symbol, arg|
@options[long_opt_symbol] = str_to_regexp_map(arg)
end
end
section(opts, "Dependencies Configuration") do
set_option(opts, "--typhoeus ") do |long_opt_symbol, arg|
@options[long_opt_symbol] = parse_json_option("typhoeus", arg, symbolize_names: false)
end
set_option(opts, "--hydra ") do |long_opt_symbol, arg|
@options[long_opt_symbol] = parse_json_option("hydra", arg, symbolize_names: true)
end
set_option(opts, "--cache ") do |long_opt_symbol, arg|
@options[long_opt_symbol] = parse_json_option("cache", arg, symbolize_names: true)
end
end
section(opts, "Reporting Configuration") do
set_option(opts, "--log-level ") do |long_opt_symbol, arg|
@options[long_opt_symbol] = arg.to_sym
end
end
section(opts, "General Configuration") do
set_option(opts, "--version") do
puts HTMLProofer::VERSION
exit(0)
end
end
end
end
private def to_regex?(item)
if item.start_with?("/") && item.end_with?("/")
Regexp.new(item[1...-1])
else
item
end
end
private def str_to_regexp_map(arg)
arg.split(",").each_with_object({}) do |s, hsh|
split = s.split(/(? :allow_hash_href.
private def parse_long_opt(long_opt)
long_opt[2..].sub("[no-]", "").sub(/ .*/, "").tr("-", "_").gsub(/[\[\]]/, "").to_sym
end
def parse_json_option(option_name, config, symbolize_names: true)
raise ArgumentError, "Must provide an option name in string format." unless option_name.is_a?(String)
raise ArgumentError, "Must provide an option name in string format." if option_name.strip.empty?
return {} if config.nil?
raise ArgumentError, "Must provide a JSON configuration in string format." unless config.is_a?(String)
return {} if config.strip.empty?
begin
JSON.parse(config, { symbolize_names: symbolize_names })
rescue StandardError
raise ArgumentError, "Option '#{option_name} did not contain valid JSON."
end
end
module ConfigurationHelp
TEXT = {
as_links: ["Assumes that `PATH` is a comma-separated array of links to check."],
assume_extension: [
"Automatically add specified extension to files for internal links, ",
"to allow extensionless URLs (as supported by most servers) (default: `.html`).",
],
directory_index_file: ["Sets the file to look for when a link refers to a directory. (default: `index.html`)."],
extensions: [
"A comma-separated list of Strings indicating the file extensions you",
"would like to check (default: `.html`)",
],
allow_hash_href: ['"If `true`, assumes `href="#"` anchors are valid (default: `true`)"'],
allow_missing_href: [
"If `true`, does not flag `a` tags missing `href`. In HTML5, this is technically ",
"allowed, but could also be human error. (default: `false`)",
],
checks: [
"A comma-separated list of Strings indicating which checks you",
"want to run (default: `[\"Links\", \"Images\", \"Scripts\"]",
],
check_external_hash: ["Checks whether external hashes exist (even if the webpage exists) (default: `true`)."],
check_internal_hash: ["Checks whether internal hashes exist (even if the webpage exists) (default: `true`)."],
check_sri: ["Check that `` and `