menu_markup 0.1.0
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 +7 -0
- data/.bundle/config +2 -0
- data/.gitignore +2 -0
- data/.travis.yml +4 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/Guardfile +70 -0
- data/README.md +120 -0
- data/Rakefile +1 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/menu_markup.rb +15 -0
- data/lib/menu_markup.treetop +77 -0
- data/lib/menu_markup/parsed_price.rb +75 -0
- data/lib/menu_markup/version.rb +3 -0
- data/lib/menu_markup_parser.rb +132 -0
- data/menu_markup.gemspec +38 -0
- metadata +186 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 4e0d49032e872c8972e2c9be2e47921983f1d972
|
|
4
|
+
data.tar.gz: 5f65d9129666338f23022691d4baa968ff5db83f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5f1b40046f99bb7a1c1e7b096eaddc5c9bf5f8a254ac11d4c942db101750f13e0cf5ae35fe7676b1739c4544d26f4f0326c235d70d722ec3539568ffd5404a63
|
|
7
|
+
data.tar.gz: 7903175def35662261833299bb195cc88bacb4d699426145b2138d8d61a3d7f95525ded5c8694be1f311a05f715f39ccf8c2e5f36f54281dccd4f4515f551abd
|
data/.bundle/config
ADDED
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
menu_markup (0.1.0)
|
|
5
|
+
activesupport
|
|
6
|
+
treetop (~> 1.4.15)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
activesupport (4.2.3)
|
|
12
|
+
i18n (~> 0.7)
|
|
13
|
+
json (~> 1.7, >= 1.7.7)
|
|
14
|
+
minitest (~> 5.1)
|
|
15
|
+
thread_safe (~> 0.3, >= 0.3.4)
|
|
16
|
+
tzinfo (~> 1.1)
|
|
17
|
+
coderay (1.1.0)
|
|
18
|
+
coveralls (0.8.2)
|
|
19
|
+
json (~> 1.8)
|
|
20
|
+
rest-client (>= 1.6.8, < 2)
|
|
21
|
+
simplecov (~> 0.10.0)
|
|
22
|
+
term-ansicolor (~> 1.3)
|
|
23
|
+
thor (~> 0.19.1)
|
|
24
|
+
diff-lcs (1.2.5)
|
|
25
|
+
docile (1.1.5)
|
|
26
|
+
domain_name (0.5.24)
|
|
27
|
+
unf (>= 0.0.5, < 1.0.0)
|
|
28
|
+
ffi (1.9.10)
|
|
29
|
+
formatador (0.2.5)
|
|
30
|
+
guard (2.12.8)
|
|
31
|
+
formatador (>= 0.2.4)
|
|
32
|
+
listen (>= 2.7, <= 4.0)
|
|
33
|
+
lumberjack (~> 1.0)
|
|
34
|
+
nenv (~> 0.1)
|
|
35
|
+
notiffany (~> 0.0)
|
|
36
|
+
pry (>= 0.9.12)
|
|
37
|
+
shellany (~> 0.0)
|
|
38
|
+
thor (>= 0.18.1)
|
|
39
|
+
guard-compat (1.2.1)
|
|
40
|
+
guard-rspec (4.6.2)
|
|
41
|
+
guard (~> 2.1)
|
|
42
|
+
guard-compat (~> 1.1)
|
|
43
|
+
rspec (>= 2.99.0, < 4.0)
|
|
44
|
+
http-cookie (1.0.2)
|
|
45
|
+
domain_name (~> 0.5)
|
|
46
|
+
i18n (0.7.0)
|
|
47
|
+
json (1.8.3)
|
|
48
|
+
listen (3.0.2)
|
|
49
|
+
rb-fsevent (>= 0.9.3)
|
|
50
|
+
rb-inotify (>= 0.9)
|
|
51
|
+
lumberjack (1.0.9)
|
|
52
|
+
method_source (0.8.2)
|
|
53
|
+
mime-types (2.6.1)
|
|
54
|
+
minitest (5.7.0)
|
|
55
|
+
nenv (0.2.0)
|
|
56
|
+
netrc (0.10.3)
|
|
57
|
+
notiffany (0.0.6)
|
|
58
|
+
nenv (~> 0.1)
|
|
59
|
+
shellany (~> 0.0)
|
|
60
|
+
polyglot (0.3.5)
|
|
61
|
+
pry (0.10.1)
|
|
62
|
+
coderay (~> 1.1.0)
|
|
63
|
+
method_source (~> 0.8.1)
|
|
64
|
+
slop (~> 3.4)
|
|
65
|
+
rake (10.4.2)
|
|
66
|
+
rb-fsevent (0.9.5)
|
|
67
|
+
rb-inotify (0.9.5)
|
|
68
|
+
ffi (>= 0.5.0)
|
|
69
|
+
rest-client (1.8.0)
|
|
70
|
+
http-cookie (>= 1.0.2, < 2.0)
|
|
71
|
+
mime-types (>= 1.16, < 3.0)
|
|
72
|
+
netrc (~> 0.7)
|
|
73
|
+
rspec (3.3.0)
|
|
74
|
+
rspec-core (~> 3.3.0)
|
|
75
|
+
rspec-expectations (~> 3.3.0)
|
|
76
|
+
rspec-mocks (~> 3.3.0)
|
|
77
|
+
rspec-collection_matchers (1.1.2)
|
|
78
|
+
rspec-expectations (>= 2.99.0.beta1)
|
|
79
|
+
rspec-core (3.3.2)
|
|
80
|
+
rspec-support (~> 3.3.0)
|
|
81
|
+
rspec-expectations (3.3.1)
|
|
82
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
83
|
+
rspec-support (~> 3.3.0)
|
|
84
|
+
rspec-mocks (3.3.2)
|
|
85
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
86
|
+
rspec-support (~> 3.3.0)
|
|
87
|
+
rspec-support (3.3.0)
|
|
88
|
+
shellany (0.0.1)
|
|
89
|
+
simplecov (0.10.0)
|
|
90
|
+
docile (~> 1.1.0)
|
|
91
|
+
json (~> 1.8)
|
|
92
|
+
simplecov-html (~> 0.10.0)
|
|
93
|
+
simplecov-html (0.10.0)
|
|
94
|
+
slop (3.6.0)
|
|
95
|
+
term-ansicolor (1.3.2)
|
|
96
|
+
tins (~> 1.0)
|
|
97
|
+
thor (0.19.1)
|
|
98
|
+
thread_safe (0.3.5)
|
|
99
|
+
tins (1.5.4)
|
|
100
|
+
treetop (1.4.15)
|
|
101
|
+
polyglot
|
|
102
|
+
polyglot (>= 0.3.1)
|
|
103
|
+
tzinfo (1.2.2)
|
|
104
|
+
thread_safe (~> 0.1)
|
|
105
|
+
unf (0.1.4)
|
|
106
|
+
unf_ext
|
|
107
|
+
unf_ext (0.0.7.1)
|
|
108
|
+
|
|
109
|
+
PLATFORMS
|
|
110
|
+
ruby
|
|
111
|
+
|
|
112
|
+
DEPENDENCIES
|
|
113
|
+
bundler (~> 1.9)
|
|
114
|
+
coveralls
|
|
115
|
+
guard
|
|
116
|
+
guard-rspec
|
|
117
|
+
menu_markup!
|
|
118
|
+
rake (~> 10.0)
|
|
119
|
+
rspec
|
|
120
|
+
rspec-collection_matchers
|
data/Guardfile
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# A sample Guardfile
|
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
|
3
|
+
|
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
|
5
|
+
# directories %w(app lib config test spec features) \
|
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
|
7
|
+
|
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
|
11
|
+
#
|
|
12
|
+
# $ mkdir config
|
|
13
|
+
# $ mv Guardfile config/
|
|
14
|
+
# $ ln -s config/Guardfile .
|
|
15
|
+
#
|
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
|
17
|
+
|
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
|
20
|
+
# * bundler: 'bundle exec rspec'
|
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
|
23
|
+
# installed the spring binstubs per the docs)
|
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
|
25
|
+
# * 'just' rspec: 'rspec'
|
|
26
|
+
|
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
|
28
|
+
require "guard/rspec/dsl"
|
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
|
30
|
+
|
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
|
32
|
+
|
|
33
|
+
# RSpec files
|
|
34
|
+
rspec = dsl.rspec
|
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
|
37
|
+
watch(rspec.spec_files)
|
|
38
|
+
|
|
39
|
+
# Ruby files
|
|
40
|
+
ruby = dsl.ruby
|
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
|
42
|
+
|
|
43
|
+
# Rails files
|
|
44
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
|
45
|
+
dsl.watch_spec_files_for(rails.app_files)
|
|
46
|
+
dsl.watch_spec_files_for(rails.views)
|
|
47
|
+
|
|
48
|
+
watch(rails.controllers) do |m|
|
|
49
|
+
[
|
|
50
|
+
rspec.spec.("routing/#{m[1]}_routing"),
|
|
51
|
+
rspec.spec.("controllers/#{m[1]}_controller"),
|
|
52
|
+
rspec.spec.("acceptance/#{m[1]}")
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Rails config changes
|
|
57
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
|
58
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
|
59
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
|
60
|
+
|
|
61
|
+
# Capybara features specs
|
|
62
|
+
watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
|
|
63
|
+
watch(rails.layouts) { |m| rspec.spec.("features/#{m[1]}") }
|
|
64
|
+
|
|
65
|
+
# Turnip features and steps
|
|
66
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
|
67
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
|
68
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
|
69
|
+
end
|
|
70
|
+
end
|
data/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# MenuMarkup [](https://travis-ci.org/refreshingmenus/menu_markup) [](https://coveralls.io/r/refreshingmenus/menu_markup)
|
|
2
|
+
|
|
3
|
+
Ruby gem to parse MenuMarkup. See the [MenuMarkup specification][].
|
|
4
|
+
|
|
5
|
+
MenuMarkup is a super simple markup to specify menu data in plain text. When the MenuMarkup is parsed it creates a Menu.
|
|
6
|
+
A Menu consists of two types: Items and Sections. Items have multiple Prices.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem 'menu_markup'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
And then execute:
|
|
17
|
+
|
|
18
|
+
$ bundle
|
|
19
|
+
|
|
20
|
+
Or install it yourself as:
|
|
21
|
+
|
|
22
|
+
$ gem install menu_markup
|
|
23
|
+
|
|
24
|
+
## MenuMarkup example
|
|
25
|
+
|
|
26
|
+
For the full specs see the [MenuMarkup specification][].
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
# Lines with a # are comments!
|
|
30
|
+
# Empty lines are ignored
|
|
31
|
+
|
|
32
|
+
* Main dishes
|
|
33
|
+
The main dishes can be ordered between 17.00 and 21.00.
|
|
34
|
+
- Super meat 29.95 euro
|
|
35
|
+
This is 500 grams of pure super meat.
|
|
36
|
+
- Some other dish
|
|
37
|
+
#This dish has the price on a different line. It will be parsed correctly.
|
|
38
|
+
12,95
|
|
39
|
+
- Daily fish
|
|
40
|
+
#This differs every day and the price can also change.
|
|
41
|
+
=depends
|
|
42
|
+
|
|
43
|
+
*Dessert
|
|
44
|
+
- Special dessert
|
|
45
|
+
#This dessert has multiple sizes and prices.
|
|
46
|
+
= small 12,30
|
|
47
|
+
= large 23,30
|
|
48
|
+
- A desert for 2
|
|
49
|
+
= 12,30 per person
|
|
50
|
+
- 300 grams of chocolate
|
|
51
|
+
#To not make the price 300 we specify an empty price using '='.
|
|
52
|
+
=
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
parser = MenuMarkupParser.new
|
|
59
|
+
parser.parse("*Some\n-Menu\n-Markup")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
MenuMarkup.parse("*Some\n-Menu\n-Markup")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Next you should create some logic around the parsed content.
|
|
69
|
+
In the example below we have Entry, Item and Price ActiveRecord models.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
result = parser.parse(text)
|
|
73
|
+
result.menu.elements.collect { |element| build_entry(element) } if result
|
|
74
|
+
|
|
75
|
+
def build_entry(element)
|
|
76
|
+
entry = send("build_#{element.class.to_s.demodulize.underscore}", element) # Calls build_xx method
|
|
77
|
+
entry.menu = self # All models belong_to a Menu
|
|
78
|
+
entry.entries = element.children.collect { |subelement| build_entry(subelement) }
|
|
79
|
+
|
|
80
|
+
unless entry.valid?
|
|
81
|
+
line = parser.input.line_of(element.interval.first)
|
|
82
|
+
type = entry.class.name.humanize.downcase
|
|
83
|
+
errors.add(:markup_text, "has invalid #{type} on line ##{line}: #{entry.errors.to_a.to_sentence}")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
entry
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Define the build_xx methods and use the parsed element content.
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
def build_section(element)
|
|
94
|
+
Section.new(title: element.title, desc: element.description, choice: element.choice?, prices: build_prices(element))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_item(element)
|
|
98
|
+
Item.new(title: element.title, desc: element.description, spicy: element.spicy, prices: build_prices(element),
|
|
99
|
+
restriction_list: element.restrictions)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_prices(element)
|
|
103
|
+
# Up to the reader :)
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
bundle install
|
|
110
|
+
guard
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
1. Fork it ( https://github.com/[my-github-username]/menu_markup/fork )
|
|
115
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
116
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
|
117
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
|
118
|
+
5. Create a new Pull Request
|
|
119
|
+
|
|
120
|
+
[MenuMarkup specification]: http://www.webuildinternet.com/2012/07/04/menu-markup-specification/
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "bundler/setup"
|
|
4
|
+
require "menu_markup"
|
|
5
|
+
|
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
|
8
|
+
|
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
+
# require "pry"
|
|
11
|
+
# Pry.start
|
|
12
|
+
|
|
13
|
+
require "irb"
|
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/lib/menu_markup.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require 'treetop'
|
|
2
|
+
require "menu_markup/version"
|
|
3
|
+
require "menu_markup/parsed_price"
|
|
4
|
+
|
|
5
|
+
require "menu_markup_parser"
|
|
6
|
+
|
|
7
|
+
require 'active_support'
|
|
8
|
+
require 'active_support/core_ext/object/blank' # presence method
|
|
9
|
+
require 'active_support/core_ext/hash/reverse_merge' # reverse_merge! method
|
|
10
|
+
|
|
11
|
+
module MenuMarkup
|
|
12
|
+
def self.parse(text)
|
|
13
|
+
MenuMarkupParser.new.parse(text)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
grammar MenuMarkup
|
|
2
|
+
rule root
|
|
3
|
+
s menu
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
rule menu
|
|
7
|
+
(section / item)*
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
rule subsections
|
|
11
|
+
(subsection / item)*
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
rule subsubsections
|
|
15
|
+
(subsubsection / item)*
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
rule items do
|
|
19
|
+
item*
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rule section
|
|
23
|
+
s '*' choice_node:choice attributes_node:(attributes '*')? s description_node:description children_nodes:subsections <MenuMarkupParser::Section>
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
rule subsection
|
|
27
|
+
s '**' choice_node:choice attributes_node:(attributes '**')? s description_node:description children_nodes:subsubsections <MenuMarkupParser::Section>
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
rule subsubsection
|
|
31
|
+
s '***' choice_node:choice attributes_node:(attributes '***')? s description_node:description children_nodes:items <MenuMarkupParser::Section>
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
rule item
|
|
35
|
+
s '-' attributes_node:(attributes '-')? s description_node:description <MenuMarkupParser::Item>
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
rule choice
|
|
39
|
+
'/'?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
rule attributes
|
|
43
|
+
[vVhknmMH]* <MenuMarkupParser::Attributes>
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
rule price
|
|
47
|
+
s '=' s text_node:text_line
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
rule description
|
|
51
|
+
description_line*
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
rule description_line
|
|
55
|
+
text:([^-*#] text_line) s
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
rule s #optional space
|
|
59
|
+
S?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
rule S # mandatory space
|
|
63
|
+
(comment / whitespace)+
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
rule whitespace
|
|
67
|
+
[\s]+
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
rule comment
|
|
71
|
+
'#' text_line
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
rule text_line
|
|
75
|
+
[^\r\n]*
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
module MenuMarkup
|
|
3
|
+
class ParsedPrice
|
|
4
|
+
EOP = /(?=\s|$)/ui
|
|
5
|
+
BOP = /(?<=\s|^)/ui
|
|
6
|
+
DASH = /\-{1,2}|—{1,2}/ui # TODO: Support different dashes?, see: http://csswizardry.com/2010/01/the-three-types-of-dash/
|
|
7
|
+
CURRENCY = /\$|€|euro?|usd|dollar|gbp|tl|£|₤/ui # tl = Turkish lira
|
|
8
|
+
|
|
9
|
+
INTEGER = /1[0-7]\d\d|\d{1,3}/ui
|
|
10
|
+
COMMA_DECIMAL = /\d+,\d/ui
|
|
11
|
+
DOT_DECIMAL = /\d+\.\d/ui
|
|
12
|
+
DASH_COMMA_DECIMAL = /\d+,#{DASH}/ui
|
|
13
|
+
DASH_DOT_DECIMAL = /\d+\.#{DASH}/ui
|
|
14
|
+
FULL_COMMA_DECIMAL = /\d+,\d\d/ui
|
|
15
|
+
FULL_DOT_DECIMAL = /\d+\.\d\d/ui
|
|
16
|
+
|
|
17
|
+
PRE_CURRENCY_INTEGER = /#{CURRENCY}\s*#{INTEGER}/ui
|
|
18
|
+
POST_CURRENCY_INTEGER = /#{INTEGER}\s*#{CURRENCY}/ui
|
|
19
|
+
|
|
20
|
+
PRE_CURRENCY_COMMA = /#{CURRENCY}\s*#{COMMA_DECIMAL}/ui
|
|
21
|
+
POST_CURRENCY_COMMA = /#{COMMA_DECIMAL}\s*#{CURRENCY}/ui
|
|
22
|
+
PRE_CURRENCY_DOT = /#{CURRENCY}\s*#{DOT_DECIMAL}/ui
|
|
23
|
+
POST_CURRENCY_DOT = /#{DOT_DECIMAL}\s*#{CURRENCY}/ui
|
|
24
|
+
|
|
25
|
+
PRE_CURRENCY_DASH_COMMA = /#{CURRENCY}\s*#{DASH_COMMA_DECIMAL}/ui
|
|
26
|
+
POST_CURRENCY_DASH_COMMA = /#{DASH_COMMA_DECIMAL}\s*#{CURRENCY}/ui
|
|
27
|
+
PRE_CURRENCY_DASH_DOT = /#{CURRENCY}\s*#{DASH_DOT_DECIMAL}/ui
|
|
28
|
+
POST_CURRENCY_DASH_DOT = /#{DASH_DOT_DECIMAL}\s*#{CURRENCY}/ui
|
|
29
|
+
|
|
30
|
+
PRE_CURRENCY_FULL_COMMA = /#{CURRENCY}\s*#{FULL_COMMA_DECIMAL}/ui
|
|
31
|
+
POST_CURRENCY_FULL_COMMA = /#{FULL_COMMA_DECIMAL}\s*#{CURRENCY}/ui
|
|
32
|
+
PRE_CURRENCY_FULL_DOT = /#{CURRENCY}\s*#{FULL_DOT_DECIMAL}/ui
|
|
33
|
+
POST_CURRENCY_FULL_DOT = /#{FULL_DOT_DECIMAL}\s*#{CURRENCY}/ui
|
|
34
|
+
|
|
35
|
+
LITERAL = /dagprijs/ui
|
|
36
|
+
|
|
37
|
+
PRICES = [
|
|
38
|
+
PRE_CURRENCY_FULL_COMMA, PRE_CURRENCY_FULL_DOT, PRE_CURRENCY_DASH_COMMA, PRE_CURRENCY_DASH_DOT, PRE_CURRENCY_COMMA, PRE_CURRENCY_DOT, PRE_CURRENCY_INTEGER,
|
|
39
|
+
POST_CURRENCY_FULL_COMMA, POST_CURRENCY_FULL_DOT, POST_CURRENCY_DASH_COMMA, POST_CURRENCY_DASH_DOT, POST_CURRENCY_COMMA, POST_CURRENCY_DOT, POST_CURRENCY_INTEGER,
|
|
40
|
+
FULL_COMMA_DECIMAL, FULL_DOT_DECIMAL, DASH_COMMA_DECIMAL, DASH_DOT_DECIMAL, COMMA_DECIMAL, DOT_DECIMAL, INTEGER,
|
|
41
|
+
LITERAL
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
attr_accessor :money, :title, :unit
|
|
45
|
+
|
|
46
|
+
def self.find_match(text, options = {})
|
|
47
|
+
options.reverse_merge!(eop: EOP, bop: BOP)
|
|
48
|
+
PRICES.each do |regex|
|
|
49
|
+
return $~ if text.scan(/#{options[:bop]}#{regex}#{options[:eop]}/ui).present?
|
|
50
|
+
end
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.parse_line(line)
|
|
55
|
+
match = find_match(line)
|
|
56
|
+
if match
|
|
57
|
+
new(match.to_s, match.pre_match, match.post_match)
|
|
58
|
+
else
|
|
59
|
+
new(nil, line)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def initialize(money, title = nil, unit = nil)
|
|
64
|
+
# In case this is literal price, we want to set it as title of price and not money
|
|
65
|
+
if money =~ LITERAL
|
|
66
|
+
title = "#{title}#{money}#{unit}"
|
|
67
|
+
money = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@money, @title, @unit = money.presence, title.presence, unit.presence
|
|
71
|
+
# Replace .- with .00
|
|
72
|
+
@money.gsub!(/([\.,])#{DASH}/, "\\100") if @money
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
#
|
|
3
|
+
# MenuMarkup::Parser parses our superspecial menu markup into Entries (Items and Sections).
|
|
4
|
+
#
|
|
5
|
+
# Maybe use one of these:
|
|
6
|
+
# http://stackoverflow.com/questions/3454047/ruby-markup-parser-for-custom-markup-language
|
|
7
|
+
#
|
|
8
|
+
# Example:
|
|
9
|
+
# parser = MenuMarkup::MenuMarkupParser.new
|
|
10
|
+
# result = parser.parse(File.read('spec/support/menu5.txt')).elements
|
|
11
|
+
#
|
|
12
|
+
# TODO:
|
|
13
|
+
# * Better implementation
|
|
14
|
+
# * Support 'vanaf', 'v.a.', 'per persoon', '2 personen', 'p.p.', ..
|
|
15
|
+
# * Recognize best price. Eg. wines often use Blablawine 2003 (year) which is recognized as Price. We should prefer 4,50 (comma seperated) above this kind of values.
|
|
16
|
+
#
|
|
17
|
+
|
|
18
|
+
Treetop.load(File.join(__dir__, 'menu_markup')) # MenuMarkupParser is created by Treetop
|
|
19
|
+
|
|
20
|
+
class MenuMarkupParser
|
|
21
|
+
class Node < Treetop::Runtime::SyntaxNode
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class Section < Node
|
|
25
|
+
attr_reader :prices, :title, :description_lines
|
|
26
|
+
|
|
27
|
+
def description
|
|
28
|
+
description_lines.join("\n")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def restrictions
|
|
32
|
+
attributes_node.respond_to?(:attributes) ? attributes_node.attributes.restrictions : []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def spicy
|
|
36
|
+
attributes_node.respond_to?(:attributes) ? attributes_node.attributes.spicy : nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_elements!
|
|
40
|
+
@description_lines = description_node.elements.collect { |node| node.text.text_value.strip }
|
|
41
|
+
@prices = parse_prices!
|
|
42
|
+
@title = @description_lines.shift
|
|
43
|
+
|
|
44
|
+
children.each { |node| node.parse_elements! }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def choice?
|
|
48
|
+
choice_node.text_value.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def children
|
|
52
|
+
children_nodes.elements
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def parse_prices!
|
|
58
|
+
parse_explicit_prices
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_explicit_prices
|
|
62
|
+
price_lines, @description_lines = @description_lines.partition { |line| line.start_with?('=') }
|
|
63
|
+
price_lines.collect { |price_line| MenuMarkup::ParsedPrice.parse_line(price_line[1..-1]) }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Item < Section
|
|
68
|
+
def children
|
|
69
|
+
[]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def parse_prices!
|
|
75
|
+
parse_explicit_prices.presence || parse_price_line.presence || parse_inline_price
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_price_line
|
|
79
|
+
new_description = description
|
|
80
|
+
match = MenuMarkup::ParsedPrice.find_match(new_description, eop: /(?=$)/ui) || MenuMarkup::ParsedPrice.find_match(new_description, bop: /(?<=^)/ui)
|
|
81
|
+
|
|
82
|
+
if match
|
|
83
|
+
new_description.slice!(match.begin(0)...match.end(0))
|
|
84
|
+
@description_lines = new_description.lines.collect(&:strip).reject(&:blank?)
|
|
85
|
+
[MenuMarkup::ParsedPrice.new(match.to_s)]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_inline_price
|
|
90
|
+
match = MenuMarkup::ParsedPrice.find_match(description)
|
|
91
|
+
if match
|
|
92
|
+
[MenuMarkup::ParsedPrice.new(match.to_s)]
|
|
93
|
+
else
|
|
94
|
+
[MenuMarkup::ParsedPrice.new(nil)]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Attributes < Node
|
|
100
|
+
SPICY_MAP = {
|
|
101
|
+
'n' => 'none',
|
|
102
|
+
'm' => 'mild',
|
|
103
|
+
'M' => 'medium',
|
|
104
|
+
'H' => 'hot', # h = halal!
|
|
105
|
+
}
|
|
106
|
+
RESTRICTIONS_MAP = {
|
|
107
|
+
'V' => 'vegan',
|
|
108
|
+
'v' => 'vegetarian',
|
|
109
|
+
'k' => 'kosher',
|
|
110
|
+
'h' => 'halal'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def restrictions
|
|
114
|
+
map(RESTRICTIONS_MAP)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def spicy
|
|
118
|
+
map(SPICY_MAP).first
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def map(map)
|
|
124
|
+
map.values_at(*text_value.chars).compact
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse(text)
|
|
129
|
+
super(text).tap { |result| result.menu.elements.each(&:parse_elements!) if result }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
end
|
data/menu_markup.gemspec
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'menu_markup/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |spec|
|
|
7
|
+
spec.name = "menu_markup"
|
|
8
|
+
spec.version = MenuMarkup::VERSION
|
|
9
|
+
spec.authors = ["Joost Hietbrink"]
|
|
10
|
+
spec.email = ["joost@webuildinternet.com"]
|
|
11
|
+
spec.license = "MIT"
|
|
12
|
+
|
|
13
|
+
spec.summary = %q{Ruby gem to parce MenuMarkup.}
|
|
14
|
+
spec.description = %q{MenuMarkup is a super simple markup to specify menu data in plain text. When the MenuMarkup is parsed it creates a Menu. A Menu consists of two types: Items and Sections. Items have multiple Prices.}
|
|
15
|
+
spec.homepage = "http://www.webuildinternet.com/2012/07/04/menu-markup-specification/"
|
|
16
|
+
|
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
|
19
|
+
# if spec.respond_to?(:metadata)
|
|
20
|
+
# spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
|
|
21
|
+
# else
|
|
22
|
+
# raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
|
23
|
+
# end
|
|
24
|
+
|
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
|
26
|
+
spec.bindir = "exe"
|
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
28
|
+
spec.require_paths = ["lib"]
|
|
29
|
+
|
|
30
|
+
spec.add_dependency "treetop", '~> 1.4', ">= 1.4.15"
|
|
31
|
+
spec.add_dependency "activesupport", '~> 4' # for #presence method
|
|
32
|
+
spec.add_development_dependency "bundler", '~> 1.9'
|
|
33
|
+
spec.add_development_dependency "rake", "~> 10.4"
|
|
34
|
+
spec.add_development_dependency "rspec", "~> 3.3"
|
|
35
|
+
spec.add_development_dependency "rspec-collection_matchers", '~> 1.1', '>= 1.1.2'
|
|
36
|
+
spec.add_development_dependency "guard", "~> 2.12"
|
|
37
|
+
spec.add_development_dependency "guard-rspec", "~> 4.6"
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: menu_markup
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Joost Hietbrink
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2015-07-21 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: treetop
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.4'
|
|
20
|
+
- - ">="
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: 1.4.15
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - "~>"
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.4'
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 1.4.15
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: activesupport
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '4'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '4'
|
|
47
|
+
- !ruby/object:Gem::Dependency
|
|
48
|
+
name: bundler
|
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.9'
|
|
54
|
+
type: :development
|
|
55
|
+
prerelease: false
|
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.9'
|
|
61
|
+
- !ruby/object:Gem::Dependency
|
|
62
|
+
name: rake
|
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '10.4'
|
|
68
|
+
type: :development
|
|
69
|
+
prerelease: false
|
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '10.4'
|
|
75
|
+
- !ruby/object:Gem::Dependency
|
|
76
|
+
name: rspec
|
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '3.3'
|
|
82
|
+
type: :development
|
|
83
|
+
prerelease: false
|
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.3'
|
|
89
|
+
- !ruby/object:Gem::Dependency
|
|
90
|
+
name: rspec-collection_matchers
|
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '1.1'
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: 1.1.2
|
|
99
|
+
type: :development
|
|
100
|
+
prerelease: false
|
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
102
|
+
requirements:
|
|
103
|
+
- - "~>"
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
version: '1.1'
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: 1.1.2
|
|
109
|
+
- !ruby/object:Gem::Dependency
|
|
110
|
+
name: guard
|
|
111
|
+
requirement: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '2.12'
|
|
116
|
+
type: :development
|
|
117
|
+
prerelease: false
|
|
118
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '2.12'
|
|
123
|
+
- !ruby/object:Gem::Dependency
|
|
124
|
+
name: guard-rspec
|
|
125
|
+
requirement: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '4.6'
|
|
130
|
+
type: :development
|
|
131
|
+
prerelease: false
|
|
132
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '4.6'
|
|
137
|
+
description: 'MenuMarkup is a super simple markup to specify menu data in plain text.
|
|
138
|
+
When the MenuMarkup is parsed it creates a Menu. A Menu consists of two types: Items
|
|
139
|
+
and Sections. Items have multiple Prices.'
|
|
140
|
+
email:
|
|
141
|
+
- joost@webuildinternet.com
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- ".bundle/config"
|
|
147
|
+
- ".gitignore"
|
|
148
|
+
- ".travis.yml"
|
|
149
|
+
- Gemfile
|
|
150
|
+
- Gemfile.lock
|
|
151
|
+
- Guardfile
|
|
152
|
+
- README.md
|
|
153
|
+
- Rakefile
|
|
154
|
+
- bin/console
|
|
155
|
+
- bin/setup
|
|
156
|
+
- lib/menu_markup.rb
|
|
157
|
+
- lib/menu_markup.treetop
|
|
158
|
+
- lib/menu_markup/parsed_price.rb
|
|
159
|
+
- lib/menu_markup/version.rb
|
|
160
|
+
- lib/menu_markup_parser.rb
|
|
161
|
+
- menu_markup.gemspec
|
|
162
|
+
homepage: http://www.webuildinternet.com/2012/07/04/menu-markup-specification/
|
|
163
|
+
licenses:
|
|
164
|
+
- MIT
|
|
165
|
+
metadata: {}
|
|
166
|
+
post_install_message:
|
|
167
|
+
rdoc_options: []
|
|
168
|
+
require_paths:
|
|
169
|
+
- lib
|
|
170
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
171
|
+
requirements:
|
|
172
|
+
- - ">="
|
|
173
|
+
- !ruby/object:Gem::Version
|
|
174
|
+
version: '0'
|
|
175
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
176
|
+
requirements:
|
|
177
|
+
- - ">="
|
|
178
|
+
- !ruby/object:Gem::Version
|
|
179
|
+
version: '0'
|
|
180
|
+
requirements: []
|
|
181
|
+
rubyforge_project:
|
|
182
|
+
rubygems_version: 2.4.5
|
|
183
|
+
signing_key:
|
|
184
|
+
specification_version: 4
|
|
185
|
+
summary: Ruby gem to parce MenuMarkup.
|
|
186
|
+
test_files: []
|