rsolr 0.9.6 → 0.9.7.1

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.
data/CHANGES.txt CHANGED
@@ -1,3 +1,18 @@
1
+ 0.9.7.1 - November 5, 2009
2
+ added support for multibyte url characters in RSolr::Connection::Utils
3
+ - this is because Ruby 1.9's String size method is different than 1.8
4
+
5
+ 0.9.7 - October 23, 2009
6
+ Removed XML message builders - using pure Ruby generator instead
7
+ - benchmarks show generation speed is a little faster than libxml
8
+ - minimal xml escaping so binary posting to Solr should no longer be a problem.
9
+ Changed response.adapter_resonse to response.raw
10
+ Removed HTTP adapters - sticking with NetHTTP
11
+ Removed builder dependency in gemspec
12
+ Removed Adapter and HTTPClient modules
13
+ Moved HTTPCLient::Util to Connection::Utils
14
+ Updated all tests
15
+
1
16
  0.9.6 - September 9, 2009
2
17
  Added ability to create direct connections from existing Java::OrgApacheSolrCore::SolrCore
3
18
  Added ability to send queries using POST
data/README.rdoc CHANGED
@@ -3,8 +3,8 @@
3
3
  A Ruby client for Apache Solr. RSolr has been developed to be simple and extendable. It features transparent JRuby DirectSolrConnection support and a simple Hash-in, Hash-out architecture.
4
4
 
5
5
  == Installation:
6
- gem sources -a http://gems.github.com
7
- sudo gem install mwmitchell-rsolr
6
+ gem sources -a http://gemcutter.org
7
+ sudo gem install rsolr
8
8
 
9
9
  ==Related Resources & Projects
