Class: HeadMusic::Notation::LilyPond::SourceScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/notation/lily_pond/source_scanner.rb

Overview

A StringScanner over a LilyPond source that knows where it is.

The line and column of the scan position stay true across every consumed match -- block comments, strings, and whitespace runs all span newlines -- so a token can be stamped with where it began and an error can say where it happened. Comments are skipped here rather than pre-stripped, so a % inside a string survives and the line numbers stay true.

Constant Summary collapse

BLOCK_COMMENT_PATTERN =
/%\{.*?%\}/m
LINE_COMMENT_PATTERN =
/%[^\n]*/
SNIPPET_LENGTH =
20

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ SourceScanner

Returns a new instance of SourceScanner.



21
22
23
24
25
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 21

def initialize(source)
  @scanner = StringScanner.new(normalize(source))
  @line = 1
  @line_start = 0
end

Instance Attribute Details

#line ⇒ Object (readonly)

Returns the value of attribute line.



17
18
19
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 17

def line
  @line
end

#scanner ⇒ Object (readonly, private)

Returns the value of attribute scanner.



80
81
82
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 80

def scanner
  @scanner
end

Instance Method Details

#[](group) ⇒ Object



31
32
33
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 31

def [](group)
  scanner[group]
end

#captures ⇒ Object

StringScanner#captures answers "" for a group that took no part in the match, where #[] answers nil, and nil is what a caller can test.



37
38
39
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 37

def captures
  (1...scanner.size).map { |group| scanner[group] }
end

#column ⇒ Object



27
28
29
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 27

def column
  scanner.charpos - @line_start + 1
end

#consume(pattern) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 41

def consume(pattern)
  start = scanner.charpos
  text = scanner.scan(pattern)
  return unless text

  newlines = text.count("\n")
  if newlines.positive?
    @line += newlines
    @line_start = start + text.rindex("\n") + 1
  end
  text
end

#error(message) ⇒ Object



66
67
68
69
70
71
72
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 66

def error(message)
  ParseError.new(
    message,
    line_number: line, column: column,
    snippet: scanner.peek(SNIPPET_LENGTH).lines.first&.chomp
  )
end

#normalize(source) ⇒ Object (private)

Raises:



82
83
84
85
86
87
88
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 82

def normalize(source)
  text = source.to_s
  text = text.dup.force_encoding(Encoding::UTF_8) unless text.encoding == Encoding::UTF_8
  raise ParseError, "LilyPond input is not valid UTF-8" unless text.valid_encoding?

  text.delete_prefix("\uFEFF")
end

#skip_block_comment ⇒ Object



59
60
61
62
63
64
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 59

def skip_block_comment
  return consume(BLOCK_COMMENT_PATTERN) if match?(BLOCK_COMMENT_PATTERN)
  return false unless match?(/%\{/)

  raise error("Unterminated block comment")
end

#skip_insignificant ⇒ Object

Whitespace and comments: what the language ignores between tokens.



55
56
57
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 55

def skip_insignificant
  consume(/\s+/) || skip_block_comment || consume(LINE_COMMENT_PATTERN)
end

#unexpected_character_error ⇒ Object



74
75
76
# File 'lib/head_music/notation/lily_pond/source_scanner.rb', line 74

def unexpected_character_error
  error("Unexpected character #{peek(1).inspect} at column #{column}")
end