class EBNF::Rule

Represent individual parsed rules

Constants

BNF_OPS

Operations which are flattened to seprate rules in to_bnf.

OP_ARGN

The number of arguments expected per operator. ‘nil` for unspecified

TERM_OPS

Attributes

cleanup[RW]

Determines preparation and cleanup rules for reconstituting EBNF ? * + from BNF

comp[RW]

A comprehension is a sequence which contains all elements but the first of the original rule.

@return [Rule]

expr[RW]

Rule expression

@return [Array]

first[R]

Terminals that immediately procede this rule

@return [Array<Rule>]

follow[R]

Terminals that immediately follow this rule

@return [Array<Rule>]

id[RW]

ID of rule @return [String]

kind[RW]

Kind of rule

@return [:rule, :terminal, :terminals, or :pass]

orig[RW]

Original EBNF

@return [String]

start[RW]

Indicates that this is a starting rule

@return [Boolean]

sym[RW]

Symbol of rule

@return [Symbol]

Public Class Methods

from_sxp(sxp) click to toggle source

Return a rule from its SXP representation:

@example inputs

(pass _pass (plus (range "#x20\\t\\r\\n")))
(rule ebnf "1" (star (alt declaration rule)))
(terminal R_CHAR "19" (diff CHAR (alt "]" "-")))

Also may have ‘(first …)`, `(follow …)`, or `(start t)`.

@param [String, Array] sxp @return [Rule]

# File lib/ebnf/rule.rb, line 159
def self.from_sxp(sxp)
  if sxp.is_a?(String)
    require 'sxp' unless defined?(SXP)
    sxp = SXP.parse(sxp)
  end
  expr = sxp.detect {|e| e.is_a?(Array) && ![:first, :follow, :start].include?(e.first.to_sym)}
  first = sxp.detect {|e| e.is_a?(Array) && e.first.to_sym == :first}
  first = first[1..-1] if first
  follow = sxp.detect {|e| e.is_a?(Array) && e.first.to_sym == :follow}
  follow = follow[1..-1] if follow
  cleanup = sxp.detect {|e| e.is_a?(Array) && e.first.to_sym == :cleanup}
  cleanup = cleanup[1..-1] if cleanup
  start = sxp.any? {|e| e.is_a?(Array) && e.first.to_sym == :start}
  sym = sxp[1] if sxp[1].is_a?(Symbol)
  id = sxp[2] if sxp[2].is_a?(String)
  self.new(sym, id, expr, kind: sxp.first, first: first, follow: follow, cleanup: cleanup, start: start)
end
new(sym, id, expr, kind: nil, ebnf: nil, first: nil, follow: nil, start: nil, top_rule: nil, cleanup: nil) click to toggle source

@param [Symbol, nil] sym

`nil` is allowed only for @pass or @terminals

@param [Integer, nil] id @param [Array] expr

The expression is an internal-representation of an S-Expression with one of the following oparators:

* `alt` – A list of alternative rules, which are attempted in order. It terminates with the first matching rule, or is terminated as unmatched, if no such rule is found.
* `diff` – matches any string that matches `A` but does not match `B`.
* `hex` – A single character represented using the hexadecimal notation `#xnn`.
* `istr` – A string which matches in a case-insensitive manner, so that `(istr "fOo")` will match either of the strings `"foo"`, `"FOO"` or any other combination.
* `opt` – An optional rule or terminal. It either results in the matching rule or returns `nil`.
* `plus` – A sequence of one or more of the matching rule. If there is no such rule, it is terminated as unmatched; otherwise, the result is an array containing all matched input.
* `range` – A range of characters, possibly repeated, of the form `(range "a-z")`. May also use hexadecimal notation.
* `rept m n` – A sequence of at lest `m` and at most `n` of the matching rule. It will always return an array.
* `seq` – A sequence of rules or terminals. If any (other than `opt` or `star`) to not parse, the rule is terminated as unmatched.
* `star` – A sequence of zero or more of the matching rule. It will always return an array.

@param [:rule, :terminal, :terminals, :pass] kind (nil) @param [String] ebnf (nil)

When parsing, records the EBNF string used to create the rule.

@param [Array] first (nil)

Recorded set of terminals that can proceed this rule (LL(1))

@param [Array] follow (nil)

