class Glyph::Parser
The Glyph::Parser
class can parse a string of text containing Glyph
macros and produce the corresponding syntax tree. @since 0.3.0
Public Class Methods
new(text, source_name="--")
click to toggle source
Initializes the parser. @param [String] text the text to parse @param [String] source_name the name of the source file (stored in the root node) @since 0.3.0
# File lib/glyph/parser.rb, line 16 def initialize(text, source_name="--") @ruby19 = RUBY_VERSION >= '1.9' ? true : false @source_name = source_name || "--" @input = StringScanner.new text @output = create_node DocumentNode, :name => @source_name.to_sym @current_macro = nil @current_attribute = nil end
Public Instance Methods
parse()
click to toggle source
Parses the string of text provided during initialization @return [Glyph::SyntaxNode] the Abstract Syntax Tree corresponding to the string @since 0.3.0
# File lib/glyph/parser.rb, line 28 def parse count = 0 while result = parse_contents(@output) do @output << result count +=1 end if @input.pos < @input.string.bytesize then current_char = @input.string[@input.pos].chr rescue nil illegal_delimiter = current_char.match(/\]|\[/) rescue nil error "Macro delimiter '#{current_char}' not escaped" if illegal_delimiter end @output end
Protected Instance Methods
attribute(current)
click to toggle source
# File lib/glyph/parser.rb, line 116 def attribute(current) if @input.scan(/@[^:\[\]\|\\\s]+\[/) then error "Attributes cannot be nested" if current.is_a?(AttributeNode) name = @input.matched[1..@input.matched.bytesize-2] node = create_node(AttributeNode, { :escape => false, :name => name.to_sym }) while contents = parse_contents(node) do node << contents end current[:attributes] << node @input.scan(/\]/) or error "Attribute @#{name} not closed" node else nil end end
escape_sequence(current)
click to toggle source
# File lib/glyph/parser.rb, line 188 def escape_sequence(current) if @input.scan(/\\./) then create_node EscapeNode, :value => @input.matched, :escaped => true end end
escaped_text(current)
click to toggle source
# File lib/glyph/parser.rb, line 150 def escaped_text(current) start_p = @input.pos res = @input.scan_until /(\\.)|(\A(\=\]|\|)|[^\\](\=\]|\|)|\Z)/ case when @input.matched.match(/^[^\\]\=\]$/) then offset = 2 when @input.matched.match(/^[^\\]\|$/) then offset = 1 else offset = @input.matched.bytesize end @input.pos = @input.pos - offset rescue @input.pos return nil if @input.pos == start_p match = extract_string(start_p..@input.pos-1) illegal_nesting = match.match(/([^\[\]\|\\\s]+)\[\=/)[1] rescue nil if illegal_nesting then error "Cannot nest escaping macro '#{illegal_nesting}' within escaping macro '#{current[:name]}'" end if match.bytesize > 0 then create_node TextNode, :value => match, :escaped => true else nil end end
escaping_attribute(current)
click to toggle source
# File lib/glyph/parser.rb, line 78 def escaping_attribute(current) if @input.scan(/@[^:\[\]\|\\\s]+\[\=/) then error "Attributes cannot be nested" if @current_attribute name = @input.matched[1..@input.matched.bytesize-3] node = create_node(AttributeNode, { :escape => true, :name => name.to_sym }) while contents = parse_escaped_contents(node) do node << contents end current[:attributes] << node @input.scan(/\=\]/) or error "Attribute @#{name} not closed" node else nil end end
escaping_macro(current)
click to toggle source
# File lib/glyph/parser.rb, line 58 def escaping_macro(current) if @input.scan(/[^\[\]\|\\\s]+\[\=/) then name = @input.matched name.chop! name.chop! error "#{name}[=...=] - A macro cannot start with a digit or contain '@'" if (name.match(/^[0-1]/) || name.match(/@/)) && !name.match(/^@:?$/) node = macro_node_for name, true leaf = node node.descend { |n, level| leaf = n } while contents = parse_escaped_contents(leaf) do leaf << contents unless contents.is_a?(AttributeNode) end @input.scan(/\=\]/) or error "Escaping macro '#{name}' not closed" organize_children_for leaf node else nil end end
macro(current)
click to toggle source
# File lib/glyph/parser.rb, line 97 def macro(current) if @input.scan(/[^\[\]\|\\\s]+\[/) then name = @input.matched name.chop! error "#{name}[...] - A macro cannot start with a digit or contain '@'" if (name.match(/^[0-1]/) || name.match(/@/)) && !name.match(/^@:?$/) node = macro_node_for name leaf = node node.descend { |n, level| leaf = n } while contents = parse_contents(leaf) do leaf << contents unless contents.is_a?(AttributeNode) end @input.scan(/\]/) or error "Macro '#{name}' not closed" organize_children_for leaf node else nil end end
parameter_delimiter(current)
click to toggle source
# File lib/glyph/parser.rb, line 175 def parameter_delimiter(current) if @input.scan(/\|/) then # Parameters are not allowed outside macros or inside attributes if current.is_a?(DocumentNode) || current.is_a?(AttributeNode) then @input.pos = @input.pos-1 error "Parameter delimiter '|' not allowed here" end create_node SyntaxNode, :parameter => true else nil end end
parse_contents(current)
click to toggle source
# File lib/glyph/parser.rb, line 44 def parse_contents(current) escape_sequence(current) || parameter_delimiter(current) || escaping_attribute(current) || escaping_macro(current) || attribute(current) || macro(current) || text(current) end
parse_escaped_contents(current)
click to toggle source
# File lib/glyph/parser.rb, line 54 def parse_escaped_contents(current) escape_sequence(current) || parameter_delimiter(current) || escaped_text(current) end
text(current)
click to toggle source
# File lib/glyph/parser.rb, line 135 def text(current) start_p = @input.pos res = @input.scan_until /(\\.)|(\A(\]|\|)|[^\\](\]|\|)|[^\[\]\|\\\s]+\[|\Z)/ offset = @input.matched.match(/^[^\\](\]|\|)$/) ? 1 : @input.matched.bytesize @input.pos = @input.pos - offset rescue @input.pos return nil if @input.pos == start_p match = extract_string(start_p..@input.pos-1) illegal_macro_delimiter? start_p, match if match.bytesize > 0 then create_node TextNode, :value => match else nil end end
Private Instance Methods
aggregate_parameters_for(node)
click to toggle source
# File lib/glyph/parser.rb, line 235 def aggregate_parameters_for(node) indices = [] count = 0 node.children.each do |n| indices << count if n[:parameter] count += 1 end # No parameter found if indices == [] then node[:parameters][0] = create_node ParameterNode, :name => :"0" node.children.each do |c| node[:parameters][0] << c end else # Parameters found current_index = 0 total_parameters = 0 save_parameter = lambda do |max_index| parameter = create_node ParameterNode, :name => "#{total_parameters}".to_sym total_parameters +=1 current_index.upto(max_index) do |index| parameter << (node & index) end node[:parameters] << parameter end indices.each do |i| save_parameter.call(i-1) current_index = i+1 end save_parameter.call(node.children.length-1) end node[:parameters] end
create_node(klass, hash={})
click to toggle source
# File lib/glyph/parser.rb, line 305 def create_node(klass, hash={}) klass.new.from hash end
error(msg)
click to toggle source
# File lib/glyph/parser.rb, line 298 def error(msg) lines = @input.string[0..@input.pos].split(/\n/) line = lines.length column = lines.last.length raise Glyph::SyntaxError.new("#{@source_name} [#{line}, #{column}] "+msg) end
extract_string(range)
click to toggle source
Thanks Thomas Leitner redmine.ruby-lang.org/issues/show/2645
# File lib/glyph/parser.rb, line 219 def extract_string(range) result = nil if @ruby19 then begin enc = @input.string.encoding @input.string.force_encoding('ASCII-8BIT') result = @input.string[range].force_encoding(enc) ensure @input.string.force_encoding(enc) end else result = @input.string[range] end result end
illegal_macro_delimiter?(start_p, string)
click to toggle source
# File lib/glyph/parser.rb, line 289 def illegal_macro_delimiter?(start_p, string) string.match(/\A(\[|\])|[^\\](\[|\])/) illegal_delimiter = $1 || $2 if illegal_delimiter then @input.pos = start_p + string.index(illegal_delimiter) error "Macro delimiter '#{illegal_delimiter}' not escaped" end end
macro_node_for(ident, escape=false)
click to toggle source
# File lib/glyph/parser.rb, line 196 def macro_node_for(ident, escape=false) macro_names = ident.split(/\//).select{|e| !e.blank?} nest_node = lambda do |parent, count| node = create_node(MacroNode, { :escape => false, :name => macro_names[count].to_sym }) parent ? (parent&0) << node : parent = node if macro_names[count+1] then node << create_node(ParameterNode, :name => :"0") nest_node.call(node, count+1) else node[:parameters] = [] node[:attributes] = [] node[:escape] = escape end node end nest_node.call(nil, 0) end
organize_children_for(node)
click to toggle source
# File lib/glyph/parser.rb, line 269 def organize_children_for(node) aggregate_parameters_for node node.children.clear node[:parameters].each do |p| node << p end empty_parameter = node.children.length == 1 && ((node&0).children.length == 0 || (node&0).children.length == 0 && (node&0&0).is_a?(TextNode) && (node&0&0)[:value].blank?) node.children.clear if empty_parameter node.delete(:parameters) node[:attributes].each do |a| node << a end node.delete(:attributes) end