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