jsgf 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a35679fd0c76d2953a7e6bf557f145e0fff63a63
4
- data.tar.gz: 04a9e77a8561aa76bf73e4977933adfdec8f9f13
3
+ metadata.gz: 9e90d5a58e0adaadef490c11911c9443e14fb0a5
4
+ data.tar.gz: fd7deb3081c443f76c4505b72e1f18397d1ce183
5
5
  SHA512:
6
- metadata.gz: 43fd8dc5e6757e7c9cc3753784e1fecda9cf4fda61049c54eeb45a0e4b48cf318d1f953ecd2b3b454be709f081ab6d61c885e73a03b24a8bcc1d7bba708563da
7
- data.tar.gz: a5d9a59efb08497e731232b6554aaa9015cc8cb8fdc71b7444fdcad6c769789d34d42a2e23643edf9be9814da5dba0eca3c6b663f9e95edd8812139f3609ca6e
6
+ metadata.gz: 28d2d63a8f51d32efdcf597877e18ce86d45bded53077ef40e2908364af21fa23eb7b1f90262630738c866dd1df79d0a193bea81e04cb9619685bfbe9541645a
7
+ data.tar.gz: 132cefa593326e8b2d7c603746c8e114a1872de5bb566a5fa1fe8ec76ff5127492a8e28e9fa6f686ba4d1641071732399459bb976cb4a27c2b10592306a98d28
data/README.md CHANGED
@@ -9,12 +9,48 @@ For all of your [Java Speech Grammar Format](http://www.w3.org/TR/jsgf/) parsing
9
9
  Usage
10
10
  -----
11
11
 
12
+ Reading an existing JSGF file is as simple as...
13
+
12
14
  ```ruby
13
15
  require 'jsgf'
14
16
 
15
17
  grammar = JSGF.read('example.gram')
16
18
  ```
17
19
 
20
+ Writing to a file is similarly simple.
21
+
22
+ ```ruby
23
+ grammar.write('my_grammar.gram')
24
+ ```
25
+
26
+ You can also write to an IO stream.
27
+
28
+ ```ruby
29
+ File.open('my_grammar.gram', 'w') do |f|
30
+ grammar.write(f)
31
+ end
32
+ ```
33
+
34
+ ### Grammar DSL
35
+
36
+ The JSGF gem includes a simple DSL for generating new grammars. The syntax follows the
37
+ the [JSGF](http://www.w3.org/TR/jsgf/) syntax, but with a few differences.
38
+
39
+ - Rule names can be either Symbols or Strings (they're converted to Strings internally)
40
+ - Rules can be referenced using symbols in addition to the angle-bracket syntax used by JSGF.
41
+ - Alternations are created using arrays
42
+ - Rules are private by default, however the root rules are automatically made public
43
+
44
+ ```ruby
45
+ grammar = JSGF.grammar 'Turtle' do
46
+ rule quit: 'quit' # 'quit' and 'move' are public because
47
+ rule move: 'go :direction :distance' # they are the roots of the grammar tree
48
+
49
+ rule direction: %w{forward backward}
50
+ rule distance: %w{one two three four five six seven eight nine ten}
51
+ end
52
+ ```
53
+
18
54
  Installation
19
55
  ------------
20
56
 
data/jsgf.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "jsgf"
7
- spec.version = '0.3'
7
+ spec.version = '0.4'
8
8
  spec.authors = ["Brandon Fosdick"]
9
9
  spec.email = ["bfoz@bfoz.net"]
10
10
  spec.summary = %q{Java Speech Grammar Format}
data/lib/jsgf.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require_relative 'jsgf/builder'
1
2
  require_relative 'jsgf/parser'
2
3
  require_relative 'jsgf/tokenizer'
3
4
 
@@ -5,8 +6,11 @@ require_relative 'jsgf/tokenizer'
5
6
  A parser for JSGF files.
6
7
 
7
8
  http://www.w3.org/TR/jsgf/
9
+ http://en.wikipedia.org/wiki/JSGF
8
10
 
9
- # Usage
11
+ http://en.wikipedia.org/wiki/JSGF
12
+
13
+ @example
10
14
  grammar = JSGF.read(filename)
11
15
  =end
12
16
  module JSGF
@@ -16,4 +20,9 @@ module JSGF
16
20
  tokenzier = JSGF::Tokenizer.new(File.read(filename))
17
21
  JSGF::Parser.new(tokenzier).parse
18
22
  end
23
+
24
+ # @return [Grammar] a new {Grammar} built from the block argument
25
+ def self.grammar(name=nil, &block)
26
+ Builder.build(name, &block)
27
+ end
19
28
  end
@@ -7,7 +7,20 @@ module JSGF
7
7
  attr_reader :tags
8
8
 
9
9
  def initialize(*args)
10
- @elements = args
10
+ @elements = args.map do |a|
11
+ case a
12
+ when String
13
+ case a
14
+ when /\<(.*)\>/, /:(.*)/ then {name:$1, weight:1.0, tags:[]}
15
+ else
16
+ {atom:a, weight:1.0, tags:[]}
17
+ end
18
+ when Symbol then {name:a.to_s, weight:1.0, tags:[]}
19
+ else
20
+ a
21
+ end
22
+ end
23
+
11
24
  @optional = false
12
25
  @tags = []
13
26
  end
@@ -0,0 +1,54 @@
1
+ require_relative 'alternation'
2
+ require_relative 'grammar'
3
+
4
+ module JSGF
5
+ class Builder
6
+ def initialize
7
+ @rules = {}
8
+ end
9
+
10
+ # Convenience method for instantiating a builder and then building a {Grammar}
11
+ # @param name [String] the name of the new {Grammar}
12
+ # @return [Grammar] a new {Grammar} built from the block argument
13
+ def self.build(name=nil, &block)
14
+ self.new.build(name, &block)
15
+ end
16
+
17
+ # @param name [String] the name of the new {Grammar}
18
+ # @return [Grammar] a new {Grammar} built from the block argument
19
+ def build(name=nil, &block)
20
+ @name = name || 'DSL'
21
+ instance_eval(&block) if block_given?
22
+ Grammar.new(name:@name, rules:@rules)
23
+ end
24
+
25
+ # Set the name attribute from the DSL block
26
+ def name(arg)
27
+ @name = arg
28
+ end
29
+
30
+ # Create a new rule using the provided name and string
31
+ # By default, all new rules are private, unless they're root rules
32
+ # @example
33
+ # rule rule1: 'This is a rule'
34
+ # rule rule2: ['one', 'two']
35
+ # rule rule3: 'This is not :rule2'
36
+ # @return [JSGF::Grammar]
37
+ def rule(**options)
38
+ options.each do |name, v|
39
+ @rules[name.to_s] = case v
40
+ when Array then [Alternation.new(*v)]
41
+ when Symbol then [{name:v.to_s, weight:1.0, tags:[]}]
42
+ else
43
+ v.split(' ').map do |a|
44
+ case a
45
+ when /\<(.*)\>/, /:(.*)/ then {name:$1, weight:1.0, tags:[]}
46
+ else
47
+ {atom:a, weight:1.0, tags:[]}
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/jsgf/grammar.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative 'optional'
2
+
1
3
  module JSGF
2
4
  class Grammar
3
5
  attr_reader :character_encoding
@@ -7,26 +9,50 @@ module JSGF
7
9
  attr_reader :public_rules
8
10
  attr_reader :version
9
11
 
10
- def initialize(name:nil, character_encoding:nil, locale:nil, private_rules:{}, public_rules:{}, version:nil)
12
+ # @param rules [Hash] the rules to search
13
+ # @return [Array<Hash,Hash>] the set of root and non-root rules from the given set of rules
14
+ def self.roots(rules)
15
+ # Descend through the rule trees to see if they reference each other.
16
+ # Root rules aren't referenced by any other rule.
17
+ names = rules.flat_map {|(name,rule)| find_rule_names(rule)}.compact.uniq
18
+ left, right = rules.partition {|name, rule| names.include?(name) }
19
+ [Hash[right], Hash[left]]
20
+ end
21
+
22
+ # Expand the given rule and collect any references to other rules
23
+ # @param rule [Array] the right-hand-side of the rule to expand
24
+ # @return [Array] an array of referenced rule names
25
+ def self.find_rule_names(rule)
26
+ case rule
27
+ when Alternation, Array, Optional
28
+ rule.flat_map {|a| find_rule_names(a) }
29
+ when Hash
30
+ rule[:name]
31
+ else
32
+ raise StandardError, "Unkown atom #{rule.class}"
33
+ end
34
+ end
35
+
36
+ def initialize(name:nil, character_encoding:nil, locale:nil, private_rules:{}, public_rules:{}, rules:nil, version:nil)
11
37
  raise ArgumentError, "Grammar requires a name" unless name
12
38
  @character_encoding = character_encoding
13
39
  @locale = locale
14
40
  @grammar_name = name
15
- @private_rules = private_rules
16
- @public_rules = public_rules
17
41
  @version = version
42
+
43
+ if rules and !rules.empty?
44
+ @public_rules, @private_rules = ::JSGF::Grammar.roots(rules)
45
+ else
46
+ @private_rules = private_rules
47
+ @public_rules = public_rules
48
+ end
18
49
  end
19
50
 
20
51
  # @!attribute roots
21
52
  # @return [Hash] the set of public rules that comprise the roots of the {Grammar} tree
22
53
  def roots
23
- # Descend through the public rule trees to see if they reference each other
24
- root_rules = public_rules.dup
25
- public_rules.each do |(name, rule)|
26
- names = find_rule_names(rule).compact
27
- root_rules.delete_if {|k,v| names.include?(k)}
28
- end
29
- root_rules.empty? ? nil : root_rules
54
+ r = self.class.roots(public_rules).first
55
+ r.empty? ? nil : r
30
56
  end
31
57
 
32
58
  # @!attribute rules
@@ -54,21 +80,18 @@ module JSGF
54
80
  [header, grammar_header, *public_rule_array, *private_rule_array].join("\n")
55
81
  end
56
82
 
57
- private
58
- # Expand the given rule and collect any references to other rules
59
- # @param rule [Array] the right-hand-side of the rule to expand
60
- # @return [Array] an array of referenced rule names
61
- def find_rule_names(rule)
62
- case rule
63
- when Alternation, Array, Optional
64
- rule.flat_map {|a| find_rule_names(a) }
65
- when Hash
66
- rule[:name]
67
- else
68
- raise StandardError, "Unkown atom #{rule.class}"
83
+ # Write the {Grammar} to a file or an {IO} stream
84
+ # @param file [String,IO] a filename, or an IO stream, to write to
85
+ def write(file)
86
+ if file.is_a?(String)
87
+ File.write(file, self.to_s)
88
+ else
89
+ file.write(self.to_s)
69
90
  end
70
91
  end
71
92
 
93
+ private
94
+
72
95
  # Generate the grammar name header line
73
96
  def grammar_header
74
97
  "grammar #{grammar_name};"
@@ -12,7 +12,7 @@ module JSGF
12
12
  %r{/\d*(\.\d+)?/} => :WEIGHT,
13
13
  }
14
14
 
15
- # @param io [IO] the {IO} stream to read from
15
+ # @param io [IO] the {::IO} stream to read from
16
16
  def initialize(io)
17
17
  io = io.read unless io.is_a?(String)
18
18
  @scanner = StringScanner.new(io)
@@ -2,6 +2,11 @@ require 'minitest/autorun'
2
2
  require 'jsgf/parser'
3
3
 
4
4
  describe JSGF::Alternation do
5
+ it 'must convert string arguments into atoms' do
6
+ alternation = JSGF::Alternation.new('one', 'two', 'three')
7
+ alternation.elements.must_equal [{atom:'one', weight:1.0, tags:[]}, {atom:'two', weight:1.0, tags:[]}, {atom:'three', weight:1.0, tags:[]}]
8
+ end
9
+
5
10
  it 'must be enumerable' do
6
11
  alternation = JSGF::Alternation.new('one', 'two', 'three')
7
12
  alternation.all? {|a| a}
@@ -0,0 +1,108 @@
1
+ require 'minitest/autorun'
2
+ require 'jsgf/builder'
3
+
4
+ describe JSGF::Builder do
5
+ it 'must set the name from inside the block' do
6
+ grammar = JSGF::Builder.build do
7
+ name 'MyName'
8
+ end
9
+ grammar.grammar_name.must_equal 'MyName'
10
+ end
11
+
12
+ it 'must set the name from an argument' do
13
+ grammar = JSGF::Builder.build 'MyName'
14
+ grammar.grammar_name.must_equal 'MyName'
15
+ end
16
+
17
+ it 'must build a single-atom rule' do
18
+ grammar = JSGF::Builder.new.build do
19
+ rule rule1: 'one'
20
+ end
21
+
22
+ grammar.rules.size.must_equal 1
23
+ grammar.rules['rule1'].must_equal [{atom:'one', weight:1.0, tags:[]}]
24
+ end
25
+
26
+ it 'must build a multi-atom rule' do
27
+ grammar = JSGF::Builder.build {rule rule1: 'one two' }
28
+ grammar.rules.size.must_equal 1
29
+ grammar.rules['rule1'].must_equal [{atom:'one', weight:1.0, tags:[]}, {atom:'two', weight:1.0, tags:[]}]
30
+ end
31
+
32
+ it 'must build a rule with a rule reference as a Symbol' do
33
+ grammar = JSGF::Builder.build do
34
+ rule rule1: :one
35
+ rule one: 'one'
36
+ end
37
+
38
+ grammar.rules.size.must_equal 2
39
+ grammar.rules['rule1'].must_equal [{name:'one', weight:1.0, tags:[]}]
40
+ end
41
+
42
+ it 'must build a rule with a JSGF-style rule reference embedded in a string' do
43
+ grammar = JSGF::Builder.build do
44
+ rule rule1: '<one>'
45
+ rule one: 'one'
46
+ end
47
+
48
+ grammar.rules.size.must_equal 2
49
+ grammar.rules['rule1'].must_equal [{name:'one', weight:1.0, tags:[]}]
50
+ end
51
+
52
+ it 'must build a rule with a rule reference symbol embedded in a string' do
53
+ grammar = JSGF::Builder.build do
54
+ rule rule1: ':one'
55
+ rule one: 'one'
56
+ end
57
+
58
+ grammar.rules.size.must_equal 2
59
+ grammar.rules['rule1'].must_equal [{name:'one', weight:1.0, tags:[]}]
60
+ end
61
+
62
+ describe 'alternation' do
63
+ it 'must build an alternation from an array of atoms' do
64
+ grammar = JSGF::Builder.build do
65
+ rule rule1: %w{one two}
66
+ end
67
+ grammar.rules.size.must_equal 1
68
+ grammar.rules['rule1'].first.must_be_kind_of JSGF::Alternation
69
+ grammar.rules['rule1'].first.elements.must_equal [{atom:'one', weight:1.0, tags:[]}, {atom:'two', weight:1.0, tags:[]}]
70
+ end
71
+
72
+ it 'must build an alternation from an array of rule reference symbols' do
73
+ grammar = JSGF::Builder.build do
74
+ rule rule1: [:one, :two]
75
+ rule one: 'one'
76
+ rule two: 'two'
77
+ end
78
+
79
+ grammar.rules.size.must_equal 3
80
+ grammar.rules['rule1'].first.must_be_kind_of JSGF::Alternation
81
+ grammar.rules['rule1'].first.elements.must_equal [{name:'one', weight:1.0, tags:[]}, {name:'two', weight:1.0, tags:[]}]
82
+ end
83
+
84
+ it 'must build an alternation from an array of strings containing embedded rule reference symbols' do
85
+ grammar = JSGF::Builder.build do
86
+ rule rule1: %w[:one :two]
87
+ rule one: 'one'
88
+ rule two: 'two'
89
+ end
90
+
91
+ grammar.rules.size.must_equal 3
92
+ grammar.rules['rule1'].first.must_be_kind_of JSGF::Alternation
93
+ grammar.rules['rule1'].first.elements.must_equal [{name:'one', weight:1.0, tags:[]}, {name:'two', weight:1.0, tags:[]}]
94
+ end
95
+
96
+ it 'must build an alternation from an array of strings containing embedded JSGF-style rule reference names' do
97
+ grammar = JSGF::Builder.build do
98
+ rule rule1: %w[<one> <two>]
99
+ rule one: 'one'
100
+ rule two: 'two'
101
+ end
102
+
103
+ grammar.rules.size.must_equal 3
104
+ grammar.rules['rule1'].first.must_be_kind_of JSGF::Alternation
105
+ grammar.rules['rule1'].first.elements.must_equal [{name:'one', weight:1.0, tags:[]}, {name:'two', weight:1.0, tags:[]}]
106
+ end
107
+ end
108
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsgf
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.3'
4
+ version: '0.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Fosdick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-26 00:00:00.000000000 Z
11
+ date: 2015-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -53,12 +53,14 @@ files:
53
53
  - jsgf.gemspec
54
54
  - lib/jsgf.rb
55
55
  - lib/jsgf/alternation.rb
56
+ - lib/jsgf/builder.rb
56
57
  - lib/jsgf/grammar.rb
57
58
  - lib/jsgf/optional.rb
58
59
  - lib/jsgf/parser.rb
59
60
  - lib/jsgf/repetition.rb
60
61
  - lib/jsgf/tokenizer.rb
61
62
  - test/jsgf/alternation.rb
63
+ - test/jsgf/builder.rb
62
64
  - test/jsgf/grammar.rb
63
65
  - test/jsgf/optional.rb
64
66
  - test/jsgf/parser.rb
@@ -83,12 +85,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
85
  version: '0'
84
86
  requirements: []
85
87
  rubyforge_project:
86
- rubygems_version: 2.2.2
88
+ rubygems_version: 2.4.5
87
89
  signing_key:
88
90
  specification_version: 4
89
91
  summary: Java Speech Grammar Format
90
92
  test_files:
91
93
  - test/jsgf/alternation.rb
94
+ - test/jsgf/builder.rb
92
95
  - test/jsgf/grammar.rb
93
96
  - test/jsgf/optional.rb
94
97
  - test/jsgf/parser.rb