jsgf 0.3 → 0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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