murlsh 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/.htaccess +1 -0
  2. data/README.textile +6 -12
  3. data/Rakefile +19 -17
  4. data/VERSION +1 -1
  5. data/bin/murlsh +2 -35
  6. data/config.ru +1 -1
  7. data/config.yaml +9 -9
  8. data/lib/murlsh/ask.rb +15 -0
  9. data/lib/murlsh/cp_r_safe.rb +33 -0
  10. data/lib/murlsh/doc.rb +1 -16
  11. data/lib/murlsh/install.rb +28 -0
  12. data/lib/murlsh/plugin.rb +0 -2
  13. data/lib/murlsh/search_conditions.rb +12 -2
  14. data/lib/murlsh/search_grammar.rb +225 -0
  15. data/lib/murlsh/search_grammar.treetop +34 -0
  16. data/lib/murlsh/uri_ask.rb +10 -14
  17. data/lib/murlsh/url_body.rb +33 -19
  18. data/lib/murlsh/url_result_set.rb +12 -2
  19. data/lib/murlsh.rb +4 -1
  20. data/murlsh.gemspec +26 -21
  21. data/plugins/add_post_50_update_m3u.rb +35 -0
  22. data/plugins/{add_pre_41_unajax_twitter.rb → add_pre_30_unajax_twitter.rb} +1 -1
  23. data/plugins/add_pre_35_url_clean.rb +21 -0
  24. data/plugins/add_pre_50_media_thumbnail.rb +6 -3
  25. data/plugins/add_pre_65_html_thumb.rb +1 -3
  26. data/plugins/url_display_add_60_via.rb +4 -1
  27. data/public/css/jquery.jgrowl.css +11 -7
  28. data/public/css/screen.css +3 -2
  29. data/public/js/jquery-1.5.min.js +16 -0
  30. data/public/js/jquery.jgrowl_compressed.js +40 -32
  31. data/public/js/js.js +3 -43
  32. data/public/js/{twitter-text-1.0.4.js → twitter-text-1.3.1.js} +25 -22
  33. data/spec/doc_spec.rb +10 -4
  34. metadata +75 -61
  35. data/lib/murlsh/sqlite3_adapter.rb +0 -13
  36. data/plugins/add_pre_60_flickr.rb +0 -27
  37. data/plugins/add_pre_60_vimeo.rb +0 -38
  38. data/plugins/html_parse_50_nokogiri.rb +0 -16
  39. data/public/js/comments.json +0 -1
  40. data/public/js/jquery-1.4.4.min.js +0 -167
data/.htaccess CHANGED
@@ -5,6 +5,7 @@ AddOutputFilterByType DEFLATE application/javascript
5
5
  AddOutputFilterByType DEFLATE application/rss+xml
6
6
  AddOutputFilterByType DEFLATE application/xhtml+xml
7
7
  AddOutputFilterByType DEFLATE application/xml
8
+ AddOutputFilterByType DEFLATE audio/x-mpegurl
8
9
  AddOutputFilterByType DEFLATE text/css
9
10
  AddOutputFilterByType DEFLATE text/html
10
11
 
data/README.textile CHANGED
@@ -1,11 +1,12 @@
1
- Site for sharing and archiving links.
1
+ Host your bookmarks or maintain a link blog.
2
2
 
3
- * fetches url titles and generates thumbnails
4
- * jGrowls embedded versions of Imageshack, Vimeo and YouTube urls
3
+ * fetches url titles and generates thumbnails from page
4
+ * jGrowls embedded versions of Imageshack, Imgur, Vimeo and YouTube urls
5
5
  * converts Twitter status urls to their full text and adds user thumbnail
6
6
  * generates Atom and RSS feeds
7
+ * generates podcast RSS feed and m3u file for all audio urls
7
8
  * generates json and jsonp feeds for client-side inclusion in other sites
8
- * regex search
9
+ * search
9
10
  * uses HTML5 audio for mp3 and ogg urls