Recorded set of terminals that can follow this rule (LL(1))

@param [Boolean] start (nil)

Is this the starting rule for the grammar?

@param [Rule] top_rule (nil)

The top-most rule. All expressed rules are top-rules, derived rules have the original rule as their top-rule.

@param [Boolean] cleanup (nil)

Records information useful for cleaning up converted :plus, and :star expansions (LL(1)).
# File lib/ebnf/rule.rb, line 107
def initialize(sym, id, expr, kind: nil, ebnf: nil, first: nil, follow: nil, start: nil, top_rule: nil, cleanup: nil)
  @sym, @id = sym, id
  @expr = expr.is_a?(Array) ? expr : [:seq, expr].compact
  @ebnf, @kind, @first, @follow, @start, @cleanup, @top_rule = ebnf, kind, first, follow, start, cleanup, top_rule
  @top_rule ||= self
  @kind ||= case
  when sym.to_s == sym.to_s.upcase then :terminal
  when !BNF_OPS.include?(@expr.first) then :terminal
  else :rule
  end

  # Allow @pass and @terminals to not be named
  @sym ||= :_pass if @kind == :pass
  @sym ||= :_terminals if @kind == :terminals

  raise ArgumentError, "Rule sym must be a symbol, was #{@sym.inspect}" unless @sym.is_a?(Symbol)
  raise ArgumentError, "Rule id must be a string or nil, was #{@id.inspect}" unless (@id || "").is_a?(String)
  raise ArgumentError, "Rule kind must be one of :rule, :terminal, :terminals, or :pass, was #{@kind.inspect}" unless
    @kind.is_a?(Symbol) && %w(rule terminal terminals pass).map(&:to_sym).include?(@kind)

  case @expr.first
  when :alt
    raise ArgumentError, "#{@expr.first} operation must have at least one operand, had #{@expr.length - 1}" unless @expr.length > 1
  when :diff
    raise ArgumentError, "#{@expr.first} operation must have exactly two operands, had #{@expr.length - 1}" unless @expr.length == 3
  when :hex, :istr, :not, :opt, :plus, :range, :star
    raise ArgumentError, "#{@expr.first} operation must have exactly one operand, had #{@expr.length - 1}" unless @expr.length == 2
  when :rept
    raise ArgumentError, "#{@expr.first} operation must have exactly three, had #{@expr.length - 1}" unless @expr.length == 4
    raise ArgumentError, "#{@expr.first} operation must an non-negative integer minimum, was #{@expr[1]}" unless
      @expr[1].is_a?(Integer) && @expr[1] >= 0
    raise ArgumentError, "#{@expr.first} operation must an non-negative integer maximum or '*', was #{@expr[2]}" unless
      @expr[2] == '*' || @expr[2].is_a?(Integer) && @expr[2] >= 0
  when :seq
    # It's legal to have a zero-length sequence
  else
    raise ArgumentError, "Rule expression must be an array using a known operator, was #{@expr.first}"
  end
end

Public Instance Methods

<=>(other) click to toggle source

Rules compare using their ids

# File lib/ebnf/rule.rb, line 435
def <=>(other)
  if id && other.id
    if id == other.id
      id.to_s <=> other.id.to_s
    else
      id.to_f <=> other.id.to_f
    end
  else
    sym.to_s <=> other.sym.to_s
  end
end
==(other) click to toggle source

