lib/asciidoctor/abstract_node.rb in asciidoctor-1.5.8 vs lib/asciidoctor/abstract_node.rb in asciidoctor-2.0.0.rc.1

- old
+ new

@@ -1,30 +1,29 @@ -# encoding: UTF-8 +# frozen_string_literal: true module Asciidoctor # Public: An abstract base class that provides state and methods for managing a # node of AsciiDoc content. The state and methods on this class are common to # all content segments in an AsciiDoc document. class AbstractNode - include Logging - include Substitutors + include Substitutors, Logging # Public: Get the Hash of attributes for this node attr_reader :attributes # Public: Get the Symbol context for this node attr_reader :context # Public: Get the Asciidoctor::Document to which this node belongs attr_reader :document - # Public: Get/Set the id of this node + # Public: Get/Set the String id of this node attr_accessor :id # Public: Get the String name of this node attr_reader :node_name - # Public: Get the element which is the parent of this node + # Public: Get the AbstractBlock parent element of this node attr_reader :parent def initialize parent, context, opts = {} if context == :document # document is a special case, should refer to itself @@ -37,11 +36,11 @@ end end @node_name = (@context = context).to_s # QUESTION are we correct in duplicating the attributes (seems to be just as fast) @attributes = (opts.key? :attributes) ? opts[:attributes].dup : {} - @passthroughs = {} + @passthroughs = [] end # Public: Returns whether this {AbstractNode} is an instance of {Block} # # Returns [Boolean] @@ -73,55 +72,47 @@ # Returns the new parent Block associated with this Block def parent= parent @parent, @document = parent, parent.document end - # Public: Get the value of the specified attribute + # Public: Get the value of the specified attribute. If the attribute is not found on this node, fallback_name is set, + # and this node is not the Document node, get the value of the specified attribute from the Document node. # - # Get the value for the specified attribute. First look in the attributes on - # this node and return the value of the attribute if found. Otherwise, if - # this node is a child of the Document node, look in the attributes of the - # Document node and return the value of the attribute if found. Otherwise, - # return the default value, which defaults to nil. + # Look for the specified attribute in the attributes on this node and return the value of the attribute, if found. + # Otherwise, if fallback_name is set (default: same as name) and this node is not the Document node, look for that + # attribute on the Document node and return its value, if found. Otherwise, return the default value (default: nil). # - # name - the String or Symbol name of the attribute to lookup - # default_val - the Object value to return if the attribute is not found (default: nil) - # inherit - a Boolean indicating whether to check for the attribute on the - # AsciiDoctor::Document if not found on this node (default: false) + # name - The String or Symbol name of the attribute to resolve. + # default_value - The Object value to return if the attribute is not found (default: nil). + # fallback_name - The String or Symbol of the attribute to resolve on the Document if the attribute is not found on + # this node (default: same as name). # - # return the value of the attribute or the default value if the attribute - # is not found in the attributes of this node or the document node - def attr name, default_val = nil, inherit = true - name = name.to_s - # NOTE if @parent is set, it means @document is also set - @attributes[name] || (inherit && @parent ? @document.attributes[name] || default_val : default_val) + # Returns the [Object] value (typically a String) of the attribute or default_value if the attribute is not found. + def attr name, default_value = nil, fallback_name = nil + @attributes[name.to_s] || (fallback_name && @parent && @document.attributes[(fallback_name == true ? name : fallback_name).to_s] || default_value) end - # Public: Check if the attribute is defined, optionally performing a - # comparison of its value if expected is not nil + # Public: Check if the specified attribute is defined using the same logic as {#attr}, optionally performing a + # comparison with the expected value if specified. # - # Check if the attribute is defined. First look in the attributes on this - # node. If not found, and this node is a child of the Document node, look in - # the attributes of the Document node. If the attribute is found and a - # comparison value is specified (not nil), return whether the two values match. - # Otherwise, return whether the attribute was found. + # Look for the specified attribute in the attributes on this node. If not found, fallback_name is specified (default: + # same as name), and this node is not the Document node, look for that attribute on the Document node. In either case, + # if the attribute is found, and the comparison value is truthy, return whether the two values match. Otherwise, + # return whether the attribute was found. # - # name - the String or Symbol name of the attribute to lookup - # expect_val - the expected Object value of the attribute (default: nil) - # inherit - a Boolean indicating whether to check for the attribute on the - # AsciiDoctor::Document if not found on this node (default: false) + # name - The String or Symbol name of the attribute to resolve. + # expected_value - The expected Object value of the attribute (default: nil). + # fallback_name - The String or Symbol of the attribute to resolve on the Document if the attribute is not found on + # this node (default: same as name). # - # return a Boolean indicating whether the attribute exists and, if a - # comparison value is specified, whether the value of the attribute matches - # the comparison value - def attr? name, expect_val = nil, inherit = true - name = name.to_s - # NOTE if @parent is set, it means @document is also set - if expect_val.nil? - (@attributes.key? name) || (inherit && @parent && (@document.attributes.key? name)) + # Returns a [Boolean] indicating whether the attribute exists and, if a truthy comparison value is specified, whether + # the value of the attribute matches the comparison value. + def attr? name, expected_value = nil, fallback_name = nil + if expected_value + expected_value == (@attributes[name.to_s] || (fallback_name && @parent ? @document.attributes[(fallback_name == true ? name : fallback_name).to_s] : nil)) else - expect_val == (@attributes[name] || (inherit && @parent ? @document.attributes[name] : nil)) + (@attributes.key? name.to_s) || (fallback_name && @parent ? (@document.attributes.key? (fallback_name == true ? name : fallback_name).to_s) : false) end end # Public: Assign the value to the attribute name for the current node. # @@ -151,40 +142,38 @@ # Public: A convenience method to check if the specified option attribute is # enabled on the current node. # # Check if the option is enabled. This method simply checks to see if the - # %name%-option attribute is defined on the current node. + # <name>-option attribute is defined on the current node. # # name - the String or Symbol name of the option # # return a Boolean indicating whether the option has been specified - def option?(name) - @attributes.key? %(#{name}-option) + def option? name + @attributes[%(#{name}-option)] ? true : false end # Public: Set the specified option on this node. # - # This method sets the specified option on this node if not already set. - # It will add the name to the options attribute and set the <name>-option - # attribute. + # This method sets the specified option on this node by setting the <name>-option attribute. # # name - the String name of the option # - # returns truthy if the option was set or falsey if the option was already set - def set_option(name) - if (attrs = @attributes)['options'] - unless attrs[key = %(#{name}-option)] - attrs['options'] += %(,#{name}) - attrs[key] = '' - end - else - attrs['options'] = name - attrs[%(#{name}-option)] = '' - end + # Returns Nothing + def set_option name + @attributes[%(#{name}-option)] = '' + nil end + # Public: Retrieve the Set of option names that are set on this node + # + # Returns a [Set] of option names + def options + ::Set.new.tap {|accum| @attributes.each_key {|k| accum << (k.slice 0, k.length - 7) if k.to_s.end_with? '-option' } } + end + # Public: Update the attributes of this node with the new values in # the attributes argument. # # If an attribute already exists with the same key, it's value will # be overwritten. @@ -195,63 +184,70 @@ def update_attributes(attributes) @attributes.update(attributes) nil end - # Public: A convenience method that returns the value of the role attribute + # Public: Retrieves the space-separated String role for this node. + # + # Returns the role as a space-separated [String]. def role - @attributes['role'] || @document.attributes['role'] + @attributes['role'] end - # Public: A convenience method that returns the role names as an Array + # Public: Retrieves the String role names for this node as an Array. # - # Returns the role names as an Array or an empty Array if the role attribute is absent. + # Returns the role names as a String [Array], which is empty if the role attribute is absent on this node. def roles - (val = @attributes['role'] || @document.attributes['role']).nil_or_empty? ? [] : val.split + (val = @attributes['role']) ? val.split : [] end - # Public: A convenience method that checks if the role attribute is specified - def role? expect_val = nil - if expect_val - expect_val == (@attributes['role'] || @document.attributes['role']) - else - @attributes.key?('role') || @document.attributes.key?('role') - end + # Public: Checks if the role attribute is set on this node and, if an expected value is given, whether the + # space-separated role matches that value. + # + # expected_value - The expected String value of the role (optional, default: nil) + # + # Returns a [Boolean] indicating whether the role attribute is set on this node and, if an expected value is given, + # whether the space-separated role matches that value. + def role? expected_value = nil + expected_value ? expected_value == @attributes['role'] : (@attributes.key? 'role') end - # Public: A convenience method that checks if the specified role is present - # in the list of roles on this node - def has_role?(name) + # Public: Checks if the specified role is present in the list of roles for this node. + # + # name - The String name of the role to find. + # + # Returns a [Boolean] indicating whether this node has the specified role. + def has_role? name # NOTE center + include? is faster than split + include? - (val = @attributes['role'] || @document.attributes['role']) ? %( #{val} ).include?(%( #{name} )) : false + (val = @attributes['role']) ? (%( #{val} ).include? %( #{name} )) : false end - # Public: A convenience method that adds the given role directly to this node + # Public: Adds the given role directly to this node. # - # Returns a Boolean indicating whether the role was added. - def add_role(name) - if (val = @attributes['role']).nil_or_empty? + # Returns a [Boolean] indicating whether the role was added. + def add_role name + if (val = @attributes['role']) + # NOTE center + include? is faster than split + include? + if %( #{val} ).include? %( #{name} ) + false + else + @attributes['role'] = %(#{val} #{name}) + true + end + else @attributes['role'] = name true - # NOTE center + include? is faster than split + include? - elsif %( #{val} ).include?(%( #{name} )) - false - else - @attributes['role'] = %(#{val} #{name}) - true end end - # Public: A convenience method that removes the given role directly from this node + # Public: Removes the given role directly from this node. # - # Returns a Boolean indicating whether the role was removed. - def remove_role(name) - if (val = @attributes['role']).nil_or_empty? - false - elsif (val = val.split).delete name + # Returns a [Boolean] indicating whether the role was removed. + def remove_role name + if (val = @attributes['role']) && ((val = val.split).delete name) if val.empty? - @attributes.delete('role') + @attributes.delete 'role' else @attributes['role'] = val.join ' ' end true else @@ -271,27 +267,28 @@ # Public: Construct a reference or data URI to an icon image for the # specified icon name. # # If the 'icon' attribute is set on this block, the name is ignored and the - # value of this attribute is used as the target image path. Otherwise, + # value of this attribute is used as the target image path. Otherwise, # construct a target image path by concatenating the value of the 'iconsdir' - # attribute, the icon name and the value of the 'icontype' attribute + # attribute, the icon name, and the value of the 'icontype' attribute # (defaulting to 'png'). # - # The target image path is then passed through the #image_uri() method. If + # The target image path is then passed through the #image_uri() method. If # the 'data-uri' attribute is set on the document, the image will be # safely converted to a data URI. # # The return value of this method can be safely used in an image tag. # # name - The String name of the icon # # Returns A String reference or data URI for an icon image def icon_uri name if attr? 'icon' - if ::File.extname(icon = (attr 'icon')).empty? + # Ruby 2.3 requires the extra brackets around the attr method call + if (::File.extname (icon = (attr 'icon'))).empty? # QUESTION should we be adding the extension if the icon is an absolute URI? icon = %(#{icon}.#{@document.attr 'icontype', 'png'}) end else icon = %(#{name}.#{@document.attr 'icontype', 'png'}) @@ -318,11 +315,11 @@ # the image is located (default: 'imagesdir') # # Returns A String reference or data URI for the target image def image_uri(target_image, asset_dir_key = 'imagesdir') if (doc = @document).safe < SafeMode::SECURE && (doc.attr? 'data-uri') - if ((Helpers.uriish? target_image) && (target_image = uri_encode_spaces target_image)) || + if ((Helpers.uriish? target_image) && (target_image = Helpers.encode_uri target_image)) || (asset_dir_key && (images_base = doc.attr asset_dir_key) && (Helpers.uriish? images_base) && (target_image = normalize_web_path target_image, images_base, false)) if doc.attr? 'allow-uri-read' generate_data_uri_from_uri target_image, (doc.attr? 'cache-uri') else @@ -369,18 +366,18 @@ def generate_data_uri(target_image, asset_dir_key = nil) ext = ::File.extname target_image # QUESTION what if ext is empty? mimetype = (ext == '.svg' ? 'image/svg+xml' : %(image/#{ext.slice 1, ext.length})) if asset_dir_key - image_path = normalize_system_path(target_image, @document.attr(asset_dir_key), nil, :target_name => 'image') + image_path = normalize_system_path(target_image, @document.attr(asset_dir_key), nil, target_name: 'image') else image_path = normalize_system_path(target_image) end if ::File.readable? image_path # NOTE base64 is autoloaded by reference to ::Base64 - %(data:#{mimetype};base64,#{::Base64.strict_encode64 ::IO.binread image_path}) + %(data:#{mimetype};base64,#{::Base64.strict_encode64 ::File.binread image_path}) else logger.warn %(image to embed not found or not readable: #{image_path}) %(data:#{mimetype};base64,) # uncomment to return 1 pixel white dot instead #'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' @@ -402,21 +399,17 @@ def generate_data_uri_from_uri image_uri, cache_uri = false if cache_uri # caching requires the open-uri-cached gem to be installed # processing will be automatically aborted if these libraries can't be opened Helpers.require_library 'open-uri/cached', 'open-uri-cached' - elsif !::RUBY_ENGINE_OPAL + elsif !RUBY_ENGINE_OPAL # autoload open-uri ::OpenURI end begin - mimetype = nil - bindata = open image_uri, 'rb' do |f| - mimetype = f.content_type - f.read - end + mimetype, bindata = ::OpenURI.open_uri(image_uri, URI_READ_MODE) {|f| [f.content_type, f.read] } # NOTE base64 is autoloaded by reference to ::Base64 %(data:#{mimetype};base64,#{::Base64.strict_encode64 bindata}) rescue logger.warn %(could not retrieve image data from URI: #{image_uri}) image_uri @@ -430,12 +423,11 @@ # Public: Normalize the asset file or directory to a concrete and rinsed path # # Delegates to normalize_system_path, with the start path set to the value of # the base_dir instance variable on the Document object. def normalize_asset_path(asset_ref, asset_name = 'path', autocorrect = true) - normalize_system_path(asset_ref, @document.base_dir, nil, - :target_name => asset_name, :recover => autocorrect) + normalize_system_path(asset_ref, @document.base_dir, nil, target_name: asset_name, recover: autocorrect) end # Public: Resolve and normalize a secure path from the target and start paths # using the PathResolver. # @@ -484,11 +476,11 @@ # preserve_uri_target - a Boolean indicating whether target should be preserved if contains a URI (default: true) # # Returns the resolved [String] path def normalize_web_path(target, start = nil, preserve_uri_target = true) if preserve_uri_target && (Helpers.uriish? target) - uri_encode_spaces target + Helpers.encode_uri target else @document.path_resolver.web_path target, start end end @@ -505,19 +497,14 @@ # # Returns the [String] content of the file at the specified path, or nil # if the file does not exist. def read_asset path, opts = {} # remap opts for backwards compatibility - opts = { :warn_on_failure => (opts != false) } unless ::Hash === opts + opts = { warn_on_failure: (opts != false) } unless ::Hash === opts if ::File.readable? path - if opts[:normalize] - # NOTE Opal does not yet support File#readlines - (Helpers.normalize_lines_array ::File.open(path, 'rb') {|f| f.each_line.to_a }).join LF - else - # QUESTION should we chomp or rstrip content? - ::IO.read path - end + # QUESTION should we chomp content if normalize is false? + opts[:normalize] ? ((Helpers.prepare_source_string ::File.read path, mode: FILE_READ_MODE).join LF) : (::File.read path, mode: FILE_READ_MODE) elsif opts[:warn_on_failure] logger.warn %(#{(attr 'docfile') || '<stdin>'}: #{opts[:label] || 'file'} does not exist or cannot be read: #{path}) nil end end @@ -544,39 +531,31 @@ (target = doc.path_resolver.web_path target, start)) if doc.attr? 'allow-uri-read' Helpers.require_library 'open-uri/cached', 'open-uri-cached' if doc.attr? 'cache-uri' begin if opts[:normalize] - # NOTE Opal does not yet support File#readlines - (Helpers.normalize_lines_array ::OpenURI.open_uri(target) {|f| f.each_line.to_a }).join LF + (Helpers.prepare_source_string ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read }).join LF else - ::OpenURI.open_uri(target) {|f| f.read } + ::OpenURI.open_uri(target, URI_READ_MODE) {|f| f.read } end rescue logger.warn %(could not retrieve contents of #{opts[:label] || 'asset'} at URI: #{target}) if opts.fetch :warn_on_failure, true return end else logger.warn %(cannot retrieve contents of #{opts[:label] || 'asset'} at URI: #{target} (allow-uri-read attribute not enabled)) if opts.fetch :warn_on_failure, true return end else - target = normalize_system_path target, opts[:start], nil, :target_name => (opts[:label] || 'asset') - read_asset target, :normalize => opts[:normalize], :warn_on_failure => (opts.fetch :warn_on_failure, true), :label => opts[:label] + target = normalize_system_path target, opts[:start], nil, target_name: (opts[:label] || 'asset') + read_asset target, normalize: opts[:normalize], warn_on_failure: (opts.fetch :warn_on_failure, true), label: opts[:label] end end - # Internal: URI encode spaces in a String - # - # str - the String to encode - # - # Returns the String with all spaces replaced with %20. - def uri_encode_spaces str - (str.include? ' ') ? (str.gsub ' ', '%20') : str - end - - # Public: Check whether the specified String is a URI by + # Deprecated: Check whether the specified String is a URI by # matching it against the Asciidoctor::UriSniffRx regex. + # + # In use by Asciidoctor PDF # # @deprecated Use Helpers.uriish? instead def is_uri? str Helpers.uriish? str end