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