10
10
  * {Solr}[http://lucene.apache.org/solr/]
@@ -100,12 +100,6 @@ Commit & optimize shortcuts
100
100
  solr.commit
101
101
  solr.optimize
102
102
 
103
- ===XML Builders for RSolr
104
- As of version 0.9.1, RSolr can use LibXml to create the update messages sent to solr. To switch from Builder to LibXml, set the RSolr::Message.builder like:
105
- solr = RSolr.connect
106
- solr.message.adapter = RSolr::Message::Adapter::Libxml.new
107
-
108
-
109
103
  == Response Formats
110
104
  The default response format is Ruby. When the :wt param is set to :ruby, the response is eval'd resulting in a Hash. You can get a raw response by setting the :wt to "ruby" - notice, the string -- not a symbol. RSolr will eval the Ruby string ONLY if the :wt value is :ruby. All other response formats are available as expected, :wt=>'xml' etc..
111
105
 
@@ -119,30 +113,10 @@ The default response format is Ruby. When the :wt param is set to :ruby, the res
119
113
  ===JSON:
120
114
  solr.select(:wt=>:json)
121
115
 
122
- You can access the original request context (path, params, url etc.) by calling the #adapter_response method:
116
+ You can access the original request context (path, params, url etc.) by calling the #raw method:
123
117
  response = solr.select :q=>'*:*'
124
- response.adapter_response[:status_code]
125
- response.adapter_response[:body]
126
- response.adapter_response[:url]
127
-
128
- The adapter_response is a hash that contains the generated params, url, path, post data, headers etc., very useful for debugging and testing.
129
-
130
-
131
- == HTTP Client Adapter
132
- You can specify the http client adapter:
133
- :net_http uses the standard Net::HTTP library
134
- :curb uses the C based "curl" library
118
+ response.raw[:status_code]
119
+ response.raw[:body]
120
+ response.raw[:url]
135
121
 
136
- NOTE: The Net::Http is the default adapter.
137
-
138
- Example:
139
-
140
- RSolr.connect(:adapter => :curb)
141
- RSolr.connect(:adapter => :net_http)
142
-
143
- Intereseting read about Ruby's Net::HTTP library:
144
- http://apocryph.org/2008/11/09/more_indepth_analysis_ruby_http_client_performance
145
-
146
- NOTE: You can't use the :curb adapter under jRuby. To install curb:
147
-
148
- sudo gem install curb
122
+ The raw is a hash that contains the generated params, url, path, post data, headers etc., very useful for debugging and testing.
data/Rakefile CHANGED
@@ -1,76 +1,14 @@
1
1
  require 'rake'
2
2
  require 'rake/testtask'
3
3
  require 'rake/rdoctask'
4
-
5
- namespace :rsolr do
6
-
7
- desc "Starts the HTTP server used for running HTTP connection tests"
8
- task :start_test_server do
9
- system "cd apache-solr/example; java -jar start.jar"
10
- end
11
-
12
- end
13
-
14
- task :default => [:test_units]
15
-
16
- # rake package
17
-
18
- require 'rubygems'
19
4
  require 'rake/gempackagetask'
20
- raw_spec = File.read 'rsolr.gemspec'
21
- spec = eval(raw_spec)
22
- Rake::GemPackageTask.new(spec) do |pkg|
23
- pkg.need_tar = true
24
- end
25
-
26
- desc "Run basic tests"
27
- Rake::TestTask.new("test_units") { |t|
28
- t.pattern = 'test/**/*_test.rb'
29
- t.verbose = true
30
- t.warning = true
31
- t.libs << "test"
32
- }
33
-
34
- require 'spec/rake/spectask'
35
-
36
- desc "Run specs"
37
- Spec::Rake::SpecTask.new('spec') do |t|
38
- t.spec_files = FileList['spec/**/*_spec.rb']
39
- t.libs += ["lib", "spec"]
40
- end
41
-
42
- desc 'Run specs' # this task runs each test in its own process
43
- task :specs do
44
- require 'rubygems'
45
- require 'facets/more/filelist' unless defined?(FileList)
46
- files = FileList["**/*_spec.rb"]
47
- p files.to_a
48
- files.each do |filename|
49
- system "cd #{File.dirname(filename)} && ruby #{File.basename(filename)}"
50
- end
51
- end
52
-
53
- desc "Run specs"
54
- Rake::TestTask.new("specs") { |t|
55
- t.pattern = 'spec/**/*_spec.rb'
56
- t.verbose = true
57
- t.warning = true
58
- t.libs += ["lib", "spec"]
59
- }
60
5
 
61
- # Clean house
62
- desc 'Clean up tmp files.'
63
- task :clean do |t|
64
- FileUtils.rm_rf "doc"
65
- FileUtils.rm_rf "pkg"
6
+ ENV['RUBYOPT'] = '-W1'
7
+
8
+ task :environment do
9
+ require File.dirname(__FILE__) + '/lib/rsolr'
66
10
  end
11
+
12
+ Dir['tasks/**/*.rake'].each { |t| load t }
67
13
 
68
- # Rdoc
69
- desc 'Generate documentation for the rsolr gem.'
70
- Rake::RDocTask.new(:doc) do |rdoc|
71
- rdoc.rdoc_dir = 'doc'
72
- rdoc.title = 'RSolr'
73
- rdoc.options << '--line-numbers' << '--inline-source'
74
- rdoc.rdoc_files.include('README.rdoc')
75
- rdoc.rdoc_files.include('lib/**/*.rb')
76
- end
14
+ task :default => ['spec:api']
data/examples/http.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  require File.join(File.dirname(__FILE__), '..', 'lib', 'rsolr')
2
2
 
3
- # switch out the http adapter from net_http to curb
4
- solr = RSolr.connect :adapter=>:curb
3
+ solr = RSolr.connect
5
4
 
6
5
  Dir['../apache-solr/example/exampledocs/*.xml'].each do |xml_file|
7
6
  puts "Updating with #{xml_file}"
@@ -14,7 +13,7 @@ puts
14
13
 
15
14
  response = solr.select(:q=>'ipod', :fq=>['price:[0 TO 50]'], :rows=>2, :start=>0)
16
15
 
17
- puts "URL : #{response.adapter_response[:url]} -> #{response.adapter_response[:status_code]}"
16
+ puts "URL : #{response.raw[:url]} -> #{response.raw[:status_code]}"
18
17
 
19
18
  puts
20
19
 
@@ -5,9 +5,9 @@ require 'java'
5
5
  #
6
6
  # Connection for JRuby + DirectSolrConnection
7
7
  #
8
- class RSolr::Connection::Adapter::Direct
8
+ class RSolr::Connection::Direct
9
9
 
10
- include RSolr::HTTPClient::Util
10
+ include RSolr::Connection::Utils
11
11
 
12
12
  attr_accessor :opts
13
13
 
@@ -0,0 +1,79 @@
1
+ require 'net/http'
2
+
3
+ #
4
+ # Connection for standard HTTP Solr server
5
+ #
6
+ class RSolr::Connection::NetHttp
7
+
8
+ include RSolr::Connection::Utils
9
+
10
+ attr_reader :opts, :uri
11
+
12
+ # opts can have:
13
+ # :url => 'http://localhost:8080/solr'
14
+ def initialize opts={}
15
+ opts[:url] ||= 'http://127.0.0.1:8983/solr'
16
+ @opts = opts
17
+ @uri = URI.parse opts[:url]
18
+ end
19
+
20
+ # send a request to the connection
21
+ # request '/update', :wt=>:xml, '</commit>'
22
+ def request path, params={}, *extra
23
+ opts = extra[-1].kind_of?(Hash) ? extra.pop : {}
24
+ data = extra[0]
25
+ # force a POST, use the query string as the POST body
26
+ if opts[:method] == :post and data.to_s.empty?
27
+ http_context = self.post(path, hash_to_query(params), {}, {'Content-Type' => 'application/x-www-form-urlencoded'})
28
+ else
29
+ if data
30
+ # standard POST, using "data" as the POST body
31
+ http_context = self.post(path, data, params, {"Content-Type" => 'text/xml; charset=utf-8'})
32
+ else
33
+ # standard GET
34
+ http_context = self.get(path, params)
35
+ end
36
+ end
37
+ raise RSolr::RequestError.new(http_context[:body]) unless http_context[:status_code] == 200
38
+ http_context
39
+ end
40
+
41
+ protected
42
+
43
+ def connection
44
+ @connection ||= Net::HTTP.new(@uri.host, @uri.port)
45
+ end
46
+
47
+ def get path, params={}
48
+ url = self.build_url path, params
49
+ net_http_response = self.connection.get url
50
+ create_http_context net_http_response, url, path, params
51
+ end
52
+
53
+ def post path, data, params={}, headers={}
54
+ url = self.build_url path, params
55
+ net_http_response = self.connection.post url, data, headers
56
+ create_http_context net_http_response, url, path, params, data, headers
57
+ end
58
+
59
+ def create_http_context net_http_response, url, path, params, data=nil, headers={}
60
+ full_url = "#{@uri.scheme}://#{@uri.host}"
61
+ full_url += @uri.port ? ":#{@uri.port}" : ''
62
+ full_url += url
63
+ {
64
+ :status_code=>net_http_response.code.to_i,
65
+ :url=>full_url,
66
+ :body=>net_http_response.body,
67
+ :path=>path,
68
+ :params=>params,
69
+ :data=>data,
70
+ :headers=>headers
71
+ }
72
+ end
73
+
74
+ def build_url path, params={}
75
+ full_path = @uri.path + path
76
+ super full_path, params, @uri.query
77
+ end
78
+
79
+ end
@@ -1,124 +1,72 @@
1
1
  module RSolr::Connection
2
2
 
3
- module Adapter
4
- autoload :Direct, 'rsolr/connection/adapter/direct'
5
- autoload :HTTP, 'rsolr/connection/adapter/http'
6
- end
7
-
8
- class Base
9
-
10
- attr_reader :adapter
11
-
12
- # "adapter" is instance of:
13
- # RSolr::Adapter::HTTP
14
- # RSolr::Adapter::Direct (jRuby only)
15
- # or any other class that uses the connection "interface"
16
- def initialize(adapter)
17
- @adapter = adapter
18
- end
3
+ autoload :Direct, 'rsolr/connection/direct'
4
+ autoload :NetHttp, 'rsolr/connection/net_http'
19
5
 
20
- # Send a request to a request handler using the method name.
21
- def method_missing(method_name, *args, &blk)
22
- request("/#{method_name}", *args, &blk)
6
+ # Helpful utility methods for building queries to a Solr server
7
+ module Utils
8
+
9
+ # Performs URI escaping so that you can construct proper
10
+ # query strings faster. Use this rather than the cgi.rb
11
+ # version since it's faster. (Stolen from Rack).
12
+ def escape(s)
13
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
14
+ #'%'+$1.unpack('H2'*$1.size).join('%').upcase
15
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
16
+ }.tr(' ', '+')
23
17
  end
