jsgf 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +36 -0
- data/jsgf.gemspec +1 -1
- data/lib/jsgf.rb +10 -1
- data/lib/jsgf/alternation.rb +14 -1
- data/lib/jsgf/builder.rb +54 -0
- data/lib/jsgf/grammar.rb +45 -22
- data/lib/jsgf/tokenizer.rb +1 -1
- data/test/jsgf/alternation.rb +5 -0
- data/test/jsgf/builder.rb +108 -0
- metadata +6 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e90d5a58e0adaadef490c11911c9443e14fb0a5
|
4
|
+
data.tar.gz: fd7deb3081c443f76c4505b72e1f18397d1ce183
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
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
|
data/lib/jsgf/alternation.rb
CHANGED
@@ -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
|
data/lib/jsgf/builder.rb
ADDED
@@ -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
|
-
|
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
|
-
|
24
|
-
|
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
|
-
|
58
|
-
#
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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};"
|
data/lib/jsgf/tokenizer.rb
CHANGED
data/test/jsgf/alternation.rb
CHANGED
@@ -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.
|
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-
|
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.
|
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
|