module Puppet::Pops::Parser::HeredocSupport

Constants

PATTERN_HEREDOC

Pattern for heredoc `@(endtag[/escapes]) Produces groups for endtag (group 1), syntax (group 2), and escapes (group 3)

Public Instance Methods

heredoc() click to toggle source
    # File lib/puppet/pops/parser/heredoc_support.rb
 13 def heredoc
 14   scn = @scanner
 15   ctx = @lexing_context
 16   locator = @locator
 17   before = scn.pos
 18 
 19   # scanner is at position before @(
 20   # find end of the heredoc spec
 21   str = scn.scan_until(/\)/) || lex_error(Issues::HEREDOC_UNCLOSED_PARENTHESIS, :followed_by => followed_by)
 22   pos_after_heredoc = scn.pos
 23   # Note: allows '+' as separator in syntax, but this needs validation as empty segments are not allowed
 24   md = str.match(PATTERN_HEREDOC)
 25   lex_error(Issues::HEREDOC_INVALID_SYNTAX) unless md
 26   endtag = md[1]
 27   syntax = md[2] || ''
 28   escapes = md[3]
 29 
 30   endtag.strip!
 31 
 32   # Is this a dq string style heredoc? (endtag enclosed in "")
 33   if endtag =~ /^"(.*)"$/
 34     dqstring_style = true
 35     endtag = $1.strip
 36   end
 37 
 38   lex_error(Issues::HEREDOC_EMPTY_ENDTAG) unless endtag.length >= 1
 39 
 40   resulting_escapes = []
 41   if escapes
 42     escapes = "trnsuL$" if escapes.length < 1
 43 
 44     escapes = escapes.split('')
 45     unless escapes.length == escapes.uniq.length
 46       lex_error(Issues::HEREDOC_MULTIPLE_AT_ESCAPES, :escapes => escapes)
 47     end
 48     resulting_escapes = ["\\"]
 49     escapes.each do |e|
 50       case e
 51       when "t", "r", "n", "s", "u", "$"
 52         resulting_escapes << e
 53       when "L"
 54         resulting_escapes += ["\n", "\r\n"]
 55       else
 56         lex_error(Issues::HEREDOC_INVALID_ESCAPE, :actual => e)
 57       end
 58     end
 59   end
 60 
 61   # Produce a heredoc token to make the syntax available to the grammar
 62   enqueue_completed([:HEREDOC, syntax, pos_after_heredoc - before], before)
 63 
 64   # If this is the second or subsequent heredoc on the line, the lexing context's :newline_jump contains
 65   # the position after the \n where the next heredoc text should scan. If not set, this is the first
 66   # and it should start scanning after the first found \n (or if not found == error).
 67 
 68   if ctx[:newline_jump]
 69     scn.pos = ctx[:newline_jump]
 70   else
 71     scn.scan_until(/\n/) || lex_error(Issues::HEREDOC_WITHOUT_TEXT)
 72   end
 73   # offset 0 for the heredoc, and its line number
 74   heredoc_offset = scn.pos
 75   heredoc_line = locator.line_for_offset(heredoc_offset)-1
 76 
 77   # Compute message to emit if there is no end (to make it refer to the opening heredoc position).
 78   eof_error = create_lex_error(Issues::HEREDOC_WITHOUT_END_TAGGED_LINE)
 79 
 80   # Text from this position (+ lexing contexts offset for any preceding heredoc) is heredoc until a line
 81   # that terminates the heredoc is found.
 82 
 83   # (Endline in EBNF form): WS* ('|' WS*)? ('-' WS*)? endtag WS* \r? (\n|$)
 84   endline_pattern = /([[:blank:]]*)(?:([|])[[:blank:]]*)?(?:(\-)[[:blank:]]*)?#{Regexp.escape(endtag)}[[:blank:]]*\r?(?:\n|\z)/
 85   lines = []
 86   while !scn.eos? do
 87     one_line = scn.scan_until(/(?:\n|\z)/)
 88     raise eof_error unless one_line
 89     md = one_line.match(endline_pattern)
 90     if md
 91       leading      = md[1]
 92       has_margin   = md[2] == '|'
 93       remove_break = md[3] == '-'
 94       # Record position where next heredoc (from same line as current @()) should start scanning for content
 95       ctx[:newline_jump] = scn.pos
 96 
 97 
 98       # Process captured lines - remove leading, and trailing newline
 99       # get processed string and index of removed margin/leading size per line
100       str, margin_per_line = heredoc_text(lines, leading, has_margin, remove_break)
101 
102       # Use a new lexer instance configured with a sub-locator to enable correct positioning
103       sublexer = self.class.new()
104       locator = Locator::SubLocator.new(locator, str, heredoc_line, heredoc_offset, has_margin, margin_per_line)
105 
106       # Emit a token that provides the grammar with location information about the lines on which the heredoc
107       # content is based.
108       enqueue([:SUBLOCATE,
109         LexerSupport::TokenValue.new([:SUBLOCATE,
110           lines, lines.reduce(0) {|size, s| size + s.length} ],
111           heredoc_offset,
112           locator)])
113 
114       sublexer.lex_unquoted_string(str, locator, resulting_escapes, dqstring_style)
115       sublexer.interpolate_uq_to(self)
116       # Continue scan after @(...)
117       scn.pos = pos_after_heredoc
118       return
119     else
120       lines << one_line
121     end
122   end
123   raise eof_error
124 end
heredoc_text(lines, leading, has_margin, remove_break) click to toggle source

Produces the heredoc text string given the individual (unprocessed) lines as an array and array with margin sizes per line @param lines [Array<String>] unprocessed lines of text in the heredoc w/o terminating line @param leading [String] the leading text up (up to pipe or other terminating char) @param has_margin [Boolean] if the left margin should be adjusted as indicated by `leading` @param remove_break [Boolean] if the line break (r?n) at the end of the last line should be removed or not @return [Array] - a tuple with resulting string, and an array with margin size per line

    # File lib/puppet/pops/parser/heredoc_support.rb
133 def heredoc_text(lines, leading, has_margin, remove_break)
134   if has_margin && leading.length > 0
135     leading_pattern = /^#{Regexp.escape(leading)}/
136     # TODO: This implementation is not according to the specification, but is kept to be bug compatible.
137     # The specification says that leading space up to the margin marker should be removed, but this implementation
138     # simply leaves lines that have text in the margin untouched.
139     #
140     processed_lines = lines.collect {|s| s.gsub(leading_pattern, '') }
141     margin_per_line = Array.new(processed_lines.length) {|x| lines[x].length - processed_lines[x].length }
142     lines = processed_lines
143   else
144     # Array with a 0 per line
145     margin_per_line = Array.new(lines.length, 0)
146   end
147   result = lines.join('')
148   result.gsub!(/\r?\n\z/m, '') if remove_break
149   [result, margin_per_line]
150 end