lib/wlang/parser.rb in wlang-0.8.5 vs lib/wlang/parser.rb in wlang-0.9.1
- old
+ new
@@ -2,99 +2,155 @@
require 'wlang/rule'
require 'wlang/rule_set'
require 'wlang/errors'
require 'wlang/template'
module WLang
-
#
# Parser for wlang templates.
#
# This class implements the parsing algorithm of wlang, recognizing special tags
# and replacing them using installed rules. Instanciating a template is done
# using instantiate. All other methods (parse, parse_block, has_block?) and the
# like are callbacks for rules and should not be used by users themselve.
#
- # Obtaining a parser MUST be made through Parser.instantiator (new is private).
- #
# == Detailed API
class Parser
- # Factors a parser instance for a given template and an output buffer.
- def self.instantiator(template, buffer=nil)
- Parser.send(:new, nil, template, nil, 0, buffer)
+ # Initializes a parser instance.
+ def initialize(hosted, template, scope)
+ raise(ArgumentError, "Hosted language is mandatory (a ::WLang::HostedLanguage)") unless ::WLang::HostedLanguage===hosted
+ raise(ArgumentError, "Template is mandatory (a ::WLang::Template)") unless ::WLang::Template===template
+ raise(ArgumentError, "Scope is mandatory (a Hash)") unless ::Hash===scope
+ @state = ::WLang::Parser::State.new(self).branch(
+ :hosted => hosted,
+ :template => template,
+ :dialect => template.dialect,
+ :offset => 0,
+ :shared => :none,
+ :scope => scope,
+ :buffer => template.dialect.factor_buffer)
end
-
- # Current parsed template
- attr_reader :template
-
- # Current execution context
- attr_reader :context
-
- # Current buffer
- attr_reader :buffer
-
- #
- # Initializes a parser instance. _parent_ is the Parser instance of the higher
- # parsing stage. _template_ is the current instantiated template, _offset_ is
- # where the parsing must start in the template and _buffer_ is the output buffer
- # where the instantiation result must be pushed.
- #
- def initialize(parent, template, dialect, offset, buffer)
- raise(ArgumentError, "Template is mandatory") unless WLang::Template===template
- raise(ArgumentError, "Offset is mandatory") unless Integer===offset
- dialect = template.dialect if dialect.nil?
- buffer = dialect.factor_buffer if buffer.nil?
- raise(ArgumentError, "Buffer is mandatory") unless buffer.respond_to?(:<<)
- @parent = parent
- @template = template
- @context = template.context
- @offset = offset
- @dialect = dialect
- @buffer = buffer
+
+ ###################################################################### Facade on the parser state
+
+ # Returns the current parser state
+ def state(); @state; end
+
+ # Returns the current template
+ def template() state.template; end
+
+ # Returns the current buffer
+ def dialect() state.dialect; end
+
+ # Returns the current template's source text
+ def source_text() state.template.source_text; end
+
+ # Returns the current offset
+ def offset() state.offset; end
+
+ # Sets the current offset of the parser
+ def offset=(offset) state.offset = offset; end
+
+ # Returns the current buffer
+ def buffer() state.buffer; end
+
+ # Returns the current hosted language
+ def hosted() state.hosted; end
+
+ # Branches the current parser
+ def branch(opts = {})
+ raise ArgumentError, "Parser branching requires a block" unless block_given?
+ @state = @state.branch(opts)
+ result = yield(@state)
+ @state = @state.parent
+ result
end
- # Factors a specific buffer on the current dialect
- def factor_buffer
- @dialect.factor_buffer
+ ###################################################################### Facade on the file system
+
+ # Resolves an URI throught the current template
+ def file_resolve(uri)
+ # TODO: refactor me to handle absolute URIs
+ template.file_resolve(uri)
end
- # Appends on a given buffer
- def append_buffer(buffer, str, block)
- if buffer.respond_to?(:wlang_append)
- buffer.wlang_append(str, block)
- else
- buffer << str
+ ###################################################################### Facade on wlang itself
+
+ # Factors a template instance for a given file
+ def file_template(file, dialect = nil, block_symbols = :braces)
+ WLang::file_template(file, dialect, block_symbols)
+ end
+
+ # Finds a real dialect instance from an argument (Dialect instance or
+ # qualified name)
+ def ensure_dialect(dialect)
+ if String===dialect
+ dname, dialect = dialect, WLang::dialect(dialect)
+ raise(ParseError,"Unknown modulation dialect: #{dname}") if dialect.nil?
+ elsif not(Dialect===dialect)
+ raise(ParseError,"Unknown modulation dialect: #{dialect}")
+ end
+ dialect
+ end
+
+ # Finds a real ecoder instance from an argument (Encoder instance or
+ # qualified or unqualified name)
+ def ensure_encoder(encoder)
+ if String===encoder
+ if encoder.include?("/")
+ ename, encoder = encoder, WLang::encoder(encoder)
+ raise(ParseError,"Unknown encoder: #{ename}") if encoder.nil?
+ else
+ ename, encoder = encoder, self.dialect.find_encoder(encoder)
+ raise(ParseError,"Unknown encoder: #{ename}") if encoder.nil?
+ end
+ elsif not(Encoder===encoder)
+ raise(ParseError,"Unknown encoder: #{encoder}")
end
+ encoder
end
- # Pushes a given string on the output buffer
- def <<(str, block)
- append_buffer(@buffer, str, block)
+ ###################################################################### Main parser methods
+
+ # Checks the result of a given rule
+ def launch_rule(dialect, rule_symbol, rule, offset)
+ result = rule.start_tag(self, offset)
+ raise WLang::Error, "Bad rule implementation #{dialect.qualified_name} #{rule_symbol}{}\n#{result.inspect}"\
+ unless result.size == 2 and String===result[0] and Integer===result[1]
+ result
end
-
- # Parses the text
+
+ # Parses the template's text and instantiate it
def instantiate
- # Main variables:
- # - offset: matching current position
- # - rules: handlers of '{' currently opened
- offset, pattern, rules = @offset, @dialect.pattern(@template.block_symbols), []
- @source_text = template.source_text
+ # Main variables put in local scope for efficiency:
+ # - template: current parsed template
+ # - source_text: current template's source text
+ # - offset: matching current position
+ # - pattern: current dialect's regexp pattern
+ # - rules: handlers of '{' currently opened
+ template = self.template
+ symbols = self.template.block_symbols
+ source_text = self.source_text
+ dialect = self.dialect
+ buffer = self.buffer
+ pattern = dialect.pattern(template.block_symbols)
+ rules = []
# we start matching everything in the ruleset
- while match_at=@source_text.index(pattern,offset)
+ while match_at=source_text.index(pattern, self.offset)
match, match_length = $~[0], $~[0].length
# puts pre_match (we can't use $~.pre_match !)
- self.<<(@source_text[offset, match_at-offset], false) if match_at>0
+ self.<<(source_text[self.offset, match_at-self.offset], false) if match_at>0
- if @source_text[match_at,1]=='\\' # escaping sequence
+ if source_text[match_at,1]=='\\' # escaping sequence
self.<<(match[1..-1], false)
- offset = match_at + match_length
+ self.offset = match_at + match_length
elsif match.length==1 # simple '{' or '}' here
- offset = match_at + match_length
- if match==Template::BLOCK_SYMBOLS[template.block_symbols][0]
+ self.offset = match_at + match_length
+ if match==Template::BLOCK_SYMBOLS[symbols][0]
self.<<(match, false) # simple '{' are always pushed
# we push '{' in rules to recognize it's associated '}'
# that must be pushed on buffer also
rules << match
else
@@ -102,71 +158,61 @@
break if rules.empty?
# otherwise, push '}' only if associated to a simple '{'
self.<<(match, false) unless Rule===rules.pop
end
- elsif match[-1,1]==Template::BLOCK_SYMBOLS[template.block_symbols][0] # opening special tag
+ elsif match[-1,1]==Template::BLOCK_SYMBOLS[symbols][0] # opening special tag
# following line should never return nil as the matching pattern comes
# from the ruleset itself!
- rule = @dialect.ruleset[match[0..-2]]
+ rule_symbol = match[0..-2]
+ rule = dialect.ruleset[rule_symbol]
rules << rule
+ # Just added to get the last position in case of an error
+ self.offset = match_at + match_length
+
# lauch that rule, get it's replacement and my new offset
- replacement, offset = rule.start_tag(self, match_at + match_length)
- replacement = "" if replacement.nil?
- raise "Bad implementation of rule #{match[0..-2]}" if offset.nil?
-
+ replacement, self.offset = launch_rule(dialect, rule_symbol, rule, self.offset)
+
# push replacement
self.<<(replacement, true) unless replacement.empty?
end
end # while match_at=...
- # trailing data (end of @template reached only if no match_at)
+ # trailing data (end of template reached only if no match_at)
unless match_at
- unexpected_eof(@source_text.length, '}') unless rules.empty?
- self.<<(@source_text[offset, 1+@source_text.length-offset], false)
- offset = @source_text.length
+ unexpected_eof(source_text.length, '}') unless rules.empty?
+ self.<<(source_text[self.offset, 1+source_text.length-self.offset], false)
+ self.offset = source_text.length
end
- [@buffer, offset-1]
+ [buffer, self.offset-1]
end
- #
- # Evaluates a ruby expression on the current context.
- # See WLang::Parser::Context#evaluate.
- #
- def evaluate(expression)
- @context.evaluate(expression)
- rescue Exception => ex
- raise ::WLang::EvalError, "#{template.where(@offset)} evaluation of '#{expression}' failed", ex.backtrace
- end
+ ###################################################################### Callbacks for rule sets
#
# Launches a child parser for instantiation at a given _offset_ in given
# _dialect_ (same dialect than self if dialect is nil) and with an output
# _buffer_.
#
def parse(offset, dialect=nil, buffer=nil)
- if dialect.nil?
- dialect = @dialect
- elsif String===dialect
- dname, dialect = dialect, WLang::dialect(dialect)
- raise(ParseError,"Unknown modulation dialect: #{dname}") if dialect.nil?
- elsif not(Dialect===dialect)
- raise(ParseError,"Unknown modulation dialect: #{dialect}")
+ dialect = ensure_dialect(dialect.nil? ? self.dialect : dialect)
+ buffer = dialect.factor_buffer if buffer.nil?
+ branch(:offset => offset, :dialect => dialect, :buffer => buffer) do
+ instantiate
end
- Parser.send(:new, self, @template, dialect, offset, buffer).instantiate
end
#
# Checks if a given offset is a starting block. For easy implementation of rules
# the check applied here is that text starting at _offset_ in the template is precisely
# '}{' (the reason for that is that instantiate, parse, parse_block always stop
# parsing on a '}')
#
def has_block?(offset)
- @source_text[offset,2]=='}{'
+ self.source_text[offset,2]=='}{'
end
#
# Parses a given block starting at a given _offset_, expressed in a given
# _dialect_ and using an output _buffer_. This method raises a ParseError if
@@ -177,32 +223,71 @@
def parse_block(offset, dialect=nil, buffer=nil)
block_missing_error(offset+2) unless has_block?(offset)
parse(offset+2, dialect, buffer)
end
+ ###################################################################### Facade on the buffer
+
+ # Appends on a given buffer
+ def append_buffer(buffer, str, block)
+ if buffer.respond_to?(:wlang_append)
+ buffer.wlang_append(str, block)
+ else
+ buffer << str
+ end
+ end
+
+ # Pushes a given string on the output buffer
+ def <<(str, block)
+ append_buffer(buffer, str, block)
+ end
+
+ ###################################################################### Facade on the scope
+
+ # Yields the block in a new scope branch, pushing pairing values on it.
+ # Original scope is restored after that. Returns what the yielded block
+ # returned.
+ def branch_scope(pairing = {}, which = :all)
+ raise ArgumentError, "Parser.branch_scope expects a block" unless block_given?
+ branch(:scope => pairing, :shared => which) { yield }
+ end
+
+ # Adds a key/value pair on the current scope.
+ def scope_define(key, value)
+ state.scope[key] = value
+ end
+
+ ###################################################################### Facade on the hosted language
+
#
+ # Evaluates a ruby expression on the current context.
+ # See WLang::Parser::Context#evaluate.
+ #
+ def evaluate(expression)
+ hosted.evaluate(expression, state)
+ end
+
+ ###################################################################### Facade on the dialect
+
+ # Factors a specific buffer on the current dialect
+ def factor_buffer
+ self.dialect.factor_buffer
+ end
+
+ #
# Encodes a given text using an encoder, that may be a qualified name or an
# Encoder instance.
#
def encode(src, encoder, options=nil)
options = {} unless options
options['_encoder_'] = encoder
options['_template_'] = template
- if String===encoder
- if encoder.include?("/")
- ename, encoder = encoder, WLang::encoder(encoder)
- raise(ParseError,"Unknown encoder: #{ename}") if encoder.nil?
- else
- ename, encoder = encoder, @dialect.find_encoder(encoder)
- raise(ParseError,"Unknown encoder: #{ename}") if encoder.nil?
- end
- elsif not(Encoder===encoder)
- raise(ParseError,"Unknown encoder: #{encoder}")
- end
- encoder.encode(src, options)
+ ensure_encoder(encoder).encode(src, options)
end
+ ###################################################################### About errors
+
# Raises an exception with a friendly message
def error(offset, message)
template.error(offset, message)
end
@@ -227,33 +312,11 @@
# specif. the expected character when EOF found
#
def unexpected_eof(offset, expected)
template.parse_error(offset, "#{expected} expected, EOF found")
end
-
- #
- # Puts a key/value pair in the current context. See Parser::Context::define
- # for details.
- #
- def context_define(key, value)
- @context.define(key,value)
- end
-
- #
- # Pushes a new scope on the current context stack. See Parser::Context::push
- # for details.
- #
- def context_push(context)
- @context.push(context)
- end
-
- #
- # Pops the top scope of the context stack. See Parser::Context::pop for details.
- #
- def context_pop
- @context.pop
- end
-
- private_class_method :new
+
+ # Protected methods are...
+ protected :hosted, :offset, :source_text, :buffer, :dialect
+
end # class Parser
-
end # module WLang