"Fossies" - the Fresh Open Source Software Archive

Member "asciidoctor-2.0.10/lib/asciidoctor/attribute_list.rb" (1 Jun 2019, 6123 Bytes) of package /linux/www/asciidoctor-2.0.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Ruby source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 # frozen_string_literal: true
    2 module Asciidoctor
    3 # Public: Handles parsing AsciiDoc attribute lists into a Hash of key/value
    4 # pairs. By default, attributes must each be separated by a comma and quotes
    5 # may be used around the value. If a key is not detected, the value is assigned
    6 # to a 1-based positional key, The positional attributes can be "rekeyed" when
    7 # given a positional_attrs array either during parsing or after the fact.
    8 #
    9 # Examples
   10 #
   11 #    attrlist = Asciidoctor::AttributeList.new('astyle')
   12 #
   13 #    attrlist.parse
   14 #    => { 0 => 'astyle' }
   15 #
   16 #    attrlist.rekey(['style'])
   17 #    => { 'style' => 'astyle' }
   18 #
   19 #    attrlist = Asciidoctor::AttributeList.new('quote, Famous Person, Famous Book (2001)')
   20 #
   21 #    attrlist.parse(['style', 'attribution', 'citetitle'])
   22 #    => { 'style' => 'quote', 'attribution' => 'Famous Person', 'citetitle' => 'Famous Book (2001)' }
   23 #
   24 class AttributeList
   25   BACKSLASH = '\\'
   26   APOS = '\''
   27 
   28   # Public: Regular expressions for detecting the boundary of a value
   29   BoundaryRxs = {
   30     '"' => /.*?[^\\](?=")/,
   31     APOS => /.*?[^\\](?=')/,
   32     ',' => /.*?(?=[ \t]*(,|$))/
   33   }
   34 
   35   # Public: Regular expressions for unescaping quoted characters
   36   EscapedQuotes = {
   37     '"' => '\\"',
   38     APOS => '\\\''
   39   }
   40 
   41   # Public: A regular expression for an attribute name (approx. name token from XML)
   42   # TODO named attributes cannot contain dash characters
   43   NameRx = /#{CG_WORD}[#{CC_WORD}\-.]*/
   44 
   45   BlankRx = /[ \t]+/
   46 
   47   # Public: Regular expressions for skipping delimiters
   48   SkipRxs = { ',' => /[ \t]*(,|$)/ }
   49 
   50   def initialize source, block = nil, delimiter = ','
   51     @scanner = ::StringScanner.new source
   52     @block = block
   53     @delimiter = delimiter
   54     @delimiter_skip_pattern = SkipRxs[delimiter]
   55     @delimiter_boundary_pattern = BoundaryRxs[delimiter]
   56     @attributes = nil
   57   end
   58 
   59   def parse_into attributes, positional_attrs = []
   60     attributes.update parse positional_attrs
   61   end
   62 
   63   def parse positional_attrs = []
   64     # return if already parsed
   65     return @attributes if @attributes
   66 
   67     @attributes = {}
   68     # QUESTION do we want to store the attribute list as the zero-index attribute?
   69     #attributes[0] = @scanner.string
   70     index = 0
   71 
   72     while parse_attribute index, positional_attrs
   73       break if @scanner.eos?
   74       skip_delimiter
   75       index += 1
   76     end
   77 
   78     @attributes
   79   end
   80 
   81   def rekey positional_attrs
   82     AttributeList.rekey @attributes, positional_attrs
   83   end
   84 
   85   def self.rekey attributes, positional_attrs
   86     index = 0
   87     positional_attrs.each do |key|
   88       index += 1
   89       if (val = attributes[index])
   90         # QUESTION should we delete the positional key?
   91         attributes[key] = val
   92       end if key
   93     end
   94     attributes
   95   end
   96 
   97   private
   98 
   99   def parse_attribute index = 0, positional_attrs = []
  100     single_quoted_value = false
  101     skip_blank
  102     # example: "quote"
  103     if (first = @scanner.peek(1)) == '"'
  104       name = parse_attribute_value @scanner.get_byte
  105       value = nil
  106     # example: 'quote'
  107     elsif first == APOS
  108       name = parse_attribute_value @scanner.get_byte
  109       value = nil
  110       single_quoted_value = true unless name.start_with? APOS
  111     else
  112       name = scan_name
  113 
  114       skipped = 0
  115       c = nil
  116       if @scanner.eos?
  117         return false unless name
  118       else
  119         skipped = skip_blank || 0
  120         c = @scanner.get_byte
  121       end
  122 
  123       # example: quote
  124       if !c || c == @delimiter
  125         value = nil
  126       # example: Sherlock Holmes || =foo=
  127       elsif c != '=' || !name
  128         name = %(#{name}#{' ' * skipped}#{c}#{scan_to_delimiter})
  129         value = nil
  130       else
  131         skip_blank
  132         if @scanner.peek(1)
  133           # example: foo="bar" || foo="ba\"zaar"
  134           if (c = @scanner.get_byte) == '"'
  135             value = parse_attribute_value c
  136           # example: foo='bar' || foo='ba\'zaar' || foo='ba"zaar'
  137           elsif c == APOS
  138             value = parse_attribute_value c
  139             single_quoted_value = true unless value.start_with? APOS
  140           # example: foo=,
  141           elsif c == @delimiter
  142             value = ''
  143           # example: foo=bar (all spaces ignored)
  144           else
  145             value = %(#{c}#{scan_to_delimiter})
  146             return true if value == 'None'
  147           end
  148         end
  149       end
  150     end
  151 
  152     if value
  153       # example: options="opt1,opt2,opt3"
  154       # opts is an alias for options
  155       case name
  156       when 'options', 'opts'
  157         if value.include? ','
  158           value = value.delete ' ' if value.include? ' '
  159           (value.split ',').each {|opt| @attributes[%(#{opt}-option)] = '' unless opt.empty? }
  160         else
  161           @attributes[%(#{value}-option)] = '' unless value.empty?
  162         end
  163       else
  164         if single_quoted_value && @block
  165           case name
  166           when 'title', 'reftext'
  167             @attributes[name] = value
  168           else
  169             @attributes[name] = @block.apply_subs value
  170           end
  171         else
  172           @attributes[name] = value
  173         end
  174       end
  175     else
  176       resolved_name = single_quoted_value && @block ? (@block.apply_subs name) : name
  177       if (positional_attr_name = positional_attrs[index])
  178         @attributes[positional_attr_name] = resolved_name
  179       end
  180       # QUESTION should we always assign the positional key?
  181       @attributes[index + 1] = resolved_name
  182       # QUESTION should we assign the resolved name as an attribute?
  183       #@attributes[resolved_name] = nil
  184     end
  185 
  186     true
  187   end
  188 
  189   def parse_attribute_value quote
  190     # empty quoted value
  191     if @scanner.peek(1) == quote
  192       @scanner.get_byte
  193       return ''
  194     end
  195 
  196     if (value = scan_to_quote quote)
  197       @scanner.get_byte
  198       if value.include? BACKSLASH
  199         value.gsub EscapedQuotes[quote], quote
  200       else
  201         value
  202       end
  203     else
  204       %(#{quote}#{scan_to_delimiter})
  205     end
  206   end
  207 
  208   def skip_blank
  209     @scanner.skip BlankRx
  210   end
  211 
  212   def skip_delimiter
  213     @scanner.skip @delimiter_skip_pattern
  214   end
  215 
  216   def scan_name
  217     @scanner.scan NameRx
  218   end
  219 
  220   def scan_to_delimiter
  221     @scanner.scan @delimiter_boundary_pattern
  222   end
  223 
  224   def scan_to_quote quote
  225     @scanner.scan BoundaryRxs[quote]
  226   end
  227 end
  228 end