require "asciidoctor" require_relative "errors" require "csl/styles" module AsciidoctorBibliography class Options < Hash PREFIX = "bibliography-".freeze DEFAULTS = { "bibliography-database" => nil, "bibliography-locale" => "en-US", "bibliography-style" => "apa", "bibliography-hyperlinks" => "true", "bibliography-order" => "alphabetical", # TODO: deprecate "bibliography-tex-style" => "authoryear", "bibliography-sort" => nil, "bibliography-prepend-empty" => "true", "bibliography-passthrough" => "false" }.freeze def initialize merge DEFAULTS end def self.build(document, reader) header_attributes = get_header_attributes_hash reader header_attributes.select! { |key, _| DEFAULTS.keys.include? key } cli_attributes = document.attributes.select { |key, _| DEFAULTS.keys.include? key } new.merge!(header_attributes).merge!(cli_attributes) end def self.get_header_attributes_hash(reader) # NOTE: we peek at the document attributes using a throwaway document/reader pair # so the parsing flow isn't perturbed in any way. tmp_document = ::Asciidoctor::Document.new # NOTE: peek_lines processes `include` directives (among other things), # so we're able to get document attributes from included files. tmp_reader = ::Asciidoctor::PreprocessorReader.new(tmp_document, reader.peek_lines) ::Asciidoctor::Parser.parse_document_header(tmp_reader, tmp_document) tmp_document.attributes end def style # First we check whether an internal style exists to qualify its path. if self["bibliography-style"] filepath = File.join AsciidoctorBibliography.csl_styles_root, self["bibliography-style"] + ".csl" self["bibliography-style"] = filepath if File.exist? filepath end # Then error throwing is delegated to CSL library. self["bibliography-style"] || DEFAULTS["bibliography-style"] end def locale value = self["bibliography-locale"] || DEFAULTS["bibliography-locale"] raise_invalid <<~MESSAGE unless CSL::Locale.list.include? value Option :bibliography-locale: has an invalid value (#{value}). Allowed values are #{CSL::Locale.list.inspect}. MESSAGE value end def hyperlinks? value = self["bibliography-hyperlinks"] || DEFAULTS["bibliography-hyperlinks"] raise_invalid <<~MESSAGE unless %w[true false].include? value Option :bibliography-hyperlinks: has an invalid value (#{value}). Allowed values are 'true' and 'false'. MESSAGE value == "true" end def database value = self["bibliography-database"] || DEFAULTS["bibliography-database"] raise Errors::Options::Missing, <<~MESSAGE if value.nil? Option :bibliography-database: is mandatory. A bibliographic database is required. MESSAGE value end def sort begin value = YAML.safe_load self["bibliography-sort"].to_s rescue Psych::SyntaxError => psych_error raise_invalid <<~MESSAGE Option :bibliography-sort: is not a valid YAML string: \"#{psych_error}\". MESSAGE end value = validate_parsed_sort_type! value value = validate_parsed_sort_contents! value unless value.nil? value end def tex_style value = self["bibliography-tex-style"] || DEFAULTS["bibliography-tex-style"] raise_invalid <<~MESSAGE unless %w[authoryear numeric].include? value Option :bibliography-tex-style: has an invalid value (#{value}). Allowed values are 'authoryear' (default) and 'numeric'. MESSAGE value end def passthrough?(context) # NOTE: allowed contexts are :citation and :reference value = self["bibliography-passthrough"] || DEFAULTS["bibliography-passthrough"] raise_invalid <<~MESSAGE unless %w[true citations references false].include? value Option :bibliography-passthrough: has an invalid value (#{value}). Allowed values are 'true', 'citations', 'references' and 'false'. MESSAGE evaluate_ext_boolean_value_vs_context value: value, context: context end def prepend_empty?(context) # NOTE: allowed contexts are :citation and :reference value = self["bibliography-prepend-empty"] || DEFAULTS["bibliography-prepend-empty"] raise_invalid <<~MESSAGE unless %w[true citations references false].include? value Option :bibliography-prepend-empty: has an invalid value (#{value}). Allowed values are 'true', 'citations', 'references' and 'false'. MESSAGE evaluate_ext_boolean_value_vs_context value: value, context: context end private def evaluate_ext_boolean_value_vs_context(value:, context:) return true if value.to_s == "true" return false if value.to_s == "false" return context.to_s == "citation" if value.to_s == "citations" return context.to_s == "reference" if value.to_s == "references" end def raise_invalid(message) raise Errors::Options::Invalid, message end def validate_parsed_sort_type!(value) return value if value.nil? return value if value.is_a?(Array) && value.all? { |v| v.is_a? Hash } return [value] if value.is_a? Hash raise_invalid <<~MESSAGE Option :bibliography-sort: has an invalid value (#{value}). Please refer to manual for more info. MESSAGE end def validate_parsed_sort_contents!(array) # TODO: should we restrict these? Double check the CSL spec. allowed_keys = %w[variable macro sort names-min names-use-first names-use-last] return array unless array.any? { |hash| (hash.keys - allowed_keys).any? } raise_invalid <<~MESSAGE Option :bibliography-sort: has a value containing invalid keys (#{array}). Allowed keys are #{allowed_keys.inspect}. Please refer to manual for more info. MESSAGE end end end