Two rules are equal if they have the same {#sym}, {#kind} and {#expr}.

@param [Rule] other @return [Boolean]

# File lib/ebnf/rule.rb, line 419
def ==(other)
  other.is_a?(Rule) &&
  sym   == other.sym &&
  kind  == other.kind &&
  expr  == other.expr
end
add_first(terminals) click to toggle source

Add terminal as proceding this rule.

@param [Array<Rule, Symbol, String>] terminals @return [Integer] if number of terminals added

# File lib/ebnf/rule.rb, line 658
def add_first(terminals)
  @first ||= []
  terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - @first
  @first += terminals
  terminals.length
end
add_follow(terminals) click to toggle source

Add terminal as following this rule. Don’t add _eps as a follow

@param [Array<Rule, Symbol, String>] terminals @return [Integer] if number of terminals added

# File lib/ebnf/rule.rb, line 669
def add_follow(terminals)
  # Remove terminals already in follows, and empty string
  terminals = terminals.map {|t| t.is_a?(Rule) ? t.sym : t} - (@follow || []) - [:_eps]
  unless terminals.empty?
    @follow ||= []
    @follow += terminals
  end
  terminals.length
end
alt?() click to toggle source

Is this rule of the form (alt …)?

# File lib/ebnf/rule.rb, line 400
def alt?
  expr.is_a?(Array) && expr.first == :alt
end
build(expr, kind: nil, cleanup: nil, **options) click to toggle source

Build a new rule creating a symbol and numbering from the current rule Symbol and number creation is handled by the top-most rule in such a chain.

@param [Array] expr @param [Symbol] kind (nil) @param [Hash{Symbol => Symbol}] cleanup (nil) @param [Hash{Symbol => Object}] options

# File lib/ebnf/rule.rb, line 184
def build(expr, kind: nil, cleanup: nil, **options)
  new_sym, new_id = @top_rule.send(:make_sym_id)
  self.class.new(new_sym, new_id, expr,
                 kind: kind,
                 ebnf: @ebnf,
                 top_rule: @top_rule,
                 cleanup: cleanup,
                 **options)
end
eql?(other) click to toggle source

Two rules are equivalent if they have the same {#expr}.

@param [Rule] other @return [Boolean]

# File lib/ebnf/rule.rb, line 430
def eql?(other)
  expr == other.expr
end
first_includes_eps?() click to toggle source

Do the firsts of this rule include the empty string?

@return [Boolean]

# File lib/ebnf/rule.rb, line 650
def first_includes_eps?
  @first && @first.include?(:_eps)
end
for_sxp() click to toggle source

Return representation for building S-Expressions.

@return [Array]

# File lib/ebnf/rule.rb, line 197
def for_sxp
  elements = [kind, sym]
  elements << id if id
  elements << [:start, true] if start
  elements << first.sort_by(&:to_s).unshift(:first) if first
  elements << follow.sort_by(&:to_s).unshift(:follow) if follow
  elements << [:cleanup, cleanup] if cleanup
  elements << expr
  elements
end
inspect() click to toggle source
# File lib/ebnf/rule.rb, line 409
def inspect
  "#<EBNF::Rule:#{object_id} " +
  {sym: sym, id: id, kind: kind, expr: expr}.inspect +
  ">"
end
non_terminals(ast, expr = @expr) click to toggle source

Return the non-terminals for this rule.

  • ‘alt` => this is every non-terminal.

  • ‘diff` => this is every non-terminal.

  • ‘hex` => nil

  • ‘istr` => nil

  • ‘not` => this is the last expression, if any.

  • ‘opt` => this is the last expression, if any.

  • ‘plus` => this is the last expression, if any.

  • ‘range` => nil

  • ‘rept` => this is the last expression, if any.

  • ‘seq` => this is the first expression in the sequence, if any.

  • ‘star` => this is the last expression, if any.

@param [Array<Rule>] ast

The set of rules, used to turn symbols into rules

@param [Array<Symbol,String,Array>] expr (@expr)

The expression to check, defaults to the rule expression.
Typically, if the expression is recursive, the embedded expression is called recursively.

@return [Array<Rule>] @note this is used for LL(1) tansformation, so rule types are limited

# File lib/ebnf/rule.rb, line 474
def non_terminals(ast, expr = @expr)
  ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).map do |sym|
    case sym
    when Symbol
      r = ast.detect {|r| r.sym == sym}
      r if r && r.rule?
    when Array
      non_terminals(ast, sym)
    else
      nil
    end
  end.flatten.compact.uniq
end
pass?() click to toggle source

Is this a pass? @return [Boolean]

# File lib/ebnf/rule.rb, line 389
def pass?
  kind == :pass
end
rule?() click to toggle source

Is this a rule? @return [Boolean]

# File lib/ebnf/rule.rb, line 395
def rule?
  kind == :rule
end
seq?() click to toggle source

Is this rule of the form (seq …)?

# File lib/ebnf/rule.rb, line 405
def seq?
  expr.is_a?(Array) && expr.first == :seq
end
starts_with?(sym) click to toggle source

Does this rule start with ‘sym`? It does if expr is that sym, expr starts with alt and contains that sym, or expr starts with seq and the next element is that sym.

@param [Symbol, class] sym

Symbol matching any start element, or if it is String, any start element which is a String

@return [Array<Symbol, String>] list of symbol (singular), or strings which are start symbol, or nil if there are none

# File lib/ebnf/rule.rb, line 551
def starts_with?(sym)
  if seq? && sym === (v = expr.fetch(1, nil))
    [v]
  elsif alt? && expr.any? {|e| sym === e}
    expr.select {|e| sym === e}
  else
    nil
  end
end
symbols(expr = @expr) click to toggle source

Return the symbols used in the rule.

@param [Array<Symbol,String,Array>] expr (@expr)

The expression to check, defaults to the rule expression.
Typically, if the expression is recursive, the embedded expression is called recursively.

@return [Array<Rule>]

# File lib/ebnf/rule.rb, line 529
def symbols(expr = @expr)
  expr[1..-1].map do |sym|
    case sym
    when Symbol
      sym
    when Array
      symbols(sym)
    end
  end.flatten.compact.uniq
end
terminal?() click to toggle source

Is this a terminal?

@return [Boolean]

# File lib/ebnf/rule.rb, line 383
def terminal?
  kind == :terminal
end
terminals(ast, expr = @expr) click to toggle source

Return the terminals for this rule.

  • ‘alt` => this is every terminal.

  • ‘diff` => this is every terminal.

  • ‘hex` => nil

  • ‘istr` => nil

  • ‘not` => this is the last expression, if any.

  • ‘opt` => this is the last expression, if any.

  • ‘plus` => this is the last expression, if any.

  • ‘range` => nil

  • ‘rept` => this is the last expression, if any.

  • ‘seq` => this is the first expression in the sequence, if any.

  • ‘star` => this is the last expression, if any.

@param [Array<Rule>] ast

The set of rules, used to turn symbols into rules

@param [Array<Symbol,String,Array>] expr (@expr)

The expression to check, defaults to the rule expression.
Typically, if the expression is recursive, the embedded expression is called recursively.

@return [Array<Rule>] @note this is used for LL(1) tansformation, so rule types are limited

# File lib/ebnf/rule.rb, line 509
def terminals(ast, expr = @expr)
  ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).map do |sym|
    case sym
    when Symbol
      r = ast.detect {|r| r.sym == sym}
      r if r && r.terminal?
    when String
      sym
    when Array
      terminals(ast, sym)
    end
  end.flatten.compact.uniq
end
to_bnf() click to toggle source

Transform EBNF rule to BNF rules:

* Transform `(rule a "n" (op1 (op2)))` into two rules:

      (rule a "n" (op1 _a_1))
      (rule _a_1 "n.1" (op2))
* Transform `(rule a (opt b))` into `(rule a (alt _empty b))`
* Transform `(rule a (star b))` into `(rule a (alt _empty (seq b a)))`
* Transform `(rule a (plus b))` into `(rule a (seq b (star b)`

Transformation includes information used to re-construct non-transformed.

AST representation @return [Array<Rule>]

# File lib/ebnf/rule.rb, line 259
def to_bnf
  return [self] unless rule?
  new_rules = []

  # Look for rules containing recursive definition and rewrite to multiple rules. If `expr` contains elements which are in array form, where the first element of that array is a symbol, create a new rule for it.
  if expr.any? {|e| e.is_a?(Array) && (BNF_OPS + TERM_OPS).include?(e.first)}
    #   * Transform (a [n] rule (op1 (op2))) into two rules:
    #     (a.1 [n.1] rule (op1 a.2))
    #     (a.2 [n.2] rule (op2))
    # duplicate ourselves for rewriting
    this = dup
    new_rules << this

    expr.each_with_index do |e, index|
      next unless e.is_a?(Array) && e.first.is_a?(Symbol)
      new_rule = build(e)
      this.expr[index] = new_rule.sym
      new_rules << new_rule
    end

    # Return new rules after recursively applying #to_bnf
    new_rules = new_rules.map {|r| r.to_bnf}.flatten
  elsif expr.first == :opt
    this = dup
    #   * Transform (rule a (opt b)) into (rule a (alt _empty b))
    this.expr = [:alt, :_empty, expr.last]
    this.cleanup = :opt
    new_rules = this.to_bnf
  elsif expr.first == :star
    #   * Transform (rule a (star b)) into (rule a (alt _empty (seq b a)))
    this = dup
    this.cleanup = :star
    new_rule = this.build([:seq, expr.last, this.sym], cleanup: :merge)
    this.expr = [:alt, :_empty, new_rule.sym]
    new_rules = [this] + new_rule.to_bnf
  elsif expr.first == :plus
    #   * Transform (rule a (plus b)) into (rule a (seq b (star b)
    this = dup
    this.cleanup = :plus
    this.expr = [:seq, expr.last, [:star, expr.last]]
    new_rules = this.to_bnf
  elsif [:alt, :seq].include?(expr.first)
    # Otherwise, no further transformation necessary
    new_rules << self
  elsif [:diff, :hex, :range].include?(expr.first)
    # This rules are fine, they just need to be terminals
    raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.terminal?
    new_rules << self
  else
    # Some case we didn't think of
    raise "Error trying to transform #{expr.inspect} to BNF"
  end
  
  return new_rules
end
to_peg() click to toggle source

Transform EBNF rule for PEG:

* Transform `(rule a "n" (op1 ... (op2 y) ...z))` into two rules:

      (rule a "n" (op1 ... _a_1 ... z))
      (rule _a_1 "n.1" (op2 y))
* Transform `(rule a "n" (diff op1 op2))` into two rules:

      (rule a "n" (seq _a_1 op1))
      (rule _a_1 "n.1" (not op1))

@return [Array<Rule>]

# File lib/ebnf/rule.rb, line 328
def to_peg
  new_rules = []

  # Look for rules containing sub-sequences
  if expr.any? {|e| e.is_a?(Array) && e.first.is_a?(Symbol)}
    # duplicate ourselves for rewriting
    this = dup
    new_rules << this

    expr.each_with_index do |e, index|
      next unless e.is_a?(Array) && e.first.is_a?(Symbol)
      new_rule = build(e)
      this.expr[index] = new_rule.sym
      new_rules << new_rule
    end

    # Return new rules after recursively applying #to_bnf
    new_rules = new_rules.map {|r| r.to_peg}.flatten
  elsif expr.first == :diff && !terminal?
    this = dup
    new_rule = build([:not, expr[2]])
    this.expr = [:seq, new_rule.sym, expr[1]]
    new_rules << this
    new_rules << new_rule
  elsif [:hex, :istr, :range].include?(expr.first)
    # This rules are fine, they just need to be terminals
    raise "Encountered #{expr.first.inspect}, which is a #{self.kind}, not :terminal" unless self.terminal?
    new_rules << self
  else
    new_rules << self
  end
  
  return new_rules.map {|r| r.extend(EBNF::PEG::Rule)}
end
to_regexp() click to toggle source

For :hex or :range, create a regular expression.

@return [Regexp]

# File lib/ebnf/rule.rb, line 367
def to_regexp
  case expr.first
  when :hex
    Regexp.new(Regexp.escape(translate_codepoints(expr[1])))
  when :istr
    /#{expr.last}/ui
  when :range
    Regexp.new("[#{escape_regexp_character_range(translate_codepoints(expr[1]))}]")
  else
    raise "Can't turn #{expr.inspect} into a regexp"
  end
end
to_ruby() click to toggle source

Return a Ruby representation of this rule @return [String]

# File lib/ebnf/rule.rb, line 240
def to_ruby
  "EBNF::Rule.new(#{sym.inspect}, #{id.inspect}, #{expr.inspect}#{', kind: ' + kind.inspect unless kind == :rule})"
end
to_s(**options)
Alias for: to_sxp
to_sxp(**options) click to toggle source

Return SXP representation of this rule

@return [String]

# File lib/ebnf/rule.rb, line 211
def to_sxp(**options)
  require 'sxp' unless defined?(SXP)
  for_sxp.to_sxp(**options)
end
Also aliased as: to_s
to_ttl() click to toggle source

Serializes this rule to an Turtle.

@return [String]

# File lib/ebnf/rule.rb, line 221
def to_ttl
  @ebnf.debug("to_ttl") {inspect} if @ebnf
  statements = [%{:#{sym} rdfs:label "#{sym}";}]
  if orig
    comment = orig.to_s.strip.
      gsub(/"""/, '\"\"\"').
      gsub("\\", "\\\\").
      sub(/^\"/, '\"').
      sub(/\"$/m, '\"')
    statements << %{  rdfs:comment #{comment.inspect};}
  end
  statements << %{  dc:identifier "#{id}";} if id
  
  statements += ttl_expr(expr, terminal? ? "re" : "g", 1, false)
  "\n" + statements.join("\n")
end
translate_codepoints(str) click to toggle source

Utility function to translate code points of the form ‘#xN’ into ruby unicode characters

# File lib/ebnf/rule.rb, line 449
def translate_codepoints(str)
  str.gsub(/#x\h+/) {|c| c[2..-1].scanf("%x").first.chr(Encoding::UTF_8)}
end
valid?(ast) click to toggle source

Validate the rule, with respect to an AST.

Uses ‘#validate!` and catches `RangeError`

@param [Array<Rule>] ast

The set of rules, used to turn symbols into rules

@return [Boolean]

# File lib/ebnf/rule.rb, line 640
def valid?(ast)
  validate!(ast)
  true
rescue SyntaxError
  false
end
validate!(ast, expr = @expr) click to toggle source

Validate the rule, with respect to an AST.

@param [Array<Rule>] ast

The set of rules, used to turn symbols into rules

@param [Array<Symbol,String,Array>] expr (@expr)

The expression to check, defaults to the rule expression.
Typically, if the expression is recursive, the embedded expression is called recursively.

@raise [RangeError]

# File lib/ebnf/rule.rb, line 570
def validate!(ast, expr = @expr)
  op = expr.first
  raise SyntaxError, "Unknown operator: #{op}" unless OP_ARGN.key?(op)
  raise SyntaxError, "Argument count missmatch on operator #{op}, had #{expr.length - 1} expected #{OP_ARGN[op]}" if
    OP_ARGN[op] && OP_ARGN[op] != expr.length - 1

  # rept operator needs min and max
  if op == :alt
    raise SyntaxError, "alt operation must have at least one operand, had #{expr.length - 1}" unless expr.length > 1
  elsif op == :rept
    raise SyntaxError, "rept operation must an non-negative integer minimum, was #{expr[1]}" unless
      expr[1].is_a?(Integer) && expr[1] >= 0
    raise SyntaxError, "rept operation must an non-negative integer maximum or '*', was #{expr[2]}" unless
      expr[2] == '*' || expr[2].is_a?(Integer) && expr[2] >= 0
  end

  case op
  when :hex
    raise SyntaxError, "Hex operand must be of form '#xN+': #{sym}" unless expr.last.match?(/^#x\h+$/)
  when :range
    str = expr.last.dup
    str = str[1..-1] if str.start_with?('^')
    str = str[0..-2] if str.end_with?('-')  # Allowed at end of range
    scanner = StringScanner.new(str)
    hex = rchar = in_range = false
    while !scanner.eos?
      begin
        if scanner.scan(Terminals::HEX)
          raise SyntaxError if in_range && rchar
          rchar = in_range = false
          hex = true
        elsif scanner.scan(Terminals::R_CHAR)
          raise SyntaxError if in_range && hex
          hex = in_range = false
          rchar = true
        else
          raise(SyntaxError, "Range contains illegal components at offset #{scanner.pos}: was #{expr.last}")
        end

        if scanner.scan(/\-/)
          raise SyntaxError if in_range
          in_range = true
        end
      rescue SyntaxError
        raise(SyntaxError, "Range contains illegal components at offset #{scanner.pos}: was #{expr.last}")
      end
    end
  else
    ([:alt, :diff].include?(expr.first) ? expr[1..-1] : expr[1,1]).each do |sym|
      case sym
      when Symbol
        r = ast.detect {|r| r.sym == sym}
        raise SyntaxError, "No rule found for #{sym}" unless r
      when Array
        validate!(ast, sym)
      when String
        raise SyntaxError, "String must be of the form CHAR*" unless sym.match?(/^#{Terminals::CHAR}*$/)
      end
    end
  end
end

Private Instance Methods

cclass(txt) click to toggle source

turn an XML BNF character class into an N3 literal for that character class (less the outer quote marks)

>>> cclass("^<>'{}|^`")
"[^<>'{}|^`]"
>>> cclass("#x0300-#x036F")
"[\\u0300-\\u036F]"
>>> cclass("#xC0-#xD6")
"[\\u00C0-\\u00D6]"
>>> cclass("#x370-#x37D")
"[\\u0370-\\u037D]"

as in: ECHAR ::= '\' [tbnrf\"']
>>> cclass("tbnrf\\\"'")
'tbnrf\\\\\\"\''

>>> cclass("^#x22#x5C#x0A#x0D")
'^\\u0022\\\\\\u005C\\u000A\\u000D'
# File lib/ebnf/rule.rb, line 753
def cclass(txt)
  '[' +
  txt.gsub(/\#x[0-9a-fA-F]+/) do |hx|
    hx = hx[2..-1]
    if hx.length <= 4
      "\\u#{'0' * (4 - hx.length)}#{hx}" 
    elsif hx.length <= 8
      "\\U#{'0' * (8 - hx.length)}#{hx}" 
    end
  end +
  ']'
end
escape_regexp_character_range(character_range) click to toggle source

Escape “[”, “]”, and “" in ranges so they don’t result in a warning or error about empty character classes.

# File lib/ebnf/rule.rb, line 776
def escape_regexp_character_range(character_range)
  character_range.gsub(/([\[\]\\])/) {|char| "\\#{char}"}
end
make_sym_id(variation = nil) click to toggle source

Make a new symbol/number combination @param [String] variation added to symbol to aid reconstitution from BNF to EBNF

# File lib/ebnf/rule.rb, line 768
def make_sym_id(variation = nil)
  @id_seq ||= 0
  @id_seq += 1
  ["_#{@sym}_#{@id_seq}#{variation}".to_sym, ("#{@id}.#{@id_seq}#{variation}" if @id)]
end
ttl_expr(expr, pfx, depth, is_obj = true) click to toggle source
# File lib/ebnf/rule.rb, line 680
def ttl_expr(expr, pfx, depth, is_obj = true)
  indent = '  ' * depth
  @ebnf.debug("ttl_expr", depth: depth) {expr.inspect} if @ebnf
  op, *expr = expr if expr.is_a?(Array)
  statements = []
  
  if is_obj
    bra, ket = "[ ", " ]"
  else
    bra = ket = ''
  end

  case op
  when :seq, :alt, :diff
    # Multiple operands
    statements << %{#{indent}#{bra}#{pfx}:#{op} (}
    expr.each {|a| statements += ttl_expr(a, pfx, depth + 1)}
    statements << %{#{indent} )#{ket}}
  when :opt, :plus, :star, :not
    # Single operand
    statements << %{#{indent}#{bra}#{pfx}:#{op} }
    statements += ttl_expr(expr.first, pfx, depth + 1)
    statements << %{#{indent} #{ket}} unless ket.empty?
  when :rept
    # Three operands (min, max and expr)
    statements << %{  #{indent}#{pfx}:min #{expr[0].inspect};}
    statements << %{  #{indent}#{pfx}:max #{expr[1].inspect};}
    statements << %{#{indent}#{bra}#{pfx}:#{op} }
    statements += ttl_expr(expr.last, pfx, depth + 1)
    statements << %{#{indent} #{ket}} unless ket.empty?
  when :_empty, :_eps
    statements << %{#{indent}"g:#{op.to_s[1..-1]}"}
  when :"'"
    statements << %{#{indent}"#{esc(expr)}"}
  when :istr
    statements << %{#{indent}#{bra} re:matches #{expr.first.inspect} #{ket}}
  when :range
    statements << %{#{indent}#{bra} re:matches #{cclass(expr.first).inspect} #{ket}}
  when :hex
    raise "didn't expect \" in expr" if expr.include?(:'"')
    statements << %{#{indent}#{bra} re:matches #{cclass(expr.first).inspect} #{ket}}
  else
    if is_obj
      statements << %{#{indent}#{expr.inspect}}
    else
      statements << %{#{indent}g:seq ( #{expr.inspect} )}
    end
  end
  
  statements.last << " ." unless is_obj
  @ebnf.debug("statements", depth: depth) {statements.join("\n")} if @ebnf
  statements
end