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 +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
|