24
18
 
25
- # sends data to the update handler
26
- # data can be a string of xml, or an object that returns xml from its #to_xml method
27
- def update(data, params={})
28
- request '/update', params, data
19
+ # Return the bytesize of String; uses String#length under Ruby 1.8 and
20
+ # String#bytesize under 1.9.
21
+ if ''.respond_to?(:bytesize)
22
+ def bytesize(string)
23
+ string.bytesize
24
+ end
25
+ else
26
+ def bytesize(string)
27
+ string.size
28
+ end
29
29
  end
30
30
 
31
- # send request solr
32
- # params is hash with valid solr request params (:q, :fl, :qf etc..)
33
- # if params[:wt] is not set, the default is :ruby
34
- # if :wt is something other than :ruby, the raw response body is used
35
- # otherwise, a simple Hash is returned
36
- # NOTE: to get raw ruby, use :wt=>'ruby' <- a string, not a symbol like :ruby
37
- #
38
- #
39
- def request(path, params={}, *extra)
40
- response = @adapter.request(path, map_params(params), *extra)
41
- adapt_response(response)
31
+ # creates and returns a url as a string
32
+ # "url" is the base url
33
+ # "params" is an optional hash of GET style query params
34
+ # "string_query" is an extra query string that will be appended to the
35
+ # result of "url" and "params".
36
+ def build_url url='', params={}, string_query=''
37
+ queries = [string_query, hash_to_query(params)]
38
+ queries.delete_if{|i| i.to_s.empty?}
39
+ url += "?#{queries.join('&')}" unless queries.empty?
40
+ url
42
41
  end
