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