10
11
  * looks good on iPhone
11
12
  * PubSubHubbub notification
@@ -55,6 +56,7 @@ h2. Recent urls
55
56
  * http://your_root/atom.atom
56
57
  * http://your_root/rss.rss
57
58
  * http://your_root/podcast.rss (urls with audio/mpeg content type)
59
+ * http://your_root/m3u.m3u (urls with audio content types)
58
60
  * http://your_root/json.json
59
61
  * http://your_root/json.json?callback=x (jsonp)
60
62
 
@@ -77,7 +79,6 @@ Plugin hooks
77
79
  |add_pre|called before a new url is saved|url, config hash|undefined|
78
80
  |add_post|called after a new url is saved|url, config hash|undefined|
79
81
  |avatar|called to get an avatar url from an email md5 sum|avatar url, url, config hash|avatar url|
80
- |html_parse|parse HTML using something like Hpricot or Nokogiri|parseable|parsed HTML, only first plugin is run (cannot be chained)|
81
82
  |url_display_add|called to display additional information after urls|markup builder, url, config hash|undefined|
82
83
 
83
84
  h1. PubSubHubbub
@@ -99,11 +100,4 @@ subscribe_url is what gets put in the feed as link rel="hub"
99
100
 
100
101
  This will make updates to your feed show up in Google Reader instantly.
101
102
 
102
- h1. Design Goals
103
-
104
- * low effort required to add a url, get metadata programatically instead of requiring user to specify
105
- * allow customization with config and plugins
106
- * full regex search for finding saved urls
107
- * simple security (no sessions, no cookies)
108
-
109
103
  Questions and comments: "matthewm@boedicker.org":mailto:matthewm@boedicker.org
data/Rakefile CHANGED
@@ -51,7 +51,7 @@ namespace :db do
51
51
 
52
52
  last = Murlsh::Url.find(:last, :order => 'time')
53
53
  pp last
54
- response = ask('Delete this url', '?')
54
+ response = Murlsh.ask('Delete this url?', 'n')
55
55
  last.destroy if %w{y yes}.include?(response.downcase)
56
56
  end
57
57
 
@@ -98,9 +98,9 @@ namespace :db do
98
98
  ActiveRecord::Base.establish_connection(:adapter => 'sqlite3',
99
99
  :database => config.fetch('db_file'))
100
100
 
101
- Murlsh::Url.all(:conditions => [
102
- 'MURLSHMATCH(title, :search) OR MURLSHMATCH(url, :search)',
103
- { :search => args.search }]).each do |url|
101
+ like = "%#{args.search}%"
102
+ Murlsh::Url.all(:conditions =>
103
+ ['title LIKE ? OR url LIKE ?', like, like]).each do |url|
104
104
  puts "#{url.id} #{url.url} #{url.title}"
105
105
  end
106
106
  end
@@ -164,9 +164,9 @@ namespace :user do
164
164
  desc 'Add a new user.'
165
165
  task :add do
166
166
  puts "adding to #{config.fetch('auth_file')}"
167
- username = ask(:username)
168
- email = ask(:email)
169
- password = ask(:password)
167
+ username = Murlsh.ask('Username:')
168
+ email = Murlsh.ask('Email:')
169
+ password = Murlsh.ask('Password:')
170
170
 
171
171
  Murlsh::Auth.new(config.fetch('auth_file')).add_user(username, email,
172
172
  password)
@@ -270,6 +270,9 @@ namespace :css do
270
270
  unless config['css_compressed'] == compressed_url
271
271
  config['css_compressed'] = compressed_url
272
272
  config.extend(Murlsh::YamlOrderedHash)
273
+ config.each_value do |v|
274
+ v.extend(Murlsh::YamlOrderedHash) if v.is_a?(Hash)
275
+ end
273
276
  open('config.yaml', 'w') { |f| YAML.dump(config, f) }