43
-
44
- #
45
- # single record:
46
- # solr.update(:id=>1, :name=>'one')
42
+
43
+ # converts a key value pair to an escaped string:
44
+ # Example:
45
+ # build_param(:id, 1) == "id=1"
46
+ def build_param(k,v)
47
+ "#{escape(k)}=#{escape(v)}"
48
+ end
49
+
47
50
  #
48
- # update using an array
49
- # solr.update([{:id=>1, :name=>'one'}, {:id=>2, :name=>'two'}])
51
+ # converts hash into URL query string, keys get an alpha sort
52
+ # if a value is an array, the array values get mapped to the same key:
53
+ # hash_to_query(:q=>'blah', :fq=>['blah', 'blah'], :facet=>{:field=>['location_facet', 'format_facet']})
54
+ # returns:
55
+ # ?q=blah&fq=blah&fq=blah&facet.field=location_facet&facet.field=format.facet
50
56
  #
51
- def add(doc, &block)
52
- update message.add(doc, &block)
53
- end
54
-
55
- # send </commit>
56
- def commit
57
- update message.commit
58
- end
59
-
60
- # send </optimize>
61
- def optimize
62
- update message.optimize
63
- end
64
-
65
- # send </rollback>
66
- # NOTE: solr 1.4 only
67
- def rollback
68
- update message.rollback
69
- end
70
-
71
- # Delete one or many documents by id
72
- # solr.delete_by_id 10
73
- # solr.delete_by_id([12, 41, 199])
74
- def delete_by_id(id)
75
- update message.delete_by_id(id)
76
- end
77
-
78
- # delete one or many documents by query
79
- # solr.delete_by_query 'available:0'
80
- # solr.delete_by_query ['quantity:0', 'manu:"FQ"']
81
- def delete_by_query(query)
82
- update message.delete_by_query(query)
83
- end
84
-
85
- # shortcut to RSolr::Message::Builder
86
- def message
87
- @message ||= RSolr::Message::Builder.new
88
- end
89
-
90
- protected
91
-
92
- # sets default params etc.. - could be used as a mapping hook
93
- # type of request should be passed in here? -> map_params(:query, {})
94
- def map_params(params)
95
- params||={}
96
- {:wt=>:ruby}.merge(params)
97
- end
98
-
99
- # "adapter_response" must be a hash with the following keys:
100
- # :params - a sub hash of standard solr params
101
- # : body - the raw response body from the solr server
102
- # This method will evaluate the :body value if the params[:wt] == :ruby
103
- # otherwise, the body is returned
104
- # The return object has a special method attached called #adapter_response
105
- # This method gives you access to the original response from the adapter,
106
- # so you can access things like the actual :url sent to solr,
107
- # the raw :body, original :params and original :data
108
- def adapt_response(adapter_response)
109
- data = adapter_response[:body]
110
- # if the wt is :ruby, evaluate the ruby string response
111
- if adapter_response[:params][:wt] == :ruby
112
- data = Kernel.eval(data)
113
- end
114
- # attach a method called #adapter_response that returns the original adapter response value
115
- def data.adapter_response
116
- @adapter_response
57
+ # if a value is empty/nil etc., it is not added
58
+ def hash_to_query(params)
59
+ mapped = params.map do |k, v|
60
+ next if v.to_s.empty?
61
+ if v.class == Array
62
+ hash_to_query(v.map { |x| [k, x] })
63
+ else
64
+ build_param k, v
65
+ end
117
66
  end
