diff options
Diffstat (limited to 'lib/coderay/rouge_scanner_dsl.rb')
-rw-r--r-- | lib/coderay/rouge_scanner_dsl.rb | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/lib/coderay/rouge_scanner_dsl.rb b/lib/coderay/rouge_scanner_dsl.rb new file mode 100644 index 0000000..38b06f5 --- /dev/null +++ b/lib/coderay/rouge_scanner_dsl.rb @@ -0,0 +1,199 @@ +require 'set' + +module CodeRay + module Scanners + module RougeScannerDSL + NoStatesError = Class.new StandardError + + State = Struct.new :name, :rules do + def initialize(name, &block) + super name, [] + + instance_eval(&block) + end + + def code scanner + <<-RUBY +when #{name.inspect} +#{ rules_code(scanner).chomp.gsub(/^/, ' ') } + else + encoder.text_token getch, :error + end + RUBY + end + + def rules_code scanner, first: true + raise 'no rules defined for %p' % [self] if rules.empty? + + [ + rules.first.code(scanner, first: first), + *rules.drop(1).map { |rule| rule.code(scanner) } + ].join + end + + protected + + # DSL + + def rule pattern, token = nil, next_state = nil, &block + unless token || block + raise 'please pass `rule` a token to yield or a callback' + end + + case token + when Class + unless token < Rouge::Token + raise "invalid token: #{token.inspect}" + end + + case next_state + when Symbol + rules << Rule.new(pattern, token, next_state) + when nil + rules << Rule.new(pattern, token) + else + raise "invalid next state: #{next_state.inspect}" + end + when nil + rules << CallbackRule.new(pattern, block) + else + raise "invalid token: #{token.inspect}" + end + end + + def mixin state_name + rules << Mixin.new(state_name) + end + end + + Rule = Struct.new :pattern, :token, :action do + def initialize(pattern, token, action = nil) + super + end + + def code scanner, first: false + <<-RUBY + action_code.to_s +#{'els' unless first}if match = scan(#{pattern.inspect}) + encoder.text_token match, #{token.token_chain.map(&:name).join('::')} + RUBY + end + + def action_code + case action + when :pop! + <<-RUBY + states.pop + state = states.last + RUBY + when Symbol + <<-RUBY + state = #{action.inspect} + states << state + RUBY + end + end + end + + CallbackRule = Struct.new :pattern, :callback do + def code scanner, first: false + <<-RUBY +#{'els' unless first}if match = scan(#{pattern.inspect}) + @match = match + #{scanner.add_callback(callback)} + RUBY + end + end + + Mixin = Struct.new(:state_name) do + def code scanner, first: false + scanner.states[state_name].rules_code(scanner, first: first) + end + end + + attr_accessor :states + + def state name, &block + @states ||= {} + @states[name] = State.new(name, &block) + end + + def add_callback block + base_name = "__callback_line_#{block.source_location.last}" + callback_name = base_name + counter = 'a' + while callbacks.key?(callback_name) + callback_name = "#{base_name}_#{counter}" + counter = counter.succ + end + + callbacks[callback_name] = define_method(callback_name, &block) + + parameters = block.parameters + + if parameters.empty? + callback_name + else + parameter_names = parameters.map do |type, name| + raise "callbacks don't allow rest parameters: %p" % [parameters] unless type == :req || type == :opt + name = :match if name == :m + name + end + + parameter_names.each { |name| variables << name } + "#{callback_name}(#{parameter_names.join(', ')})" + end + end + + def add_variable name + variables << name + end + + protected + + def callbacks + @callbacks ||= {} + end + + def variables + @variables ||= Set.new + end + + def additional_variables + variables - %i(encoder options state states match kind) + end + + def scan_tokens_code + <<-"RUBY" +state = options[:state] || @state +states = [state] +#{ restore_local_variables_code } +until eos? + case state +#{ states_code.chomp.gsub(/^/, ' ') } + else + raise_inspect 'Unknown state: %p' % [state], encoder + end +end + +@state = state if options[:keep_state] + +close_groups(encoder, states) + +encoder + RUBY + end + + def restore_local_variables_code + additional_variables.sort.map { |name| "#{name} = @#{name}" }.join("\n") + end + + def states_code + unless defined?(@states) && !@states.empty? + raise NoStatesError, 'no states defined for %p' % [self.class] + end + + @states.values.map { |state| state.code(self) }.join + end + end + end +end
\ No newline at end of file |