274
277
  puts "updated config with css_compressed = #{compressed_url}"
275
278
  end
@@ -309,6 +312,9 @@ namespace :js do
309
312
  unless config['js_compressed'] == compressed_url
310
313
  config['js_compressed'] = compressed_url
311
314
  config.extend(Murlsh::YamlOrderedHash)
315
+ config.each_value do |v|
316
+ v.extend(Murlsh::YamlOrderedHash) if v.is_a?(Hash)
317
+ end
312
318
  open('config.yaml', 'w') { |f| YAML.dump(config, f) }
313
319
  puts "updated config with js_compressed = #{compressed_url}"
314
320
  end
@@ -416,17 +422,12 @@ EOS
416
422
 
417
423
  end
418
424
 
419
- def ask(prompt, sep=':')
420
- print "#{prompt}#{sep} "
421
- STDIN.gets.chomp
422
- end
423
-
424
425
  begin
425
426
  require 'jeweler'
426
427
  Jeweler::Tasks.new do |gemspec|
427
428
  gemspec.name = 'murlsh'
428
- gemspec.summary = 'url sharing site framework'
429
- gemspec.description = 'url sharing site framework with easy adding, title lookup, atom feed, thumbnails and embedding'
429
+ gemspec.summary = 'Host your bookmarks or maintain a link blog'
430
+ gemspec.description = 'Host your bookmarks or maintain a link blog'
430
431
  gemspec.email = 'matthewm@boedicker.org'
431
432
  gemspec.homepage = 'http://github.com/mmb/murlsh'
432
433
  gemspec.authors = ['Matthew M. Boedicker']
@@ -439,11 +440,11 @@ begin
439
440
  activerecord >= 2.3.4
440
441
  bcrypt-ruby >= 2.1.2
441
442
  builder >= 2.1.2
442
- flickraw >= 0.8.3
443
443
  htmlentities >= 4.2.0
444
444
  json >= 1.2.3
445
445
  nokogiri ~> 1.0
446
446
  plumnailer >= 0.1.0
447
+ postrank-uri ~> 1.0
447
448
  public_suffix_service ~> 0.0
448
449
  push-notify >= 0.1.0
449
450
  rack >= 1.0.0
@@ -451,12 +452,13 @@ begin
451
452
  rack-rewrite >= 1.0.2
452
453
  rack-throttle >= 0.3.0
453
454
  rmagick >= 1.15.14
454
- sqlite3-ruby >= 1.2.1
455
+ sqlite3 ~> 1.3
455
456
  tinyatom >= 0.3.3
457
+ treetop ~> 1.4
456
458
  twitter >= 0.9.12
457
- vimeo >= 1.2.2
458
459
  }.each_slice(3) { |g,o,v| gemspec.add_dependency(g, "#{o} #{v}") }