118
- data.send(:instance_variable_set, '@adapter_response', adapter_response)
119
- data
67
+ mapped.compact.join("&")
120
68
  end
121
-
69
+
122
70
  end
123
71
 
124
72
  end
data/lib/rsolr/message.rb CHANGED
@@ -2,11 +2,6 @@
2
2
 
3
3
  module RSolr::Message
4
4
 
5
- module Adapter
6
- autoload :Builder, 'rsolr/message/adapter/builder'
7
- autoload :Libxml, 'rsolr/message/adapter/libxml'
8
- end
9
-
10
5
  # A class that represents a "doc" xml element for a solr update
11
6
  class Document
12
7
 
@@ -25,7 +20,7 @@ module RSolr::Message
25
20
  values = [values] unless values.is_a?(Array)
26
21
  values.each do |v|
27
22
  next if v.to_s.empty?
28
- @fields << Field.new({:name=>field}, v)
23
+ @fields << Field.new({:name=>field}, v.to_s)
29
24
  end
30
25
  end
31
26
  @attrs={}
@@ -79,15 +74,6 @@ module RSolr::Message
79
74
 
80
75
  class Builder
81
76
 
82
- attr_writer :adapter
83
-
84
- # b = Builder.new
85
- # b.adapter = RSolr::Message::Adapter::LibXML.new
86
- # b.optimize == '<optimize/>'
87
- def adapter
88
- @adapter ||= RSolr::Message::Adapter::Builder.new
89
- end
90
-
91
77
  # generates "add" xml for updating solr
92
78
  # "data" can be a hash or an array of hashes.
93
79
  # - each hash should be a simple key=>value pair representing a solr doc.
@@ -115,39 +101,54 @@ module RSolr::Message
115
101
  #
116
102
  def add(data, add_attrs={})
117
103
  data = [data] unless data.is_a?(Array)
118
- documents = data.map do |doc|
104
+ add = Xout.new :add, add_attrs
105
+ data.each do |doc|
119
106
  doc = Document.new(doc) if doc.respond_to?(:each_pair)
120
107
  yield doc if block_given?
121
- doc
108
+ add.child :doc, doc.attrs do |doc_node|
109
+ doc.fields.each do |field_obj|
110
+ doc_node.child :field, field_obj.value, field_obj.attrs
111
+ end
112
+ end
122
113
  end
123
- adapter.add(documents, add_attrs)
114
+ add.to_xml
124
115
  end
125
116
 
126
117
  # generates a <commit/> message
127
118
  def commit(opts={})
128
- adapter.commit(opts)
119
+ Xout.new(:commit, opts).to_xml
129
120
  end
130
121
 
131
122
  # generates a <optimize/> message
132
123
  def optimize(opts={})
133
- adapter.optimize(opts)
124
+ Xout.new(:optimize, opts).to_xml
134
125
  end
135
126
 
136
127
  # generates a <rollback/> message
137
128
  def rollback
138
- adapter.rollback
129
+ Xout.new(:rollback).to_xml
139
130
  end
140
131
 
141
132
  # generates a <delete><id>ID</id></delete> message
142
133
  # "ids" can be a single value or array of values
143
134
  def delete_by_id(ids)
144
- adapter.delete_by_id(ids)
135
+ ids = [ids] unless ids.is_a?(Array)
136
+ delete_node = Xout.new(:delete) do |xml|
137
+ ids.each { |id| xml.child :id, id }
138
+ end
139
+ delete_node.to_xml
145
140
  end
146
141
 
147
142
  # generates a <delete><query>ID</query></delete> message
148
143
  # "queries" can be a single value or an array of values
149
144
  def delete_by_query(queries)
150
- adapter.delete_by_query(queries)
145
+ queries = [queries] unless queries.is_a?(Array)
146
+ delete_node = Xout.new(:delete) do |xml|
147
+ queries.each { |query| xml.child :query, query }
148
+ end
149
+ delete_node.to_xml
151
150
  end
151
+
152
152
  end
153
- end
153
+
154
+ end
data/lib/rsolr.rb CHANGED
@@ -4,13 +4,15 @@ require 'rubygems'
4
4
 
