352 lines
13 KiB
Ruby
352 lines
13 KiB
Ruby
# 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 <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 <CONFIG>") 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 <CONFIG>") do |long_opt_symbol, arg|
|
|
@options[long_opt_symbol] = parse_json_option("typhoeus", arg, symbolize_names: false)
|
|
end
|
|
|
|
set_option(opts, "--hydra <CONFIG>") do |long_opt_symbol, arg|
|
|
@options[long_opt_symbol] = parse_json_option("hydra", arg, symbolize_names: true)
|
|
end
|
|
|
|
set_option(opts, "--cache <CONFIG>") 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 <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(/(?<!\\):/, 2)
|
|
|
|
re = split[0].gsub("\\:", ":")
|
|
string = split[1].gsub("\\:", ":")
|
|
hsh[Regexp.new(re)] = string
|
|
end
|
|
end
|
|
|
|
private def section(opts, heading, &_block)
|
|
opts.separator("\n#{heading}:\n")
|
|
yield
|
|
end
|
|
|
|
private def set_option(opts, long_arg, &block)
|
|
long_opt_symbol = parse_long_opt(long_arg)
|
|
args = []
|
|
args += Array(ConfigurationHelp::TEXT[long_opt_symbol])
|
|
|
|
opts.on(long_arg, *args) do |arg|
|
|
yield long_opt_symbol, arg
|
|
end
|
|
end
|
|
|
|
# Converts the option into a symbol,
|
|
# e.g. '--allow-hash-href' => :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 `<link>` and `<script>` external resources use SRI (default: `false`)."],
|
|
disable_external: ["If `true`, does not run the external link checker (default: `false`)."],
|
|
enforce_https: ["Fails a link if it's not marked as `https` (default: `true`)."],
|
|
root_dir: ["The absolute path to the directory serving your html-files."],
|
|
|
|
ignore_empty_alt: [
|
|
"If `true`, ignores images with empty/missing ",
|
|
"alt tags (in other words, `<img alt>` and `<img alt=\"\">`",
|
|
"are valid; set this to `false` to flag those) (default: `true`).",
|
|
],
|
|
ignore_empty_mailto: [
|
|
"If `true`, allows `mailto:` `href`s which don't",
|
|
"contain an email address (default: `false`)'.",
|
|
],
|
|
ignore_missing_alt: ["If `true`, ignores images with missing alt tags (default: `false`)."],
|
|
ignore_status_codes: ["A comma-separated list of numbers representing status codes to ignore."],
|
|
ignore_files: ["A comma-separated list of Strings or RegExps containing file paths that are safe to ignore"],
|
|
ignore_urls: [
|
|
"A comma-separated list of Strings or RegExps containing URLs that are",
|
|
"safe to ignore. This affects all HTML attributes, such as `alt` tags on images.",
|
|
],
|
|
only_status_codes: ["A comma-separated list of numbers representing the only status codes to report on."],
|
|
only_4xx: ["Only reports errors for links that fall within the 4xx status code range."],
|
|
|
|
swap_attributes: [
|
|
"JSON-formatted config that maps element names to the",
|
|
"preferred attribute to check (default: `{}`).",
|
|
],
|
|
swap_urls: [
|
|
"A comma-separated list containing key-value pairs of `RegExp => String`.",
|
|
"It transforms URLs that match `RegExp` into `String` via `gsub`.",
|
|
"The escape sequences `\\:` should be used to produce literal `:`s.",
|
|
],
|
|
|
|
typhoeus: ["JSON-formatted string of Typhoeus config; if set, overrides the html-proofer defaults."],
|
|
hydra: ["JSON-formatted string of Hydra config; if set, overrides the html-proofer defaults."],
|
|
cache: ["JSON-formatted string of cache config; if set, overrides the html-proofer defaults."],
|
|
|
|
log_level: [
|
|
"Sets the logging level. One of `:debug`, `:info`, ",
|
|
"`:warn`, `:error`, or `:fatal`. (default: `:info`)",
|
|
],
|
|
|
|
version: ["Prints the version of html-proofer."],
|
|
}.freeze
|
|
end
|
|
end
|
|
end
|