459
460
  %w{
461
+ fakeweb ~> 1.3
460
462
  flog >= 2.5.0
461
463
  rack-test ~> 0.5
462
464
  rspec ~> 2.0
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.1
1
+ 1.4.0
data/bin/murlsh CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'fileutils'
3
+ require 'murlsh'
4
4
 
5
5
  def usage
6
6
  puts <<-eos
@@ -11,44 +11,11 @@ usage: murlsh DESTINATION_DIRECTORY
11
11
  eos
12
12
  end
13
13
 
14
- def ask(prompt, default)
15
- print "#{prompt} [#{default}] "
16
- answer = $stdin.gets.strip
17
- answer = default if answer.empty?
18
- answer
19
- end
20
-
21
- def cp_r_safe(sources, dest, options)
22
- sources.each do |source|
23
- new = File.join(dest, File.split(File.expand_path(source)).last)
24
-
25
- if File.directory?(source)
26
- FileUtils.mkdir_p(new, options)
27
- cp_r_safe(Dir.entries(source).
28
- reject { |f| %w{. ..}.include?(f) }.
29
- map { |f| File.join(source, f) }, new, options)
30
- else
31
- answer = if File.exists?(new)
32
- ask("#{new} exists. Overwrite?", 'n')
33
- else
34
- 'y'
35
- end
36
-
37
- FileUtils.copy(source, new, options) if answer == 'y'
38
- end
39
- end
40
-
41
- end
42
-
43
14
  if ARGV.empty?; usage; exit 1; end
44
15
 
45
16
  dest_dir = ARGV[0]
46
17
 
47
- cp_r_safe(
48
- %w{.htaccess config.ru config.yaml plugins/ public/ Rakefile}.map { |x|
49
- File.join(File.dirname(__FILE__), '..', x) }, dest_dir, :verbose => true)
50
-
51
- FileUtils.mkdir_p('tmp', :verbose => true)
18
+ Murlsh.install(dest_dir)
52
19
 
53
20
  cd_command = File.expand_path(dest_dir) == Dir.pwd ? '' : "cd #{dest_dir}\n"
54
21
 
data/config.ru CHANGED
@@ -32,7 +32,7 @@ feed_url = URI.join(config.fetch('root_url'), config.fetch('feed_file'))
32
32
  use Murlsh::MustRevalidate, :patterns => %r{^#{Regexp.escape(feed_url.path)}$}
33
33
 
34
34
  use Rack::Static, :urls => %w{/css/ /img/ /js/}, :root => 'public'
35
- use Rack::Static, :urls => %w{/atom.atom /podcast.rss /rss.rss}
35
+ use Rack::Static, :urls => %w{/atom.atom /m3u.m3u /podcast.rss /rss.rss}
36
36
 
37
37
  use Rack::Rewrite do
38
38
  r301 '/atom.xml', feed_url.to_s
data/config.yaml CHANGED
@@ -7,12 +7,11 @@ css_files:
7
7
  - css/screen.css
8
8
  db_file: murlsh.db
9
9
  feed_file: atom.atom
10
- flickr_api_key:
11
10
  gravatar_size: 32
12
11
  js_files:
13
- - js/jquery-1.4.4.min.js
12
+ - js/jquery-1.5.min.js
14
13
  - js/jquery.jgrowl_compressed.js
15
- - js/twitter-text-1.0.4.js
14
+ - js/twitter-text-1.3.1.js
16
15
  - js/js.js
17
16
  meta_tag_description: URLs found interesting by Matthew M. Boedicker
18
17
  meta_tag_verify-v1:
@@ -22,13 +21,14 @@ num_posts_page: 25
22
21
  page_title: mmb url share
23
22
  pubsubhubbub_hubs: []
24
23
 
24
+ quick_search:
25
+ audio: .mp3 .ogg
26
+ code: code.google.com github.com
27
+ images: .png .gif .jpeg .jpg
28
+ tweets: twitter.com
29
+ video: hulu.com vimeo.com www.ted.com youtube.com
30
+ wikipedia: wikipedia.org
25
31
  root_url: http://urls.matthewm.boedicker.org/
26
32
  show_names: true
27
33
  thumbnail_max_side: 90
28
34
  user_agent: murlsh (http://github.com/mmb/murlsh)
29
- predefined_searches:
30
- audio: \.(mp3|ogg)$
31
- code: (code\.google|github)\.com/
32
- images: \.(png|gif|jpe?g)$
33
- tweets: twitter\.com/
34
- video: (youtube|vimeo)\.com/
data/lib/murlsh/ask.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Murlsh
2
+
3
+ module_function
4
+
5
+ # Ask the user a question and return the answer.
6
+ def ask(prompt, default=nil)
7
+ default_given = !default.to_s.empty?
8
+ print "#{prompt} "
9
+ print "[#{default}] " if default_given
10
+ answer = $stdin.gets.strip
11
+ answer = default if answer.empty? and default_given
12
+ answer
13
+ end
14
+
15
+ end
@@ -0,0 +1,33 @@
1
+ require 'fileutils'
2
+
3
+ require 'murlsh'
4
+
5
+ module Murlsh
6
+
7
+ module_function
8
+
9
+ # Recursive copy from sources to destination but ask before overwriting.
10
+ #
11
+ # Options are passed into FileUtils.mkdir_p FileUtils.copy.
12
+ def cp_r_safe(sources, dest, options)
13
+ sources.each do |source|
14
+ new = File.join(dest, File.split(File.expand_path(source)).last)
15
+
16
+ if File.directory?(source)
17
+ FileUtils.mkdir_p(new, options)
18
+ cp_r_safe(Dir.entries(source).
19
+ reject { |f| %w{. ..}.include?(f) }.
20
+ map { |f| File.join(source, f) }, new, options)
21
+ else
22
+ answer = if File.exists?(new)
23
+ Murlsh.ask("#{new} exists. Overwrite?", 'n')
24
+ else
25
+ 'y'
26
+ end
27
+
28
+ FileUtils.copy(source, new, options) if answer == 'y'
29
+ end
30
+ end
31
+ end
32
+
33
+ end
data/lib/murlsh/doc.rb CHANGED
@@ -1,23 +1,8 @@
1
1
  module Murlsh
2
2
 
3
- # Nokogiri / Hpricot doc mixin.
3
+ # Nokogiri doc mixin.
4
4
  module Doc
5
5
 
6
- # Get the character set of the document.
7
- def charset
8
- %w{content-type Content-Type}.each do |ct|
9
- content_type = at("meta[@http-equiv='#{ct}']")
10
- unless content_type.nil?
11
- content = content_type['content']
12
- unless content.nil?
13
- charset = content[/charset=([\w.:-]+)/, 1]
14
- return charset if charset
15
- end
16
- end
17
- end
18
- nil
19
- end
20
-
21
6
  # Check a list of xpaths in order and yield and return the node matching
22
7
  # the first one that is not nil
23
8
  def xpath_search(xpaths)
@@ -0,0 +1,28 @@
1
+ require 'fileutils'
2
+
3
+ require 'murlsh'
4
+
5
+ module Murlsh
6
+
7
+ MurlshRoot = File.join(File.dirname(__FILE__), '..', '..')
8
+
9
+ module_function
10
+
11
+ # Install a murlsh site to a web directory.
12
+ #
13
+ # Copies files that are different per-site to make a site instance.
14
+ def install(dest_dir)
15
+ Murlsh.cp_r_safe(
16
+ %w{
17
+ .htaccess
18
+ Rakefile
19
+ config.ru
20
+ config.yaml
21
+ plugins/
22
+ public/
23
+ }.map { |x| File.join(MurlshRoot, x) }, dest_dir, :verbose => true)
24
+
25
+ FileUtils.mkdir_p(File.join(dest_dir, 'tmp'), :verbose => true)
26
+ end
27
+
28
+ end
data/lib/murlsh/plugin.rb CHANGED
@@ -9,8 +9,6 @@ module Murlsh
9
9
  # run arguments (config hash)
10
10
  # * avatar - called to get an avatar url from an email md5 sum
11
11
  # run arguments (avatar url, url, config hash)
12
- # * html_parse - called to parse HTML using something like Hpricot or Nokogiri
13
- # run arguments (parseable)
14
12
  # * url_display_add - called to display additional information after urls
15
13
  # run arguments (markup builder, url, config hash)
16
14
  class Plugin
@@ -1,3 +1,5 @@
1
+ require 'treetop'
2
+
1
3
  module Murlsh
2
4
 
3
5
  # Search conditions builder for ActiveRecord conditions.
@@ -8,9 +10,17 @@ module Murlsh
8
10
  # Search conditions builder for ActiveRecord conditions.
9
11
  def conditions
10
12
  if q
13
+ parser = Murlsh::SearchGrammarParser.new
14
+ tokens = parser.parse(q).content
11
15
  search_cols = %w{name title url}
12
- [search_cols.map { |x| "MURLSHMATCH(#{x}, ?)" }.join(' OR ')].push(
13
- *[q] * search_cols.size)
16
+
17
+ likes = []
18
+ params = []
19
+ search_cols.product(tokens).each do |col,tok|
20
+ likes << "#{col} LIKE ?"
21
+ params << "%#{tok}%"
22
+ end
23
+ [likes.join(' OR ')].push(*params)
14
24
  else
15
25
  []
16
26
  end
@@ -0,0 +1,225 @@
1
+ # Autogenerated from a Treetop grammar. Edits may be lost.
2
+
3
+
4
+ module Murlsh
5
+
6
+ module SearchGrammar
7
+ include Treetop::Runtime
8
+
9
+ def root
10
+ @root ||= :query
11
+ end
12
+
13
+ module Query0
14
+ def content; elements.map { |e| e.content }.compact.uniq; end
15
+ end
16
+
17
+ def _nt_query
18
+ start_index = index
19
+ if node_cache[:query].has_key?(index)
20
+ cached = node_cache[:query][index]
21
+ if cached
22
+ cached = SyntaxNode.new(input, index...(index + 1)) if cached == true
23
+ @index = cached.interval.end
24
+ end
25
+ return cached
26
+ end
27
+
28
+ s0, i0 = [], index
29
+ loop do
30
+ i1 = index
31
+ r2 = _nt_quoted_string
32
+ if r2
33
+ r1 = r2
34
+ else
35
+ r3 = _nt_whitespace
36
+ if r3
37
+ r1 = r3
38
+ else
39
+ r4 = _nt_string
40
+ if r4
41
+ r1 = r4
42
+ else
43
+ @index = i1
44
+ r1 = nil
45
+ end
46
+ end
47
+ end
48
+ if r1
49
+ s0 << r1
50
+ else
51
+ break
52
+ end
53
+ end
54
+ r0 = instantiate_node(SyntaxNode,input, i0...index, s0)
55
+ r0.extend(Query0)
56
+
57
+ node_cache[:query][start_index] = r0
58
+
59
+ r0
60
+ end
61
+
62
+ module QuotedString0
63
+ end
64
+
65
+ module QuotedString1
66
+ def content
67
+ result = text_value[1..-2]
68
+ result.empty? ? nil : result
69
+ end
70
+ end
71
+
72
+ def _nt_quoted_string
73
+ start_index = index
74
+ if node_cache[:quoted_string].has_key?(index)
75
+ cached = node_cache[:quoted_string][index]
76
+ if cached
77
+ cached = SyntaxNode.new(input, index...(index + 1)) if cached == true
78
+ @index = cached.interval.end
79
+ end
80
+ return cached
81
+ end
82
+
83
+ i0, s0 = index, []
84
+ if has_terminal?('"', false, index)
85
+ r1 = instantiate_node(SyntaxNode,input, index...(index + 1))
86
+ @index += 1
87
+ else
88
+ terminal_parse_failure('"')
89
+ r1 = nil
90
+ end
91
+ s0 << r1
92
+ if r1
93
+ s2, i2 = [], index
94
+ loop do
95
+ if has_terminal?('\G[^"]', true, index)
96
+ r3 = true
97
+ @index += 1
98
+ else
99
+ r3 = nil
100
+ end
101
+ if r3
102
+ s2 << r3
103
+ else
104
+ break
105
+ end
106
+ end
107
+ r2 = instantiate_node(SyntaxNode,input, i2...index, s2)
108
+ s0 << r2
109
+ if r2
110
+ if has_terminal?('"', false, index)
111
+ r4 = instantiate_node(SyntaxNode,input, index...(index + 1))
112
+ @index += 1
113
+ else
114
+ terminal_parse_failure('"')
115
+ r4 = nil
116
+ end
117
+ s0 << r4
118
+ end
119
+ end
120
+ if s0.last
121
+ r0 = instantiate_node(SyntaxNode,input, i0...index, s0)
122
+ r0.extend(QuotedString0)
123
+ r0.extend(QuotedString1)
124
+ else
125
+ @index = i0
126
+ r0 = nil
127
+ end
128
+
129
+ node_cache[:quoted_string][start_index] = r0
130
+
131
+ r0
132
+ end
133
+
134
+ module Whitespace0
135
+ def content; end;
136
+ end
137
+
138
+ def _nt_whitespace
139
+ start_index = index
140
+ if node_cache[:whitespace].has_key?(index)
141
+ cached = node_cache[:whitespace][index]
142
+ if cached
143
+ cached = SyntaxNode.new(input, index...(index + 1)) if cached == true
144
+ @index = cached.interval.end
145
+ end
146
+ return cached
147
+ end
148
+
149
+ s0, i0 = [], index
150
+ loop do
151
+ if has_terminal?('\G[\\s]', true, index)
152
+ r1 = true
153
+ @index += 1
154
+ else
155
+ r1 = nil
156
+ end
157
+ if r1
158
+ s0 << r1
159
+ else
160
+ break
161
+ end
162
+ end
163
+ if s0.empty?
164
+ @index = i0
165
+ r0 = nil
166
+ else
167
+ r0 = instantiate_node(SyntaxNode,input, i0...index, s0)
168
+ r0.extend(Whitespace0)
169
+ end
170
+
171
+ node_cache[:whitespace][start_index] = r0
172
+
173
+ r0
174
+ end
175
+
176
+ module String0
177
+ def content; text_value; end
178
+ end
179
+
180
+ def _nt_string
181
+ start_index = index
182
+ if node_cache[:string].has_key?(index)
183
+ cached = node_cache[:string][index]
184
+ if cached
185
+ cached = SyntaxNode.new(input, index...(index + 1)) if cached == true
186
+ @index = cached.interval.end
187
+ end
188
+ return cached
189
+ end
190
+
191
+ s0, i0 = [], index
192
+ loop do
193
+ if has_terminal?('\G[^\\s]', true, index)
194
+ r1 = true
195
+ @index += 1
196
+ else
197
+ r1 = nil
198
+ end
199
+ if r1
200
+ s0 << r1
201
+ else
202
+ break
203
+ end
204
+ end
205
+ if s0.empty?
206
+ @index = i0
207
+ r0 = nil
208
+ else
209
+ r0 = instantiate_node(SyntaxNode,input, i0...index, s0)
210
+ r0.extend(String0)
211
+ end
212
+
213
+ node_cache[:string][start_index] = r0
214
+
215
+ r0
216
+ end
217
+
218
+ end
219
+
220
+ class SearchGrammarParser < Treetop::Runtime::CompiledParser
221
+ include SearchGrammar
222
+ end
223
+
224
+
225
+ end
@@ -0,0 +1,34 @@
1
+ module Murlsh
2
+
3
+ grammar SearchGrammar
4
+
5
+ rule query
6
+ (quoted_string / whitespace / string)* {
7
+ def content; elements.map { |e| e.content }.compact.uniq; end
8
+ }
9
+ end
10
+
11
+ rule quoted_string
12
+ '"' [^"]* '"' {
13
+ def content
14
+ result = text_value[1..-2]
15
+ result.empty? ? nil : result
16
+ end
17
+ }
18
+ end
19
+
20
+ rule whitespace
21
+ [\s]+ {
22
+ def content; end;
23
+ }
24
+ end
25
+
26
+ rule string
27
+ [^\s]+ {
28
+ def content; text_value; end
29
+ }
30
+ end
31
+
32
+ end
33
+
34
+ end