# frozen_string_literal: true module Asciidoctor # Internal: Except where noted, a module that contains internal helper functions. module Helpers module_function # Public: Require the specified library using Kernel#require. # # Attempts to load the library specified in the first argument using the # Kernel#require. Rescues the LoadError if the library is not available and # passes a message to Kernel#raise if on_failure is :abort or Kernel#warn if # on_failure is :warn to communicate to the user that processing is being # aborted or functionality is disabled, respectively. If a gem_name is # specified, the message communicates that a required gem is not available. # # name - the String name of the library to require. # gem_name - a Boolean that indicates whether this library is provided by a RubyGem, # or the String name of the RubyGem if it differs from the library name # (default: true) # on_failure - a Symbol that indicates how to handle a load failure (:abort, :warn, :ignore) (default: :abort) # # Returns The [Boolean] return value of Kernel#require if the library can be loaded. # Otherwise, if on_failure is :abort, Kernel#raise is called with an appropriate message. # Otherwise, if on_failure is :warn, Kernel#warn is called with an appropriate message and nil returned. # Otherwise, nil is returned. def require_library name, gem_name = true, on_failure = :abort require name rescue ::LoadError include Logging unless include? Logging if gem_name gem_name = name if gem_name == true case on_failure when :abort details = $!.path == gem_name ? '' : %[ (reason: #{$!.path ? %(cannot load '#{$!.path}') : $!.message})] raise ::LoadError, %(asciidoctor: FAILED: required gem '#{gem_name}' is not available#{details}. Processing aborted.) when :warn details = $!.path == gem_name ? '' : %[ (reason: #{$!.path ? %(cannot load '#{$!.path}') : $!.message})] logger.warn %(optional gem '#{gem_name}' is not available#{details}. Functionality disabled.) end else case on_failure when :abort raise ::LoadError, %(asciidoctor: FAILED: #{$!.message.chomp '.'}. Processing aborted.) when :warn logger.warn %(#{$!.message.chomp '.'}. Functionality disabled.) end end nil end # Internal: Prepare the source data Array for parsing. # # Encodes the data to UTF-8, if necessary, and removes any trailing # whitespace from every line. # # If a BOM is found at the beginning of the data, a best attempt is made to # encode it to UTF-8 from the specified source encoding. # # data - the source data Array to prepare (no nil entries allowed) # # returns a String Array of prepared lines def prepare_source_array data return [] if data.empty? if (leading_2_bytes = (leading_bytes = (first = data[0]).unpack 'C3').slice 0, 2) == BOM_BYTES_UTF_16LE data[0] = first.byteslice 2, first.bytesize # NOTE you can't split a UTF-16LE string using .lines when encoding is UTF-8; doing so will cause this line to fail return data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16LE).rstrip } elsif leading_2_bytes == BOM_BYTES_UTF_16BE data[0] = first.byteslice 2, first.bytesize return data.map {|line| (line.encode UTF_8, ::Encoding::UTF_16BE).rstrip } elsif leading_bytes == BOM_BYTES_UTF_8 data[0] = first.byteslice 3, first.bytesize end if first.encoding == UTF_8 data.map {|line| line.rstrip } else data.map {|line| (line.encode UTF_8).rstrip } end end # Internal: Prepare the source data String for parsing. # # Encodes the data to UTF-8, if necessary, splits it into an array, and # removes any trailing whitespace from every line. # # If a BOM is found at the beginning of the data, a best attempt is made to # encode it to UTF-8 from the specified source encoding. # # data - the source data String to prepare # # returns a String Array of prepared lines def prepare_source_string data return [] if data.nil_or_empty? if (leading_2_bytes = (leading_bytes = data.unpack 'C3').slice 0, 2) == BOM_BYTES_UTF_16LE data = (data.byteslice 2, data.bytesize).encode UTF_8, ::Encoding::UTF_16LE elsif leading_2_bytes == BOM_BYTES_UTF_16BE data = (data.byteslice 2, data.bytesize).encode UTF_8, ::Encoding::UTF_16BE elsif leading_bytes == BOM_BYTES_UTF_8 data = data.byteslice 3, data.bytesize data = data.encode UTF_8 unless data.encoding == UTF_8 elsif data.encoding != UTF_8 data = data.encode UTF_8 end [].tap {|lines| data.each_line {|line| lines << line.rstrip } } end # Internal: Efficiently checks whether the specified String resembles a URI # # Uses the Asciidoctor::UriSniffRx regex to check whether the String begins # with a URI prefix (e.g., http://). No validation of the URI is performed. # # str - the String to check # # returns true if the String is a URI, false if it is not def uriish? str (str.include? ':') && (UriSniffRx.match? str) end # Internal: Encode a URI component String for safe inclusion in a URI. # # str - the URI component String to encode # # Returns the String with all reserved URI characters encoded (e.g., /, &, =, space, etc). if RUBY_ENGINE == 'opal' def encode_uri_component str # patch necessary to adhere with RFC-3986 (and thus CGI.escape) # see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Description %x( return encodeURIComponent(str).replace(/%20|[!'()*]/g, function (m) { return m === '%20' ? '+' : '%' + m.charCodeAt(0).toString(16) }) ) end else CGI = ::CGI def encode_uri_component str CGI.escape str end end # Internal: Apply URI path encoding to spaces in the specified string (i.e., convert spaces to %20). # # str - the String to encode # # Returns the specified String with all spaces replaced with %20. def encode_spaces_in_uri str (str.include? ' ') ? (str.gsub ' ', '%20') : str end # Public: Removes the file extension from filename and returns the result # # filename - The String file name to process; expected to be a posix path # # Examples # # Helpers.rootname 'part1/chapter1.adoc' # # => "part1/chapter1" # # Returns the String filename with the file extension removed def rootname filename if (last_dot_idx = filename.rindex '.') (filename.index '/', last_dot_idx) ? filename : (filename.slice 0, last_dot_idx) else filename end end # Public: Retrieves the basename of the filename, optionally removing the extension, if present # # filename - The String file name to process. # drop_ext - A Boolean flag indicating whether to drop the extension # or an explicit String extension to drop (default: nil). # # Examples # # Helpers.basename 'images/tiger.png', true # # => "tiger" # # Helpers.basename 'images/tiger.png', '.png' # # => "tiger" # # Returns the String filename with leading directories removed and, if specified, the extension removed def basename filename, drop_ext = nil if drop_ext ::File.basename filename, (drop_ext == true ? (extname filename) : drop_ext) else ::File.basename filename end end # Public: Returns whether this path has a file extension. # # path - The path String to check; expects a posix path # # Returns true if the path has a file extension, false otherwise def extname? path (last_dot_idx = path.rindex '.') && !(path.index '/', last_dot_idx) end # Public: Retrieves the file extension of the specified path. The file extension is the portion of the path in the # last path segment starting from the last period. # # This method differs from File.extname in that it gives us control over the fallback value and is more efficient. # # path - The path String in which to look for a file extension # fallback - The fallback String to return if no file extension is present (optional, default: '') # # Returns the String file extension (with the leading dot included) or the fallback value if the path has no file extension. if ::File::ALT_SEPARATOR def extname path, fallback = '' if (last_dot_idx = path.rindex '.') (path.index '/', last_dot_idx) || (path.index ::File::ALT_SEPARATOR, last_dot_idx) ? fallback : (path.slice last_dot_idx, path.length) else fallback end end else def extname path, fallback = '' if (last_dot_idx = path.rindex '.') (path.index '/', last_dot_idx) ? fallback : (path.slice last_dot_idx, path.length) else fallback end end end # Internal: Make a directory, ensuring all parent directories exist. def mkdir_p dir unless ::File.directory? dir unless (parent_dir = ::File.dirname dir) == '.' mkdir_p parent_dir end begin ::Dir.mkdir dir rescue ::SystemCallError raise unless ::File.directory? dir end end end ROMAN_NUMERALS = { 'M' => 1000, 'CM' => 900, 'D' => 500, 'CD' => 400, 'C' => 100, 'XC' => 90, 'L' => 50, 'XL' => 40, 'X' => 10, 'IX' => 9, 'V' => 5, 'IV' => 4, 'I' => 1 } private_constant :ROMAN_NUMERALS # Internal: Converts an integer to a Roman numeral. # # val - the [Integer] value to convert # # Returns the [String] roman numeral for this integer def int_to_roman val ROMAN_NUMERALS.map do |l, i| repeat, val = val.divmod i l * repeat end.join end # Internal: Get the next value in the sequence. # # Handles both integer and character sequences. # # current - the value to increment as a String or Integer # # returns the next value in the sequence according to the current value's type def nextval current if ::Integer === current current + 1 else intval = current.to_i if intval.to_s != current.to_s (current[0].ord + 1).chr else intval + 1 end end end # Internal: Resolve the specified object as a Class # # object - The Object to resolve as a Class # # Returns a Class if the specified object is a Class (but not a Module) or # a String that resolves to a Class; otherwise, nil def resolve_class object ::Class === object ? object : (::String === object ? (class_for_name object) : nil) end # Internal: Resolves a Class object (not a Module) for the qualified name. # # Returns Class def class_for_name qualified_name raise unless ::Class === (resolved = ::Object.const_get qualified_name, false) resolved rescue raise ::NameError, %(Could not resolve class for name: #{qualified_name}) end end end