5
5
  $: << File.dirname(__FILE__) unless $:.include?(File.dirname(__FILE__))
6
6
 
7
+ require 'xout'
8
+
7
9
  module RSolr
8
10
 
9
- VERSION = '0.9.5'
11
+ VERSION = '0.9.7'
10
12
 
11
13
  autoload :Message, 'rsolr/message'
14
+ autoload :Client, 'rsolr/client'
12
15
  autoload :Connection, 'rsolr/connection'
13
- autoload :HTTPClient, 'rsolr/http_client'
14
16
 
15
17
  # Factory for creating connections.
16
18
  # 2 modes of argument operations:
@@ -28,16 +30,16 @@ module RSolr
28
30
  type = args.first.is_a?(Symbol) ? args.shift : :http
29
31
  opts = args
30
32
  type_class = case type
31
- when :http,nil
32
- 'HTTP'
33
+ when :net_http,:http,nil
34
+ 'NetHttp'
33
35
  when :direct
34
36
  'Direct'
35
37
  else
36
38
  raise "Invalid connection type: #{type} - use :http, :direct or leave nil for :http/default"
37
39
  end
38
- adapter_class = RSolr::Connection::Adapter.const_get(type_class)
40
+ adapter_class = RSolr::Connection.const_get(type_class)
39
41
  adapter = adapter_class.new(*opts)
40
- RSolr::Connection::Base.new(adapter)
42
+ RSolr::Client.new(adapter)
41
43
  end
42
44
 
43
45
  # A module that contains string related methods
@@ -55,7 +57,7 @@ module RSolr
55
57
  # send the escape method into the Connection class ->
56
58
  # solr = RSolr.connect
57
59
  # solr.escape('asdf')
58
- RSolr::Connection::Base.send(:include, Char)
60
+ RSolr::Client.send(:include, Char)
59
61
 
60
62
  # bring escape into this module (RSolr) -> RSolr.escape('asdf')
61
63
  extend Char
data/lib/xout.rb ADDED
@@ -0,0 +1,65 @@
1
+ class Xout
2
+
3
+ attr_reader :name, :text, :attrs, :children
4
+
5
+ def initialize node_name, *args, &block
6
+ @children = []
7
+ attrs = args.last.is_a?(Hash) ? args.pop : {}
8
+ text = args.empty? ? '' : args.pop.to_s
9
+ @name, @text, @attrs = node_name, text, attrs
10
+ yield self if block_given?
11
+ end
12
+
13
+ def child name, *args, &block
14
+ add_child self.class.new(name, *args, &block)
15
+ end
16
+
17
+ def add_child node
18
+ children << node
19
+ end
20
+
21
+ def to_xml
22
+ xml = ["<#{name}#{create_attrs(attrs)}"]
23
+ if not text.empty? or not children.empty?
24
+ xml << ">#{escape_text text.to_s}"
25
+ xml += children.map{|child|child.to_xml}
26
+ xml << "</#{name}>"
27
+ else
28
+ xml << '/>'
29
+ end
30
+ xml.join
31
+ end
32
+
33
+ alias :to_s :to_xml
34
+
35
+ def to_xml_doc
36
+ '<?xml version="1.0" encoding="UTF-8"?>' + to_xml
37
+ end
38
+
39
+ protected
40
+
41
+ # builds an XML attribute string.
42
+ # escapes each attribute value by running it through #escape_attr
43
+ def create_attrs hash
44
+ r = hash.map { |k,v| "#{k}=\"#{escape_attr v.to_s}\"" }.join(' ')
45
+ " #{r}" unless r.empty?
46
+ end
47
+
48
+ # minimal escaping for attribute values
49
+ def escape_attr input
50
+ escape input, '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;', "'"=>'&apos;', '"'=>'&quote;'
51
+ end
52
+
53
+ # minimal escaping for text
54
+ def escape_text input
55
+ escape input, '&'=>'&amp;', '<'=>'&lt;', '>'=>'&gt;'
56
+ end
57
+
58
+ # accepts a string input and a hash mapping of characters => replacement values:
59
+ # Example:
60
+ # escape 'My <string>cat</strong>', '<'=>'&gt;', '>'=>'&lt;'
61
+ def escape input, map
62
+ input.gsub(/[#{map.keys.join}]+/) { | char | map[char] || char }
63
+ end
64
+
65
+ end