Class: Parser::Source::Buffer
- Inherits:
-
Object
- Object
- Parser::Source::Buffer
- Defined in:
- lib/parser/source/buffer.rb
Overview
A buffer with source code. Buffer contains the source code itself, associated location information (name and first line), and takes care of encoding.
A source buffer is immutable once populated.
Constant Summary
Instance Attribute Summary (collapse)
-
- (Integer) first_line
readonly
First line of the buffer, 1 by default.
-
- (String) name
readonly
Buffer name.
Class Method Summary (collapse)
-
+ (String|nil) recognize_encoding(string)
Try to recognize encoding of
string
as Ruby would, i.e. -
+ (String) reencode_string(input)
Recognize encoding of
input
and process it so it could be lexed.
Instance Method Summary (collapse)
-
- ([Integer, Integer]) decompose_position(position)
Convert a character index into the source to a
[line, column]
tuple. -
- (Buffer) initialize(name, first_line = 1)
constructor
A new instance of Buffer.
-
- (String) raw_source=(input)
Populate this buffer from a string without encoding autodetection.
-
- (Buffer) read
Populate this buffer from correspondingly named file.
-
- (String) source
Source code contained in this buffer.
-
- (String) source=(input)
Populate this buffer from a string with encoding autodetection.
-
- (String) source_line(lineno)
Extract line
lineno
from source, takingfirst_line
into account.
Constructor Details
- (Buffer) initialize(name, first_line = 1)
Returns a new instance of Buffer
102 103 104 105 106 107 108 109 |
# File 'lib/parser/source/buffer.rb', line 102 def initialize(name, first_line = 1) @name = name @source = nil @first_line = first_line @lines = nil @line_begins = nil end |
Instance Attribute Details
- (Integer) first_line (readonly)
First line of the buffer, 1 by default.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/parser/source/buffer.rb', line 24 class Buffer attr_reader :name, :first_line ## # @api private # ENCODING_RE = /\#.*coding\s*[:=]\s* ( # Special-case: there's a UTF8-MAC encoding. (utf8-mac) | # Chew the suffix; it's there for emacs compat. ([A-Za-z0-9_-]+?)(-unix|-dos|-mac) | ([A-Za-z0-9_-]+) ) /x ## # Try to recognize encoding of `string` as Ruby would, i.e. by looking for # magic encoding comment or UTF-8 BOM. `string` can be in any encoding. # # @param [String] string # @return [String|nil] encoding name, if recognized # def self.recognize_encoding(string) return if string.empty? # extract the first two lines in an efficient way string =~ /\A(.*)\n?(.*\n)?/ first_line, second_line = $1, $2 if first_line =~ /\A\xef\xbb\xbf/ # BOM return Encoding::UTF_8 elsif first_line[0, 2] == '#!' encoding_line = second_line else encoding_line = first_line end if (result = ENCODING_RE.match(encoding_line)) Encoding.find(result[2] || result[3] || result[5]) else nil end end ## # Recognize encoding of `input` and process it so it could be lexed. # # * If `input` does not contain BOM or magic encoding comment, it is # kept in the original encoding. # * If the detected encoding is binary, `input` is kept in binary. # * Otherwise, `input` is re-encoded into UTF-8 and returned as a # new string. # # This method mutates the encoding of `input`, but not its content. # # @param [String] input # @return [String] # @raise [EncodingError] # def self.reencode_string(input) original_encoding = input.encoding detected_encoding = recognize_encoding(input.force_encoding(Encoding::BINARY)) if detected_encoding.nil? input.force_encoding(original_encoding) elsif detected_encoding == Encoding::BINARY input else input. force_encoding(detected_encoding). encode(Encoding::UTF_8) end end def initialize(name, first_line = 1) @name = name @source = nil @first_line = first_line @lines = nil @line_begins = nil end ## # Populate this buffer from correspondingly named file. # # @example # Parser::Source::Buffer.new('foo/bar.rb').read # # @return [Buffer] self # @raise [ArgumentError] if already populated # def read File.open(@name, 'rb') do |io| self.source = io.read end self end ## # Source code contained in this buffer. # # @return [String] source code # @raise [RuntimeError] if buffer is not populated yet # def source if @source.nil? raise RuntimeError, 'Cannot extract source from uninitialized Source::Buffer' end @source end ## # Populate this buffer from a string with encoding autodetection. # `input` is mutated if not frozen. # # @param [String] input # @raise [ArgumentError] if already populated # @raise [EncodingError] if `input` includes invalid byte sequence for the encoding # @return [String] # def source=(input) if defined?(Encoding) input = input.dup if input.frozen? input = self.class.reencode_string(input) unless input.valid_encoding? raise EncodingError, "invalid byte sequence in #{input.encoding.name}" end end self.raw_source = input end ## # Populate this buffer from a string without encoding autodetection. # # @param [String] input # @raise [ArgumentError] if already populated # @return [String] # def raw_source=(input) if @source raise ArgumentError, 'Source::Buffer is immutable' end @source = input.gsub("\r\n", "\n").freeze end ## # Convert a character index into the source to a `[line, column]` tuple. # # @param [Integer] position # @return [[Integer, Integer]] `[line, column]` # def decompose_position(position) line_no, line_begin = line_for(position) [ @first_line + line_no, position - line_begin ] end ## # Extract line `lineno` from source, taking `first_line` into account. # # @param [Integer] lineno # @return [String] # @raise [IndexError] if `lineno` is out of bounds # def source_line(lineno) unless @lines @lines = @source.lines.to_a @lines.each { |line| line.chomp!("\n") } # If a file ends with a newline, the EOF token will appear # to be one line further than the end of file. @lines << "" end @lines.fetch(lineno - @first_line).dup end private def line_begins unless @line_begins @line_begins, index = [ [ 0, 0 ] ], 1 @source.each_char do |char| if char == "\n" @line_begins.unshift [ @line_begins.length, index ] end index += 1 end end @line_begins end def line_for(position) if line_begins.respond_to? :bsearch # Fast O(log n) variant for Ruby >=2.0. line_begins.bsearch do |line, line_begin| line_begin <= position end else # Slower O(n) variant for Ruby <2.0. line_begins.find do |line, line_begin| line_begin <= position end end end end |
- (String) name (readonly)
Buffer name. If the buffer was created from a file, the name corresponds to relative path to the file.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/parser/source/buffer.rb', line 24 class Buffer attr_reader :name, :first_line ## # @api private # ENCODING_RE = /\#.*coding\s*[:=]\s* ( # Special-case: there's a UTF8-MAC encoding. (utf8-mac) | # Chew the suffix; it's there for emacs compat. ([A-Za-z0-9_-]+?)(-unix|-dos|-mac) | ([A-Za-z0-9_-]+) ) /x ## # Try to recognize encoding of `string` as Ruby would, i.e. by looking for # magic encoding comment or UTF-8 BOM. `string` can be in any encoding. # # @param [String] string # @return [String|nil] encoding name, if recognized # def self.recognize_encoding(string) return if string.empty? # extract the first two lines in an efficient way string =~ /\A(.*)\n?(.*\n)?/ first_line, second_line = $1, $2 if first_line =~ /\A\xef\xbb\xbf/ # BOM return Encoding::UTF_8 elsif first_line[0, 2] == '#!' encoding_line = second_line else encoding_line = first_line end if (result = ENCODING_RE.match(encoding_line)) Encoding.find(result[2] || result[3] || result[5]) else nil end end ## # Recognize encoding of `input` and process it so it could be lexed. # # * If `input` does not contain BOM or magic encoding comment, it is # kept in the original encoding. # * If the detected encoding is binary, `input` is kept in binary. # * Otherwise, `input` is re-encoded into UTF-8 and returned as a # new string. # # This method mutates the encoding of `input`, but not its content. # # @param [String] input # @return [String] # @raise [EncodingError] # def self.reencode_string(input) original_encoding = input.encoding detected_encoding = recognize_encoding(input.force_encoding(Encoding::BINARY)) if detected_encoding.nil? input.force_encoding(original_encoding) elsif detected_encoding == Encoding::BINARY input else input. force_encoding(detected_encoding). encode(Encoding::UTF_8) end end def initialize(name, first_line = 1) @name = name @source = nil @first_line = first_line @lines = nil @line_begins = nil end ## # Populate this buffer from correspondingly named file. # # @example # Parser::Source::Buffer.new('foo/bar.rb').read # # @return [Buffer] self # @raise [ArgumentError] if already populated # def read File.open(@name, 'rb') do |io| self.source = io.read end self end ## # Source code contained in this buffer. # # @return [String] source code # @raise [RuntimeError] if buffer is not populated yet # def source if @source.nil? raise RuntimeError, 'Cannot extract source from uninitialized Source::Buffer' end @source end ## # Populate this buffer from a string with encoding autodetection. # `input` is mutated if not frozen. # # @param [String] input # @raise [ArgumentError] if already populated # @raise [EncodingError] if `input` includes invalid byte sequence for the encoding # @return [String] # def source=(input) if defined?(Encoding) input = input.dup if input.frozen? input = self.class.reencode_string(input) unless input.valid_encoding? raise EncodingError, "invalid byte sequence in #{input.encoding.name}" end end self.raw_source = input end ## # Populate this buffer from a string without encoding autodetection. # # @param [String] input # @raise [ArgumentError] if already populated # @return [String] # def raw_source=(input) if @source raise ArgumentError, 'Source::Buffer is immutable' end @source = input.gsub("\r\n", "\n").freeze end ## # Convert a character index into the source to a `[line, column]` tuple. # # @param [Integer] position # @return [[Integer, Integer]] `[line, column]` # def decompose_position(position) line_no, line_begin = line_for(position) [ @first_line + line_no, position - line_begin ] end ## # Extract line `lineno` from source, taking `first_line` into account. # # @param [Integer] lineno # @return [String] # @raise [IndexError] if `lineno` is out of bounds # def source_line(lineno) unless @lines @lines = @source.lines.to_a @lines.each { |line| line.chomp!("\n") } # If a file ends with a newline, the EOF token will appear # to be one line further than the end of file. @lines << "" end @lines.fetch(lineno - @first_line).dup end private def line_begins unless @line_begins @line_begins, index = [ [ 0, 0 ] ], 1 @source.each_char do |char| if char == "\n" @line_begins.unshift [ @line_begins.length, index ] end index += 1 end end @line_begins end def line_for(position) if line_begins.respond_to? :bsearch # Fast O(log n) variant for Ruby >=2.0. line_begins.bsearch do |line, line_begin| line_begin <= position end else # Slower O(n) variant for Ruby <2.0. line_begins.find do |line, line_begin| line_begin <= position end end end end |
Class Method Details
+ (String|nil) recognize_encoding(string)
Try to recognize encoding of string
as Ruby would, i.e. by looking for
magic encoding comment or UTF-8 BOM. string
can be in any encoding.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/parser/source/buffer.rb', line 50 def self.recognize_encoding(string) return if string.empty? # extract the first two lines in an efficient way string =~ /\A(.*)\n?(.*\n)?/ first_line, second_line = $1, $2 if first_line =~ /\A\xef\xbb\xbf/ # BOM return Encoding::UTF_8 elsif first_line[0, 2] == '#!' encoding_line = second_line else encoding_line = first_line end if (result = ENCODING_RE.match(encoding_line)) Encoding.find(result[2] || result[3] || result[5]) else nil end end |
+ (String) reencode_string(input)
Recognize encoding of input
and process it so it could be lexed.
- If
input
does not contain BOM or magic encoding comment, it is kept in the original encoding. - If the detected encoding is binary,
input
is kept in binary. - Otherwise,
input
is re-encoded into UTF-8 and returned as a new string.
This method mutates the encoding of input
, but not its content.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/parser/source/buffer.rb', line 87 def self.reencode_string(input) original_encoding = input.encoding detected_encoding = recognize_encoding(input.force_encoding(Encoding::BINARY)) if detected_encoding.nil? input.force_encoding(original_encoding) elsif detected_encoding == Encoding::BINARY input else input. force_encoding(detected_encoding). encode(Encoding::UTF_8) end end |
Instance Method Details
- ([Integer, Integer]) decompose_position(position)
Convert a character index into the source to a [line, column]
tuple.
185 186 187 188 189 |
# File 'lib/parser/source/buffer.rb', line 185 def decompose_position(position) line_no, line_begin = line_for(position) [ @first_line + line_no, position - line_begin ] end |
- (String) raw_source=(input)
Populate this buffer from a string without encoding autodetection.
171 172 173 174 175 176 177 |
# File 'lib/parser/source/buffer.rb', line 171 def raw_source=(input) if @source raise ArgumentError, 'Source::Buffer is immutable' end @source = input.gsub("\r\n", "\n").freeze end |
- (Buffer) read
Populate this buffer from correspondingly named file.
120 121 122 123 124 125 126 |
# File 'lib/parser/source/buffer.rb', line 120 def read File.open(@name, 'rb') do |io| self.source = io.read end self end |
- (String) source
Source code contained in this buffer.
134 135 136 137 138 139 140 |
# File 'lib/parser/source/buffer.rb', line 134 def source if @source.nil? raise RuntimeError, 'Cannot extract source from uninitialized Source::Buffer' end @source end |
- (String) source=(input)
Populate this buffer from a string with encoding autodetection.
input
is mutated if not frozen.
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/parser/source/buffer.rb', line 151 def source=(input) if defined?(Encoding) input = input.dup if input.frozen? input = self.class.reencode_string(input) unless input.valid_encoding? raise EncodingError, "invalid byte sequence in #{input.encoding.name}" end end self.raw_source = input end |
- (String) source_line(lineno)
Extract line lineno
from source, taking first_line
into account.
198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/parser/source/buffer.rb', line 198 def source_line(lineno) unless @lines @lines = @source.lines.to_a @lines.each { |line| line.chomp!("\n") } # If a file ends with a newline, the EOF token will appear # to be one line further than the end of file. @lines << "" end @lines.fetch(lineno - @first_line).dup end |