Codebase list mikutter / 896414b
New upstream version 3.8.0 HIGUCHI Daisuke (VDR dai) 5 years ago
262 changed file(s) with 25632 addition(s) and 1811 deletion(s). Raw diff Collapse all Expand all
1111 group :default do
1212 gem 'oauth', '>= 0.5.1'
1313 gem 'json_pure', '~> 1.8'
14 gem 'addressable', '~> 2.3'
14 gem 'addressable', '>= 2.5.2', '< 2.6'
1515 gem 'diva', '>= 0.3.2', '< 2.0'
1616 gem 'memoist', '>= 0.16', '< 0.17'
1717 gem 'ruby-hmac', '~> 0.4'
4545 NeverRetrieveOverlappedMumble = false
4646
4747 # このソフトのバージョン。
48 VERSION = [3,7,4,9999]
48 VERSION = [3,8,0,9999]
4949
5050 end
0 #!/usr/bin/env ruby
10 #-*- coding: utf-8 -*-
21
32 #
(No changes)
(No changes)
1717 "Plural-Forms: nplurals=1; plural=0;\n"
1818
1919 msgid "Statusbar default message"
20 msgstr "mikutter 3.6へようこそ。ここから未来が始まります。"
20 msgstr "mikutter 3.8 未来をその手に。"
5656 open(result.attribute('src'))
5757 end
5858
59 # twipple photo
60 defimageopener('twipple photo', %r<^http://p\.twipple\.jp/[a-zA-Z0-9]+>) do |display_url|
61 connection = HTTPClient.new
62 page = connection.get_content(display_url)
63 next nil if page.empty?
64 doc = Nokogiri::HTML(page)
65 result = doc.css('#post_image').first
66 open(result.attribute('src'))
67 end
68
6959 # moby picture
7060 defimageopener('moby picture', %r<^http://moby.to/[a-zA-Z0-9]+>) do |display_url|
7161 connection = HTTPClient.new
114104 doc = Nokogiri::HTML(page)
115105 result = doc.css('#the-image').first
116106 open(result.attribute('src'))
117 end
118
119 # twitgoo
120 defimageopener('twitgoo', %r<^http://twitgoo\.com/[a-zA-Z0-9]+>) do |display_url|
121 open(display_url)
122107 end
123108
124109 # jigokuno.com
214199 doc = Nokogiri::HTML(page)
215200 result = doc.css('meta[property="twitter:image:src"]')
216201 open(result.attribute('content').value)
217 end
218
219 defimageopener('彩の庭', %r{\Ahttp://haruicon\.com/ayanoniwa?\Z}) do |display_url|
220 connection = HTTPClient.new
221 page = connection.get_content(display_url)
222 next nil if page.empty?
223 doc = Nokogiri::HTML(page)
224 path = doc.css('img').attribute('src').value
225 img = URI.join("http://haruicon.com", path)
226 open(img)
227202 end
228203
229204 # xkcd.com
4444 option nil, _("プロキシを使わない")
4545 end
4646 end
47
48 on_userconfig_modify do |key, val|
49 next if key != :proxy_enabled
50 if UserConfig[:realtime_rewind]
51 Thread.new {
52 UserConfig[:realtime_rewind] = false
53 sleep(3)
54 UserConfig[:realtime_rewind] = true
55 }
56 end
57 end
5847 end
2323 end
2424
2525 def refresh_tab
26 if Enumerator.new{|y| Plugin.filtering(:worlds, y) }.any?{|w| w.class.slug == :twitter }
26 if Enumerator.new{|y| Plugin.filtering(:worlds, y) }.any?{|w| search?(w, q: "") }
2727 present_tab
2828 else
2929 absent_tab
4040 end
4141
4242 settings _('リアルタイム更新') do
43 boolean(_('ホームタイムライン(UserStream)'), :realtime_rewind).
44 tooltip _('Twitter の UserStream APIを用いて、リアルタイムにツイートやフォローなどのイベントを受け取ります')
4543 boolean(_('リスト(Streaming API)'), :filter_realtime_rewind).
4644 tooltip _('Twitter の Streaming APIを用いて、リアルタイムにリストの更新等を受け取ります')
4745 end
00 ---
11 slug: :streaming
22 depends:
3 mikutter: '0.2'
3 mikutter: '3.7'
44 plugin:
55 - twitter
6 version: '1.0'
6 version: '2.0'
77 author: toshi_a
88 name: Streaming
9 description: UserStream等の各種ストリーミングAPIに対応する
9 description: Filter Stream等の各種ストリーミングAPIに対応する
+0
-91
core/plugin/streaming/filter.rb less more
0 # -*- coding: utf-8 -*-
1
2 require 'set'
3
4 Plugin.create :streaming do
5 thread = nil
6 @fail_count = @wait_time = 0
7 reconnect_request_flag = false
8
9 on_filter_stream_force_retry do
10 if UserConfig[:filter_realtime_rewind]
11 thread.kill rescue nil if thread
12 thread = start end end
13
14 on_filter_stream_reconnect_request do
15 if not reconnect_request_flag
16 reconnect_request_flag = true
17 Reserver.new(30, thread: Delayer) {
18 reconnect_request_flag = false
19 Plugin.call(:filter_stream_force_retry) } end end
20
21 def start
22 twitter = Enumerator.new{|y|
23 Plugin.filtering(:worlds, y)
24 }.find{|world|
25 world.class.slug == :twitter
26 }
27 return unless twitter
28 @success_flag = false
29 @fail = MikuTwitter::StreamingFailedActions.new("Filter Stream", self)
30 Thread.new{
31 loop{
32 begin
33 follow = Plugin.filtering(:filter_stream_follow, Set.new).first || Set.new
34 track = Plugin.filtering(:filter_stream_track, "").first || ""
35 if follow.empty? && track.empty?
36 sleep(60)
37 else
38 param = {}
39 param[:follow] = follow.to_a[0, 5000].map(&:id).join(',') if not follow.empty?
40 param[:track] = track if not track.empty?
41 r = twitter.streaming(:filter_stream, param){ |json|
42 json.strip!
43 case json
44 when /\A\{.*\}\Z/
45 if @success_flag
46 @fail.success
47 @success_flag = true end
48 parsed = JSON.parse(json).symbolize
49 if not parsed[:retweeted_status]
50 MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(parsed) rescue nil end
51 end }
52 raise r if r.is_a? Exception
53 notice "filter stream: disconnected #{r}"
54 streamerror r
55 end
56 rescue Net::HTTPError => exception
57 warn "filter stream: disconnected: #{exception.code} #{exception.body}"
58 streamerror exception
59 warn exception
60 rescue Net::ReadTimeout => exception
61 streamerror exception
62 rescue Exception => exception
63 warn "filter stream: disconnected: exception #{exception}"
64 streamerror exception
65 warn exception end
66 notice "retry wait #{@fail.wait_time}, fail_count #{@fail.fail_count}"
67 sleep @fail.wait_time } }
68 end
69
70 def streamerror(exception)
71 @success_flag = false
72 @fail.notify(exception) end
73
74 on_userconfig_modify do |key, new_val|
75 next if key != :filter_realtime_rewind
76 if new_val
77 notice 'filter stream: enable'
78 thread = start unless thread.is_a? Thread
79 else
80 notice 'filter stream: disable'
81 thread.kill if thread.is_a? Thread
82 thread = nil
83 end
84 end
85
86 Delayer.new do
87 thread = start if UserConfig[:filter_realtime_rewind]
88 end
89
90 end
+0
-49
core/plugin/streaming/perma_streamer.rb less more
0 # -*- coding: utf-8 -*-
1 # 自動でコネクションを貼り直すStreamer
2 require_relative 'streamer'
3
4 module ::Plugin::Streaming
5 class PermaStreamer
6
7 # ==== Args
8 # [service] 接続するService
9 def initialize(service)
10 @service = service
11 @thread = Thread.new(&method(:mainloop))
12 @fail = MikuTwitter::StreamingFailedActions.new('UserStream', Plugin.create(:streaming)) end
13
14 def mainloop
15 loop do
16 begin
17 streamer = Plugin::Streaming::Streamer.new(@service){
18 @fail.success
19 }
20 result = streamer.thread.value
21 rescue Net::ReadTimeout => exception
22 @fail.notify(exception)
23 rescue Net::HTTPError => exception
24 warn "PermaStreamer caught exception"
25 warn exception
26 @fail.notify(exception)
27 rescue Exception => exception
28 warn "PermaStreamer caught exception"
29 warn exception
30 @fail.notify(exception)
31 else
32 notice "PermaStreamer exit"
33 notice result
34 @fail.notify(result)
35 ensure
36 streamer.kill if streamer
37 end
38 notice "retry wait #{@fail.wait_time}, fail_count #{@fail.fail_count}"
39 sleep @fail.wait_time
40 end
41 end
42
43 def kill
44 @thread.kill
45 end
46
47 end
48 end
+0
-178
core/plugin/streaming/streamer.rb less more
0 # -*- coding: utf-8 -*-
1 require 'thread'
2 require_relative 'streamer_error'
3
4 module ::Plugin::Streaming
5 class Streamer
6 attr_reader :thread, :service
7
8 # イベントを登録する
9 # ==== Args
10 # [name] イベント名
11 # [many] オブジェクトをまとめて配列で受け取るかどうか
12 # [&proc] イベントを受け取るオブジェクト。
13 def self.defevent(name, many=false, &proc)
14 speed_key = "#{name}_queue_delay".to_sym
15 define_method("_event_#{name}", &proc)
16 if many
17 define_method("event_#{name}"){ |json|
18 @queue[name] ||= TimeLimitedQueue.new(HYDE, everytime{ (UserConfig[speed_key] || 100).to_f / 1000 }){ |data|
19 begin
20 __send__("_event_#{name}", data)
21 rescue Exception => e
22 warn e end }
23 @threads[name] ||= everytime{ @queue[name].thread }
24 @queue[name].push json }
25 else
26 define_method("event_#{name}"){ |json|
27 @queue[name] ||= Queue.new
28 @threads[name] ||= Thread.new{
29 loop{
30 begin
31 sleep((UserConfig[speed_key] || 100).to_f / 1000)
32 __send__("_event_#{name}", @queue[name].pop)
33 rescue Exception => e
34 warn e end } }
35 queue_push(name, json) } end end
36
37 # ==== Args
38 # [service] 接続するService
39 # [on_connect] 接続されたら呼ばれる
40 def initialize(service, &on_connect)
41 @service = service
42 @thread = Thread.new(&method(:mainloop))
43 @on_connect = on_connect
44 @threads = {}
45 @queue = {} end
46
47 def mainloop
48 service.streaming{ |q|
49 if q and not q.empty?
50 parsed = JSON.parse(q) rescue nil
51 event_factory parsed if parsed end }
52 rescue Net::ReadTimeout
53 raise
54 rescue => exception
55 error exception
56 raise end
57
58 # UserStreamを終了する
59 def kill
60 @thread.kill
61 @threads.each{ |event, thread|
62 thread.kill }
63 @threads.clear
64 @queue.clear end
65
66 private
67
68 # イベント _name_ のキューに値 _data_ を追加する。
69 # ==== Args
70 # [name] イベント名
71 # [data] キューに入れる値
72 # ==== Exception
73 # キューを処理するスレッドが正常終了している場合、 Plugin::Streaming::StreamerError を発生させる。
74 # 異常終了している場合は、その例外をそのまま発生させる。
75 def queue_push(name, data)
76 if @threads[name] && @threads[name]
77 if @threads[name].alive?
78 @queue[name].push data
79 else
80 if @threads[name].status.nil?
81 @queue[name].thread.status.join
82 else
83 raise Plugin::Streaming::StreamerError, "event '#{name}' thread is dead." end end end end
84
85 # UserStreamで流れてきた情報を処理する
86 # ==== Args
87 # [parsed] パースされたJSONオブジェクト
88 def event_factory(json)
89 json.freeze
90 case
91 when json['friends']
92 if @on_connect
93 @on_connect.call(json)
94 @on_connect = nil end
95 when respond_to?("event_#{json['event']}")
96 __send__(:"event_#{json['event']}", json)
97 when json['direct_message']
98 event_direct_message(json['direct_message'])
99 when json['delete']
100 # if Mopt.debug
101 # Plugin.activity :system, YAML.dump(json)
102 # end
103 when !json.has_key?('event')
104 event_update(json)
105 when Mopt.debug
106 Plugin.activity :system, YAML.dump(json)
107 else
108 if Mopt.debug
109 Plugin.activity :system, "unsupported event:\n" + YAML.dump(json) end end end
110
111 defevent(:update, true) do |data|
112 events = {update: Set.new, mention: Set.new, mypost: Set.new}
113 data.each { |json|
114 msg = MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(json.symbolize)
115 events[:update] << msg
116 events[:mention] << msg if msg.to_me?
117 events[:mypost] << msg if msg.from_me? }
118 events.each{ |event_name, data|
119 Plugin.call(event_name, @service, data.freeze) } end
120
121 defevent(:direct_message, true) do |data|
122 Plugin.call(:direct_messages, @service, data.map{ |datum| MikuTwitter::ApiCallSupport::Request::Parser.direct_message(datum.symbolize) }) end
123
124 defevent(:favorite) do |json|
125 by = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize)
126 to = MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(json['target_object'].symbolize)
127 if(to.respond_to?(:add_favorited_by))
128 to.add_favorited_by(by, Time.parse(json['created_at'])) end end
129
130 defevent(:unfavorite) do |json|
131 by = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize)
132 to = MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(json['target_object'].symbolize)
133 if(to.respond_to?(:remove_favorited_by))
134 to.remove_favorited_by(by) end end
135
136 defevent(:favorited_retweet) do |json|
137 by = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize)
138 to = Plugin::Twitter::Message.findbyid(json['target_object']['id'].to_i)
139 if to.is_a?(Plugin::Twitter::Message)
140 source_message = to.retweet_source
141 if(to.respond_to?(:add_favorited_by))
142 source_message.add_favorited_by(by, Time.parse(json['created_at'])) end end end
143
144 defevent(:retweeted_retweet) do |json|
145 by = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize)
146 #to = MikuTwitter::ApiCallSupport::Request::Parser.user(json['target'].symbolize)
147 target_object = MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(json['target_object'].symbolize)
148 source_object = target_object.retweet_source
149 source_object.add_retweet_user(by, Time.parse(json['created_at'])) end
150
151 defevent(:quoted_tweet) do |json|
152 MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(json['target_object'].symbolize) end
153
154 defevent(:follow) do |json|
155 source = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize)
156 target = MikuTwitter::ApiCallSupport::Request::Parser.user(json['target'].symbolize)
157 if target.me?(@service)
158 Plugin.call(:followers_created, @service, [source])
159 elsif source.me?(@service)
160 Plugin.call(:followings_created, @service, [target]) end end
161
162 defevent(:list_member_added) do |json|
163 target_user = MikuTwitter::ApiCallSupport::Request::Parser.user(json['target'].symbolize) # リストに追加されたユーザ
164 list = MikuTwitter::ApiCallSupport::Request::Parser.list(json['target_object'].symbolize) # リスト
165 source_user = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize) # 追加したユーザ
166 list.add_member(target_user)
167 Plugin.call(:list_member_added, @service, target_user, list, source_user) end
168
169 defevent(:list_member_removed) do |json|
170 target_user = MikuTwitter::ApiCallSupport::Request::Parser.user(json['target'].symbolize) # リストに追加されたユーザ
171 list = MikuTwitter::ApiCallSupport::Request::Parser.list(json['target_object'].symbolize) # リスト
172 source_user = MikuTwitter::ApiCallSupport::Request::Parser.user(json['source'].symbolize) # 追加したユーザ
173 list.remove_member(target_user)
174 Plugin.call(:list_member_removed, @service, target_user, list, source_user) end
175
176 end
177 end
+0
-3
core/plugin/streaming/streamer_error.rb less more
0 # -*- coding: utf-8 -*-
1 module ::Plugin::Streaming
2 class StreamerError; end end
00 # -*- coding: utf-8 -*-
1 require File.join(__dir__, 'perma_streamer')
2 require File.join(__dir__, 'filter')
1
2 require 'set'
33
44 Plugin.create :streaming do
5 streamers = {} # service_id => PermaStreamer
6 Delayer.new {
7 Enumerator.new{|y|
5 thread = nil
6 @fail_count = @wait_time = 0
7 reconnect_request_flag = false
8
9 on_filter_stream_force_retry do
10 if UserConfig[:filter_realtime_rewind]
11 thread.kill rescue nil if thread
12 thread = start end end
13
14 on_filter_stream_reconnect_request do
15 if not reconnect_request_flag
16 reconnect_request_flag = true
17 Reserver.new(30, thread: Delayer) {
18 reconnect_request_flag = false
19 Plugin.call(:filter_stream_force_retry) } end end
20
21 def start
22 twitter = Enumerator.new{|y|
823 Plugin.filtering(:worlds, y)
9 }.select{|world|
24 }.find{|world|
1025 world.class.slug == :twitter
11 }.each{ |service|
12 if UserConfig[:realtime_rewind]
13 streamers[service.slug] ||= Plugin::Streaming::PermaStreamer.new(service) end } }
26 }
27 return unless twitter
28 @success_flag = false
29 @fail = MikuTwitter::StreamingFailedActions.new("Filter Stream", self)
30 Thread.new{
31 loop{
32 begin
33 follow = Plugin.filtering(:filter_stream_follow, Set.new).first || Set.new
34 track = Plugin.filtering(:filter_stream_track, "").first || ""
35 if follow.empty? && track.empty?
36 sleep(60)
37 else
38 param = {}
39 param[:follow] = follow.to_a[0, 5000].map(&:id).join(',') if not follow.empty?
40 param[:track] = track if not track.empty?
41 r = twitter.streaming(:filter_stream, param){ |json|
42 json.strip!
43 case json
44 when /\A\{.*\}\Z/
45 if @success_flag
46 @fail.success
47 @success_flag = true end
48 parsed = JSON.parse(json).symbolize
49 if not parsed[:retweeted_status]
50 MikuTwitter::ApiCallSupport::Request::Parser.streaming_message(parsed) rescue nil end
51 end }
52 raise r if r.is_a? Exception
53 notice "filter stream: disconnected #{r}"
54 streamerror r
55 end
56 rescue Net::HTTPError => exception
57 warn "filter stream: disconnected: #{exception.code} #{exception.body}"
58 streamerror exception
59 warn exception
60 rescue Net::ReadTimeout => exception
61 streamerror exception
62 rescue Exception => exception
63 warn "filter stream: disconnected: exception #{exception}"
64 streamerror exception
65 warn exception end
66 notice "retry wait #{@fail.wait_time}, fail_count #{@fail.fail_count}"
67 sleep @fail.wait_time } }
68 end
69
70 def streamerror(exception)
71 @success_flag = false
72 @fail.notify(exception) end
1473
1574 on_userconfig_modify do |key, new_val|
75 next if key != :filter_realtime_rewind
1676 if new_val
17 streamers.values.each(&:kill)
18 streamers = {}
19 Enumerator.new{|y|
20 Plugin.filtering(:worlds, y)
21 }.select{|world|
22 world.class.slug == :twitter
23 }.each{ |service|
24 streamers[service.slug] ||= Plugin::Streaming::PermaStreamer.new(service) }
77 notice 'filter stream: enable'
78 thread = start unless thread.is_a? Thread
2579 else
26 streamers.values.each(&:kill)
27 streamers = {}
80 notice 'filter stream: disable'
81 thread.kill if thread.is_a? Thread
82 thread = nil
2883 end
2984 end
3085
31 on_world_after_created do |new_world|
32 if UserConfig[:realtime_rewind] && new_world.class.slug == :twitter
33 streamers[new_world.slug] ||= Plugin::Streaming::PermaStreamer.new(new_world) end end
34
35 on_world_destroy do |deleted_world|
36 streamers[deleted_world.slug] and streamers[deleted_world.slug].kill end
37
38 onunload do
39 streamers.values.each(&:kill)
40 streamers = {} end
86 Delayer.new do
87 thread = start if UserConfig[:filter_realtime_rewind]
88 end
4189
4290 end
33 require 'addressable/uri'
44
55 module MikuTwitter::APIShortcuts
6 extend Gem::Deprecate
67
78 RELATIONAL_DEFAULT = {count: 5000}.freeze
89
210211 #
211212
212213 def userstream(params={}, &chunk)
213 stream("https://userstream.twitter.com/1.1/user.json", params, &chunk) end
214 stream("https://userstream.twitter.com/1.1/user.json", params, &chunk)
215 end
216 deprecate :userstream, "Account Activity API(see: https://developer.twitter.com/en/products/accounts-and-users/account-activity-api.html)", 2018, 8
217
214218
215219 def filter_stream(params={}, &chunk)
216220 stream("https://stream.twitter.com/1.1/statuses/filter.json", params, &chunk) end
Binary diff not shown
3737 :follow_queue_delay => 100,
3838 :direct_message_queue_delay => 100,
3939
40 # User Stream
41 :realtime_rewind => true,
40 # Streaming API(Twitter)
4241 :filter_realtime_rewind => true,
4342
4443 # デフォルトのフッダ
+0
-25
devel/ABOUTCHI less more
0 Computer Humanoid Interface
1
2 * CHIとは
3 もともと、mikutterはCHI(Computer Humanoid Interface)にGUIプラグインを足しただけのものでした。
4 現在でも、mikutterを簡単にCHIに戻すことができます。
5
6 * 必要なもの
7 - プラグインが必要としているアプリケーション
8 - temperature
9 - lm_sensors
10 - gemで入れるもの
11 - httpclient
12 - classifier
13 - stemmer
14 - sys-filesystem
15 - sys-uptime
16
17 * インストール方法・使いかた
18 このディレクトリにあるmakechi.rbを起動します。すると、src/を作り、その中にCHIを作成します。
19 あとは、src/mikutter.rb を端末から起動すればOK。
20 初回起動時は、OAuthの認証を求めてくるので、端末から切り離すのはその後で
21
22 * 起動オプション
23 - -d デーモンモード。端末と切り離される。(初回起動時はこのオプションを有効にしないこと)
24 - --debug デバッグモード
+0
-18
devel/chiskel/core/config.rb less more
0 # -*- coding: utf-8 -*-
1
2 module CHIConfig
3 LOGDIR = "~/.chi/log/"
4 TWITTER_CONSUMER_KEY = "AmDS1hCCXWstbss5624kVw"
5 TWITTER_CONSUMER_SECRET = "KOPOooopg9Scu7gJUBHBWjwkXz9xgPJxnhnhO55VQ"
6 TWITTER_AUTHENTICATE_REVISION = 1
7 NeverRetrieveOverlappedMumble = false
8 TMPDIR = "~/.chi/tmp/"
9 AutoTag = false
10 CONFROOT = "~/.chi/"
11 ACRO = "chi"
12 CACHE = "~/.chi/cache/"
13 PIDFILE = "/tmp/chi.pid"
14 VERSION = [0, 0, 4, 456]
15 NAME = "chi"
16 REVISION = 456
17 end
+0
-73
devel/chiskel/core/lib/graph.rb less more
0 #-*- coding: utf-8 -*-
1 # graph
2 # draw graph
3
4 require_if_exist 'rubygems'
5 require_if_exist 'gruff'
6
7 module Graph
8 def self.graph_drawable?
9 return defined?(Gruff)
10 end
11
12 # values = {records=>[points(Numeric)]}
13 # options = {
14 # :title => 'graph title'
15 # :tags => ['hash tags']
16 # :label => ['label of column']
17 # :start => 'label of start'
18 # :end => 'label of end'
19 # }
20 def self.drawgraph(values, options)
21 notice 'graph: '+options.inspect
22 if(self.graph_drawable?) then
23 graph = Gruff::Line.new
24 length = 0
25 values.each{ |key, ary|
26 graph.data(key, ary)
27 length = [length, ary.size].max
28 }
29 if(options[:label].is_a? Proc) then
30 label = options[:label].call(:get).freeze
31 else
32 label = (options[:label] || Hash.new).freeze
33 end
34 if (not options[:end]) then
35 options[:end] = Time.now
36 end
37 notice 'graph: '+label.inspect
38 graph.labels = label
39 graph.title = options[:title] + '(' + options[:start].strftime('%Y/%m/%d') + ')'
40 tmpfile = Tempfile.open('graph')
41 tmpfile.write(graph.to_blob)
42 result = {
43 :message => "#{options[:start].strftime('%Y/%m/%d %H:%M')}から#{options[:end].strftime('%m/%d %H:%M')}の#{options[:title]}のグラフ",
44 :tags => options[:tags],
45 :image => Message::Image.new(tmpfile.path)}
46 tmpfile.close
47 result
48 else
49 table = values.values.flatten
50 { :message => "#{options[:start].strftime('%Y/%m/%d %H:%M')}から#{options[:end].strftime('%m/%d %H:%M')}の#{options[:title]}は、最高#{table.max}、最低#{table.min}、平均#{table.avg.round_at(4)}です。",
51 :tags => options[:tags]} end end
52
53 # return graph label generator.
54 def self.gen_graph_label_defer(default={0 => Time.now.strftime('%H')})
55 last_sample_time = Time.now
56 temp_label = default
57 count = 0
58 lambda{ |value|
59 if(value == :get) then
60 return temp_label
61 elsif(value != nil) then
62 temp_label[count] = value
63 elsif(last_sample_time.strftime('%H') != Time.now.strftime('%H')) then
64 temp_label[count] = Time.now.strftime('%H')
65 last_sample_time = Time.now
66 else
67 end
68 count += 1
69 temp_label
70 }
71 end
72 end
+0
-63
devel/chiskel/core/lib/sensor.rb less more
0 #-*- coding: utf-8 -*-
1
2 require "socket"
3
4 class Sensor
5 def initialize()
6 @busy = nil
7 @all = nil
8 end
9
10 def sensor()
11 IO.popen('sensors', 'r') do |input|
12 result = Hash.new
13 input.each{ |temp|
14 key, value = temp.split(':')
15 if(value != nil and key =~ /Core|temp/) then
16 result[key] = value.trim_n
17 end
18 }
19 hddtemp().each{ |temp|
20 if(temp[:temp] =~ /^-?[0-9]+(\.[0-9]+)?$/) then
21 result[temp[:path]] = temp[:temp].to_f
22 end
23 }
24 notice result.inspect
25 return result
26 end
27 error 'sensorsの起動に失敗: lm_sensorはインストールされていますか?'
28 return false
29 end
30
31 def cputemp(temps = nil)
32 temps = self.sensor unless temps
33 temps['Core 0'] or temps['temp1']
34 end
35
36 def hddtemp()
37 begin
38 s_temp = TCPSocket.open("localhost", 7634)
39 raw = s_temp.read
40 s_temp.close
41 keys = [:path, :device, :temp, :identity]
42 raw.split('||').map{|node| Hash[*keys.zip(node.split('|').select{|r| !r.empty? }).flatten]}
43 rescue Errno::ECONNREFUSED
44 []
45 end
46 end
47
48 def cpulate()
49 open('/proc/stat', 'r') do |psh|
50 if(psh.readline =~ /^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/) then
51 user = $1.to_i; nice = $2.to_i; sys = $3.to_i; idle = $4.to_i
52 lastall = @all
53 lastbusy = @busy
54 @all = (@busy = user + nice + sys) + idle
55 if lastall then
56 return (lastbusy - @busy) * 100 / (lastall - @all)
57 end
58 end
59 end
60 end
61
62 end
+0
-7
devel/chiskel/core/plugin/ChangeLog less more
0 2009-10-26 Toshiaki Asai <qtoship@gmail.com>
1
2 * uptime.rb (Plugin::Uptime::on_nextday): 経過日数に一日足した値を
3 報告するようにした
4 (Plugin::Uptime::on_bootfirst): タグをuptimeに変更
5 (Plugin::Uptime::on_nextday): タグをuptimeに変更
6
+0
-81
devel/chiskel/core/plugin/auth.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # Auth
3 # ログイン履歴をつぶやくプラグイン
4 #
5
6 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
7 miquire :plugin, 'plugin'
8 miquire :core, 'autotag'
9
10 Module.new do
11 AuthDebugMode = false
12
13 reg_sshlogin = /Accepted ([^\s]+) for ([^\s]+) from ([^\s]+) port ([^\s]+) (ssh[^\s]+)/
14 localip = /^(10|127|192|169\.254|172\.(1[6-9]|2\d|30|31))\./
15 size = 0
16 @movable = lazy{ FileTest.readable_real?(auth_log_file) }
17 logsize = lazy{ if (@movable && !AuthDebugMode) then FileTest.size(auth_log_file) else 0 end }
18
19 plugin = Plugin::create(:auth)
20 plugin.add_event(:period){ |service|
21 if (log_avail? and has_newlog?) then
22 log_read { |log|
23 parse = reg_sshlogin.match(log)
24 if parse then
25 notice "auth: log receive '#{log.chomp}' > match"
26 vals = parse.to_a
27 vals[0] = log
28 Hash[*[:log, :auth, :user, :host, :port, :protocol].zip(vals).flatten]
29 else
30 notice "auth: log receive '#{log.chomp}' > not match"
31 nil
32 end
33 }.compact.each{ |log|
34 if(log[:user] == "root") then
35 if(localip === log[:host]) then
36 # for root login by local
37 return service.post(:message => "#{log[:host]}からrootでのログインを確認。",
38 :tags => [:auth,:warn])
39 else
40 # for root login by remote
41 return service.post(:message => "#{log[:host]}からrootでのログインを確認。",
42 :tags => [:auth, :critical])
43 end
44 elsif(log[:auth] == "password" && !(localip === log[:host])) then
45 # for password login by remote
46 return service.post(:message => "#{log[:host]}からユーザ#{log[:user]}でパスワードでのログインを確認。グローバルからは公開鍵のほうがいいと思う。",
47 :tags => [:auth, :warn])
48 elsif(log[:auth] == "password") then
49 # for password login by local
50 return service.post(:message => "#{log[:host]}からユーザ#{log[:user]}でパスワードでのログインを確認。",
51 :tags => [:auth, :notice])
52 elsif(!(localip === log[:host])) then
53 # for not password login by remote
54 return service.post(:message => "#{log[:host]}からユーザ#{log[:user]}でのログインを確認。",
55 :tags => [:auth, :notice]) end } end }
56
57 # 監視を実行すべきかどうかを判定する。
58 # もし、一度ログを削除されていたら、@logsizeを0に設定する
59 def self.log_avail?()
60 if (@movable and FileTest.exist?(auth_log_file)) then
61 true
62 else
63 logsize = 0
64 false end end
65
66 # 新しいログがあるようであれば真を返す
67 def self.has_newlog?()
68 logsize = FileTest.size(auth_log_file)
69 logsize > logsize end
70
71 def self.log_read(offset=logsize)
72 logsize = FileTest.size(auth_log_file)
73 notice "auth: read file #{auth_log_file} from #{offset} bytes"
74 IO.read(logfile, nil, offset).map{ |r| yield(r) } end
75
76 def self.auth_log_file()
77 if(AuthDebugMode)
78 'auth.log'
79 else
80 '/var/log/auth.log' end end end
+0
-53
devel/chiskel/core/plugin/diskobserver.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # ディスク使用量プラグイン
3 #
4
5 # ディスク使用量が切迫していたらTweetする
6
7 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
8 miquire :lib, 'sensor'
9 require_if_exist 'rubygems'
10 require_if_exist 'sys/filesystem'
11
12 if defined? Sys::Filesystem
13 Module.new do
14 NR_statfs = 99
15 MTAB = '/etc/mtab'
16
17 @store = ConfigLoader.create("Plugin::DiskObserver")
18 plugin = Plugin::create(:uptime)
19
20 plugin.add_event(:period){ |service|
21 if @store.at(:last_tweet, 0).to_i + 86400 > Time.new.to_i
22 notice("diskobserver: next check time:" + Time.at(@store.at(:last_tweet, 0) + 86400).inspect)
23 else
24 r = detect
25 if(r)
26 service.post(r)
27 else
28 notice('diskobserver: no critical disks') end end }
29
30 # 七割以上使われているパーティションを警告するメッセージを生成する
31 def self.detect
32 warnings = []
33 observer_divide[7, 3].each_with_index {|v, i|
34 warnings.push "#{v.join('と')}が#{i+7}割" if !v.empty? }
35 if not warnings.empty?
36 result = {:message => "#{warnings.join('、')}使われています。", :tags => [:diskobserver]}
37 @store.store(:last_tweet, Time.now.to_i)
38 return result end
39 nil end
40
41 # 各パーティションを使用されている容量10%ごとに配列に分ける
42 def self.observer_divide
43 disk_using = Array.new(10){ [] }
44 Sys::Filesystem.mounts do |fs|
45 stat = Sys::Filesystem.stat(fs.mount_point)
46 if !['none','usbdevfs','proc','tmpfs'].include?(fs.mount_type) and stat.blocks_available != 0
47 disk_using[(per_of_use(stat) * 10).to_i].push(fs.mount_point) end end
48 disk_using end
49
50 def self.per_of_use(stat)
51 (stat.blocks - stat.blocks_available).to_f / stat.blocks end end
52 end
+0
-58
devel/chiskel/core/plugin/kiriban.rb less more
0 #-*- coding: utf-8 -*-
1
2 Module.new do
3
4 @last_forenotify = Hash.new{ Hash.new(1000) }
5
6 # 毎分のイベントハンドラ
7
8 plugin = Plugin::create(:kiriban)
9 plugin.add_event(:update){ |service, messages|
10 messages.each{ |message| onupdate(service, message) } }
11
12 plugin.add_event(:mention){ |service, messages|
13 messages.each{ |message|
14 message.user.follow if /フォローして/ === message.to_s } }
15
16 def self.onupdate(watch, message)
17 if(!message[:user] or message[:user][:idname] == watch.user) then
18 notice "kiriban: reject message user #{message[:user].inspect}"
19 return nil
20 end
21 number = message[:statuses_count]
22 if(number) then
23 number = number.to_i
24 seed = (rand(10) + 1) * 10**rand(2)
25 if(self.kiriban?(number)) then
26 @last_forenotify[message[:user][:idname]][number] = 0
27 return watch.post(:message => "#{number}回目のツイートです!おめでとう!",
28 :tags => [self.name],
29 :retweet => message)
30 elsif(self.kiriban?(number + seed)) then
31 if(@last_forenotify[message[:user][:idname]][number] > seed) then
32 @last_forenotify[message[:user][:idname]][number] = seed
33 return watch.post(:message => "あと#{seed}回で#{number + seed}ツイート達成です!もうちょっと!",
34 :tags => [self.name],
35 :replyto => message)
36 end
37 end
38 notice "kiriban: tweet count #{number} #{message.inspect}"
39 end
40 end
41
42 def self.kiriban?(num)
43 if(num >= 100) then
44 multiple_number?(num) or zerofill_number?(num)
45 end
46 end
47
48 def self.multiple_number?(num)
49 (num.to_s =~ /^(\d)\1+$/) == 0
50 end
51
52 def self.zerofill_number?(num)
53 (num.to_s =~ /^\d0+$/) == 0
54 end
55
56 end
57
+0
-55
devel/chiskel/core/plugin/loadaverage.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # CPU Load
3 #
4
5 # Display Load Average as graph if support gruff
6
7 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
8 miquire :plugin, 'plugin'
9 miquire :lib, 'graph'
10
11 require 'tempfile'
12
13 if command_exist?('uptime')
14 Module.new do
15
16 def self.boot
17 config = confload("#{Environment::CONFROOT}loadaverage")
18 @interval = config.fetch(:interval, 60 * 24)
19 reset()
20 end
21
22 plugin = Plugin::create(:ping)
23 plugin.add_event(:period){ |service|
24 mins = load_average
25 @figure_la['1min'] << mins[0]
26 @figure_la['5min'] << mins[1]
27 @figure_la['15min'] << mins[2]
28 @label.call(nil)
29 p @lastcheck
30 p @interval
31 if((@lastcheck + (@interval*60)) < Time.now) then
32 notice "loadaverage: figure tweet"
33 service.post(Graph.drawgraph(@figure_la,
34 :title => 'Load Average',
35 :label => @label,
36 :tags => ['loadaverage'],
37 :start => @lastcheck,
38 :end => Time.now))
39 reset end
40 notice "loadaverage: next graph upload: #{(@lastcheck + (@interval*60) - Time.now)/60} min after" }
41
42 def self.reset
43 @lastcheck = Time.now
44 @figure_la = Hash['1min', [], '5min', [], '15min', []]
45 @label = Graph.gen_graph_label_defer end
46
47 def self.load_average
48 open('| uptime'){ |uptime|
49 if(/load averages?:\s*(\d+\.\d+)[^\d]+(\d+\.\d+)[^\d]+(\d+\.\d+)/ === uptime.read)
50 return $1.to_f, $2.to_f, $3.to_f end } end
51
52 boot
53 end
54 end
+0
-78
devel/chiskel/core/plugin/ping.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # 起動時間プラグイン
3 #
4
5 # システムの起動・終了などをTweetする
6
7 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
8 miquire :plugin, 'plugin'
9 miquire :core, 'environment'
10
11 class Plugin::Ping
12 include ConfigLoader
13 MAX_CHECK_INTERVAL = 64
14 CONFIGFILE = "#{Environment::CONFROOT}ping"
15
16 def initialize
17 @config = FileTest.exist?(CONFIGFILE) ? confload(CONFIGFILE) : {}
18 @checkinterval = Hash.new
19 plugin = Plugin::create(:ping)
20 plugin.add_event(:period, &method(:onperiod))
21 end
22
23 def onperiod(watch)
24 @config.map{|node|
25 Thread.new(node){ |host|
26 nextcheck = checkinterval(host)[:next]
27 if(nextcheck > 0) then
28 notice "ping: #{host['host']} checks about #{nextcheck} minutes after"
29 checkinterval(host)[:next] -= 1
30 Thread.exit
31 end
32 hostname = host['host']
33 if(hostname && system(sprintf(host['command'] || 'ping -c 1 -i 1 %s', hostname) + ' > /dev/null')) then
34 notice "ping: #{hostname} living"
35 check_success(host)
36 if(at(hostname)) then
37 store(hostname, false)
38 if(host['find']) then
39 watch.post(:message => host['find'])
40 end
41 end
42 else
43 notice "ping: #{hostname}: no route to host"
44 check_fail(host)
45 store("#{hostname}-nextcheck", 1)
46 unless(at(hostname)) then
47 store(hostname, true)
48 if(host['lost']) then
49 watch.post(:message => host['lost'])
50 end
51 end
52 end
53 }
54 }.each{|thread| thread.join }
55 end
56
57 def check_success(host)
58 wait = checkinterval(host)[:success] += 1
59 checkinterval(host)[:next] = [wait << 2, MAX_CHECK_INTERVAL].min
60 end
61
62 def check_fail(host)
63 wait = checkinterval(host)[:success] = 0
64 checkinterval(host)[:next] = 0
65 end
66
67 def checkinterval(host)
68 if not(@checkinterval[host['host'].to_s]) then
69 @checkinterval[host['host'].to_s] = {:next => 0, :success => 0}
70 end
71 return @checkinterval[host['host'].to_s]
72 end
73 end
74
75 if(command_exist?('ping')) then
76 Plugin::Ping.new
77 end
+0
-401
devel/chiskel/core/plugin/plugin.rb less more
0 # -*- coding: utf-8 -*-
1 # Plugin
2 #
3
4 miquire :core, 'configloader'
5 miquire :core, 'environment'
6 miquire :core, 'delayer'
7
8 require 'monitor'
9 require 'set'
10 require 'thread'
11
12 #
13 #= Plugin プラグイン管理/イベント管理モジュール
14 #
15 # CHIコアにプラグインを報告します。
16 # Plugin.create でPluginTagのインスタンスを作り、コアにプラグインを登録します。
17 # イベントリスナーの登録とイベントの発行については、Plugin::PluginTagを参照してください。
18 #
19 #== プラグインの実行順序
20 # まず、Plugin.call()が呼ばれると、予めadd_event_filter()で登録されたフィルタ関数に
21 # 引数が順次通され、最終的な戻り値がadd_event()に渡される。イメージとしては、
22 # イベントリスナ(*フィルタ(*引数))というかんじ。
23 # リスナもフィルタも、実行される順序は特に規定されていない。
24 module Plugin
25 extend Gem::Deprecate
26
27 @@eventqueue = Queue.new
28
29 Thread.new{
30 while proc = @@eventqueue.pop
31 proc.call end
32 }
33
34 def self.gen_event_ring
35 Hash.new{ |hash, key| hash[key] = [] }
36 end
37 @@event = gen_event_ring # { event_name => [[plugintag, proc]] }
38 @@add_event_hook = gen_event_ring
39 @@event_filter = gen_event_ring
40
41 # イベントリスナーを追加する。
42 def self.add_event(event_name, tag, &callback)
43 @@event[event_name.to_sym] << [tag, callback]
44 call_add_event_hook(event_name, callback)
45 callback end
46
47 # イベントフィルタを追加する。
48 # フィルタは、イベントリスナーと同じ引数で呼ばれるし、引数の数と同じ数の値を
49 # 返さなければいけない。
50 def self.add_event_filter(event_name, tag, &callback)
51 @@event_filter[event_name.to_sym] << [tag, callback]
52 callback end
53
54 def self.fetch_event(event_name, tag, &callback)
55 call_add_event_hook(event_name, callback)
56 callback end
57
58 def self.add_event_hook(event_name, tag, &callback)
59 @@add_event_hook[event_name.to_sym] << [tag, callback]
60 callback end
61
62 def self.detach(event_name, event)
63 deleter = lambda{|events| events[event_name.to_sym].reject!{ |e| e[1] == event } }
64 deleter.call(@@event) or deleter.call(@@event_filter) or deleter.call(@@add_event_hook) end
65
66 # フィルタ関数を用いて引数をフィルタリングする
67 def self.filtering(event_name, *args)
68 length = args.size
69 catch(:filter_exit){
70 @@event_filter[event_name.to_sym].inject(args){ |store, plugin|
71 result = store
72 plugintag, proc = *plugin
73 boot_plugin(plugintag, event_name, :filter, false){
74 result = proc.call(*store){ |result| throw(:filter_exit, result) }
75 if length != result.size
76 raise "filter changes arguments length (#{length} to #{result.size})" end
77 result } } } end
78
79 # イベント _event_name_ を呼ぶ予約をする。第二引数以降がイベントの引数として渡される。
80 # 実際には、これが呼ばれたあと、することがなくなってから呼ばれるので注意。
81 def self.call(event_name, *args)
82 SerialThread.new{
83 plugin_callback_loop(@@event, event_name, :proc, *filtering(event_name, *args)) } end
84
85 # イベントが追加されたときに呼ばれるフックを呼ぶ。
86 # _callback_ には、登録されたイベントのProcオブジェクトを渡す
87 def self.call_add_event_hook(event_name, callback)
88 plugin_callback_loop(@@add_event_hook, event_name, :hook, callback) end
89
90 # plugin_loopの簡略化版。プラグインに引数 _args_ をそのまま渡して呼び出す
91 def self.plugin_callback_loop(ary, event_name, kind, *args)
92 plugin_loop(ary, event_name, kind){ |tag, proc|
93 proc.call(*args){ throw(:plugin_exit) } } end
94
95 # _ary_ [ _event\_name_ ] に登録されているプラグイン一つひとつを引数に _proc_ を繰り返し呼ぶ。
96 # _proc_ のシグニチャは以下の通り。
97 # _proc_ ( プラグイン名, コールバック )
98 def self.plugin_loop(ary, event_name, kind, &proc)
99 ary[event_name.to_sym].each{ |plugin|
100 boot_plugin(plugin.first, event_name, kind){
101 proc.call(*plugin) } } end
102
103 # プラグインを起動できるならyieldする。コールバックに引数は渡されない。
104 def self.boot_plugin(plugintag, event_name, kind, delay = true, &routine)
105 if(plugintag.active?)
106 if(delay)
107 Delayer.new{ call_routine(plugintag, event_name, kind, &routine) }
108 else
109 call_routine(plugintag, event_name, kind, &routine) end end end
110
111 # プラグインタグをなければ作成して返す。
112 def self.create(name)
113 PluginTag.create(name) end
114
115 # ブロックの実行時間を記録しながら実行
116 def self.call_routine(plugintag, event_name, kind)
117 catch(:plugin_exit){ yield } end
118 # begin
119 # yield
120 # rescue Exception => e
121 # plugin_fault(plugintag, event_name, kind, e) end
122
123 # 登録済みプラグインの一覧を返す。
124 # 返すHashは以下のような構造。
125 # { plugin tag =>{
126 # event name => [proc]
127 # }
128 # }
129 # plugin tag:: Plugin::PluginTag のインスタンス
130 # event name:: イベント名。Symbol
131 # proc:: イベント発生時にコールバックされる Proc オブジェクト。
132 def self.plugins
133 result = Hash.new{ |hash, key|
134 hash[key] = Hash.new{ |hash, key|
135 hash[key] = [] } }
136 @@event.each_pair{ |event, pair|
137 result[pair[0]][event] << proc }
138 result
139 end
140
141 # 登録済みプラグイン名を一次元配列で返す
142 def self.plugin_list
143 Plugin::PluginTag.plugins end
144
145 # プラグイン処理中に例外が発生した場合、アプリケーションごと落とすかどうかを返す。
146 # trueならば、その場でバックトレースを吐いて落ちる、falseならエラーを表示してプラグインをstopする
147 def self.abort_on_exception?
148 true end
149
150 def self.plugin_fault(plugintag, event_name, event_kind, e)
151 error e
152 if abort_on_exception?
153 abort
154 else
155 Plugin.call(:update, nil, [Message.new(:message => "プラグイン #{plugintag} が#{event_kind} #{event_name} 処理中にクラッシュしました。プラグインの動作を停止します。\n#{e.to_s}",
156 :system => true)])
157 plugintag.stop! end end
158 end
159
160 =begin rdoc
161
162 = Plugin プラグインタグクラス
163
164 プラグインを一意に識別するためのタグ。
165 newは使わずに、 Plugin.create でインスタンスを作ること。
166
167 == イベントの種類
168
169 以下に、監視できる主なイベントを示す。
170
171 === boot(Post service)
172 起動時に、どのイベントよりも先に一度だけ呼ばれる。
173
174 === period(Post service)
175 毎分呼ばれる。必ず60秒ごとになる保証はない。
176
177 === update(Post service, Array messages)
178 フレンドタイムラインが更新されたら呼ばれる。ひとつのつぶやきはかならず1度しか引数に取られず、
179 _messages_ には同時に複数の Message のインスタンスが渡される(ただし、削除された場合は削除フラグを
180 立てて同じつぶやきが流れる)。
181
182 === mention(Post service, Array messages)
183 updateと同じ。ただし、自分宛のリプライが来たときに呼ばれる点が異なる。
184
185 === posted(Post service, Array messages)
186 自分が投稿したメッセージ。
187
188 === appear(Array messages)
189 updateと同じ。ただし、タイムライン、検索結果、リスト等、受信したすべてのつぶやきを対象にしている。
190
191 === message_modified(Message message)
192 messageの内容が変わったときに呼ばれる。
193 おもに、ふぁぼられ数やRT数が変わったときに呼ばれる。
194
195 === list_data(Post service, Array ulist)
196 フォローしているリスト一覧に変更があれば呼ばれる。なお、このイベントにリスナーを登録すると、すぐに
197 現在フォローしているリスト一覧を引数にコールバックが呼ばれる。
198
199 === list_created(Post service, Array ulist)
200 新しくリストが作成されると、それを引数に呼ばれる。
201
202 === list_destroy(Post service, Array ulist)
203 リストが削除されると、それを引数に呼ばれる。
204
205 === mui_tab_regist(Gtk::Widget container, String label, String image=nil)
206 ウィンドウにタブを追加する。 _label_ はウィンドウ内での識別名にも使われるので一意であること。
207 _image_ は画像への相対パスかURLで、通常は #MUI::Skin.get_path の戻り値を使う。
208 _image_ が省略された場合は、 _label_ が使われる。
209
210 === mui_tab_remove(String label)
211 ラベル _label_ をもつタブを削除する。
212
213 === mui_tab_active(String label)
214 ラベル _label_ のついたタブをアクティブにする。
215
216 === apilimit(Time time)
217 サーバによって、時間 _time_ までクエリの実行を停止された時に呼ばれる。
218
219 === apifail(String text)
220 何らかのクエリが実行に失敗した場合に呼ばれる。サーバからエラーメッセージが帰ってきた場合は
221 _text_ に渡される。エラーメッセージが得られなかった場合はnilが渡される。
222
223 === apiremain(Integer remain, Time expire, String transaction)
224 サーバへのクリエ発行が時間 _expire_ までに _remain_ 回実行できることを通知するために呼ばれる。
225 現在のTwitterの仕様では、クエリを発行するたびにこれが呼ばれる。
226
227 === ipapiremain(Integer remain, Time expire, String transaction)
228 基本的にはapiremainと同じだが、IPアドレス規制について動くことが違う。
229
230 === rewindstatus(String mes)
231 ユーザに情報 _mes_ を「さりげなく」提示する。 GUI プラグインがハンドルしていて、ステータスバーを
232 更新する。
233
234 === retweet(Array messages)
235 リツイートを受信したときに呼ばれる
236
237 === favorite(Post service, User user, Message message)
238 _user_ が _message_ をお気に入りに追加した時に呼ばれる。
239
240 === unfavorite(Post service, User user, Message message)
241 _user_ が _message_ をお気に入りから外した時に呼ばれる。
242
243 === after_event(Post service)
244 periodなど、毎分実行されるイベントのクロールが終わった後に呼び出される。
245
246 === play_sound(String filename)
247 ファイル名 _filename_ の音楽ファイルを鳴らす。
248
249 === popup_notify(User user, String text)
250 通知を表示する。雰囲気としては、
251 - Windows : バルーン
252 - Linux : libnotify
253 - Mac : Growl
254 みたいなイメージの通知。 _user_ のアイコンが使われ、名前がタイトルになり、本文は _text_ が使われる。
255
256 === query_start(:serial => Integer, :method => Symbol|String, :path => String, :options => Hash, :start_time => Time)
257 HTTP問い合わせが始まった時に呼ばれる。
258 serial::
259 コネクションのID
260 method::
261 HTTPメソッド名。GETやPOSTなど
262 path::
263 サーバ上のパス。/statuses/show.json など
264 options::
265 雑多な呼び出しオプション。
266 start_time::
267 クエリの開始時間
268
269 === query_end(:serial => Integer, :method => Symbol|String, :path => String, :options => Hash, :start_time => Time, :end_time => Time, :res => Net::HTTPResponse|Exception)
270 HTTP問い合わせが終わった時に呼ばれる。
271 serial::
272 コネクションのID
273 method::
274 HTTPメソッド名。GETやPOSTなど
275 path::
276 サーバ上のパス。/statuses/show.json など
277 options::
278 雑多な呼び出しオプション。
279 start_time::
280 クエリの開始時間
281 end_time::
282 クエリのレスポンスを受け取った時間。
283 res::
284 受け取ったレスポンス。通常はNet::HTTPResponseを渡す。捕捉できない例外が発生した場合はここにその例外を渡す。
285
286 == フィルタ
287
288 以下に、フックできる主なフィルタを示す。
289
290 === favorited_by(Message message, Set users)
291 _message_ をお気に入りに入れているユーザを取得するためのフック。
292 _users_ は、お気に入りに入れているユーザの集合。
293
294 === show_filter(Enumerable messages)
295 _messages_ から、表示してはいけないものを取り除く
296
297 =end
298 class Plugin::PluginTag
299
300 include ConfigLoader
301
302 @@plugins = [] # plugin
303
304 attr_reader :name
305 alias to_s name
306
307 def initialize(name = :anonymous)
308 @name = name
309 active!
310 register end
311
312 # 新しくプラグインを作成する。もしすでに同じ名前で作成されていれば、新しく作成せずにそれを返す。
313 def self.create(name)
314 plugin = @@plugins.find{ |p| p.name == name }
315 if plugin
316 plugin
317 else
318 Plugin::PluginTag.new(name) end end
319
320 def self.plugins
321 @@plugins
322 end
323
324 # イベント _event_name_ を監視するイベントリスナーを追加する。
325 def add_event(event_name, &callback)
326 Plugin.add_event(event_name, self, &callback)
327 end
328
329 # イベントフィルタを設定する。
330 # フィルタが存在した場合、イベントが呼ばれる前にイベントフィルタに引数が渡され、戻り値の
331 # 配列がそのまま引数としてイベントに渡される。
332 # フィルタは渡された引数と同じ長さの配列を返さなければいけない。
333 def add_event_filter(event_name, &callback)
334 Plugin.add_event_filter(event_name, self, &callback)
335 end
336
337 def fetch_event(event_name, &callback)
338 Plugin.fetch_event(event_name, self, &callback)
339 end
340
341 # イベント _event_name_ にイベントが追加されたときに呼ばれる関数を登録する。
342 def add_event_hook(event_name, &callback)
343 Plugin.add_event_hook(event_name, self, &callback)
344 end
345
346 # イベントの監視をやめる。引数 _event_ には、add_event, add_event_filter, add_event_hook の
347 # いずれかの戻り値を与える。
348 def detach(event_name, event)
349 Plugin.detach(event_name, event)
350 end
351
352 def at(key, ifnone=nil)
353 super("#{@name}_#{key}".to_sym, ifnone) end
354
355 def store(key, val)
356 super("#{@name}_#{key}".to_sym, val) end
357
358 def stop!
359 @status = :stop end
360
361 def stop?
362 @status == :stop end
363
364 def active!
365 @status = :active end
366
367 def active?
368 @status == :active end
369
370 private
371
372 def register
373 @@plugins.push(self) end
374 alias :regist :register
375 deprecate :regist, "register", 2016, 12
376
377 end
378
379 Module.new do
380 def self.gen_never_message_filter
381 appeared = Set.new
382 lambda{ |service, messages|
383 [service,
384 messages.select{ |m|
385 appeared.add(m[:id].to_i) if m and not(appeared.include?(m[:id].to_i)) }] } end
386
387 def self.never_message_filter(event_name, *other)
388 Plugin.create(:core).add_event_filter(event_name, &gen_never_message_filter)
389 never_message_filter(*other) unless other.empty?
390 end
391
392 never_message_filter(:update, :mention)
393
394 Plugin.create(:core).add_event(:appear){ |messages|
395 retweets = messages.select(&:retweet?)
396 if not(retweets.empty?)
397 Plugin.call(:retweet, retweets) end }
398 end
399
400 miquire :plugin # if defined? Test::Unit
+0
-9
devel/chiskel/core/plugin/plugin_class.rb less more
0 #
1 # Plugin class
2 #
3
4 # define plugin as class
5
6
7
8
+0
-89
devel/chiskel/core/plugin/streaming.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # Revolution!
3 #
4
5 require 'timeout'
6 miquire :core, 'userconfig'
7
8 Module.new do
9
10 @thread = nil
11
12 plugin = Plugin::create(:streaming)
13
14 plugin.add_event(:boot){ |service|
15 @service = service
16 start if UserConfig[:realtime_rewind] }
17
18 UserConfig.connect(:realtime_rewind){ |key, new_val, before_val, id|
19 if new_val
20 Delayer.new{ self.start }
21 else
22 @thread.kill if @thread
23 end
24 }
25
26 def self.start
27 unless @thread and @thread.alive?
28 @thread = Thread.new{
29 while(UserConfig[:realtime_rewind])
30 sleep(3)
31 catch(:streaming_break){
32 start_streaming{ |q|
33 throw(:streaming_break) unless(UserConfig[:realtime_rewind])
34 Delayer.new(Delayer::NORMAL, q.strip, &method(:trigger_event)) } } end } end end
35
36 def self.trigger_event(query)
37 begin
38 return nil if not /^\{.*\}$/ === query
39 json = JSON.parse(query)
40 case
41 when json['friends'] then
42 when json['event'] == 'favorite' then
43 by = @service.__send__(:parse_json, json['source'], :user_show)
44 to = @service.__send__(:parse_json, json['target_object'], :status_show)
45 to.first.add_favorited_by(by.first, Time.parse(json['created_at']))
46 when json['event'] == 'unfavorite' then
47 by = @service.__send__(:parse_json, json['source'], :user_show)
48 to = @service.__send__(:parse_json, json['target_object'], :status_show)
49 to.first.remove_favorited_by(by.first)
50 when json['event'] == 'follow' then
51 source = @service.__send__(:parse_json, json['source'], :user_show).first
52 target = @service.__send__(:parse_json, json['target'], :user_show).first
53 if(target.me?)
54 Plugin.call(:followers_created, @service, [source])
55 elsif(source.me?)
56 Plugin.call(:followings_created, @service, [target])
57 end
58 when json['delete'] then
59 if $debug
60 Plugin.call(:update, nil, [Message.new(:message => YAML.dump(json),
61 :system => true)]) end
62 when !json.has_key?('event') then
63 messages = @service.__send__(:parse_json, json, :streaming_status)
64 if messages
65 messages.each{ |msg|
66 Plugin.call(:update, @service, [msg])
67 Plugin.call(:mention, @service, [msg]) if msg.to_me?
68 Plugin.call(:mypost, @service, [msg]) if msg.from_me? }
69 elsif $debug
70 Plugin.call(:update, nil, [Message.new(:message => YAML.dump(json),
71 :system => true)]) end
72 when $debug
73 Plugin.call(:update, nil, [Message.new(:message => YAML.dump(json),
74 :system => true)])
75 end
76 rescue Exception => e
77 notice e
78 end end
79
80 def self.start_streaming(&proc)
81 begin
82 @service.streaming(&proc)
83 rescue Exception => e
84 error e
85 end
86 end
87 end
88
+0
-90
devel/chiskel/core/plugin/temperature.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # Temperature
3 #
4
5 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
6 miquire :lib, 'sensor'
7 miquire :lib, 'graph'
8
9 if(command_exist?('sensors'))
10 Module.new do
11 CONFIGFILE = "#{Environment::CONFROOT}temperature"
12
13 def self.boot
14 @store = ConfigLoader.create("Plugin::Temperature")
15 @sensor = Sensor.new
16 getconfig
17 graph_reset end
18
19 def self.getconfig
20 config = FileTest.exist?(CONFIGFILE) ? confload(CONFIGFILE) : {}
21 @identity = config.fetch(:identity, 'C')
22 @critical = config.fetch(:critical, 70)
23 @max_temp = config.fetch(:max_temp, 100)
24 @interval = config.fetch(:interval, 60 * 24) end
25
26 plugin = Plugin::create(:ping)
27 plugin.add_event(:period){ |service|
28 temps = @sensor.sensor
29 graph_temp = temps.dup
30 @sensor.hddtemp.each{ |temp|
31 graph_temp[temp[:path]] = temp[:temp] }
32 self.graph_add(graph_temp, service)
33 temp = @sensor.cputemp(temps)
34 if temp
35 if is_critical?(temp)
36 return service.post(:message => "#{temp}#{@identity}なう。熱いです・・・。",
37 :tags => [:tempreture, :critical]) end
38 if counter = @store.at(:counter, 0)
39 @store.store(:counter, counter.abs - 1) end end }
40
41 def self.is_critical?(temp)
42 limit = @store.at(:critical_limit, 0)
43 if (temp >= (limit + @critical)) then
44 @store.store(:critical_limit, @max_temp - @critical)
45 return true
46 elsif (limit != 0)
47 @store.store(:critical_limit, limit.abs - 1) end
48 return false end
49
50 def self.samples(temp)
51 samp = @store.at(:samp, [])
52 samp.unshift(temp)
53 @store.store(:samp, samp[0..@interval])
54 return samp[1..@interval+1] end
55
56 def self.graph_add(temps, watch)
57 notice temps.inspect
58 temps.each{|key, stat|
59 if(not @cputemp[key].is_a?(Array)) then
60 @cputemp[key] = Array.new
61 end
62 @cputemp[key][@temp_count] = stat.to_f
63 }
64 @count_label.call(nil)
65 @temp_count += 1
66 if(@temp_count >= @interval) then
67 watch.post(Graph.drawgraph(@cputemp,
68 :start => @start_time,
69 :title => 'Temperature',
70 :tags => ['temperature'],
71 :label => @count_label,
72 :end => Time.now)){ |e, m|
73 Delayer.new{ raise m } if e == :fail
74 }
75 self.graph_reset
76 end
77 notice "temperature: next graph upload: #{@interval-@temp_count} minutes after"
78 end
79
80 def self.graph_reset
81 @count_label = Graph.gen_graph_label_defer()
82 @temp_count = 0
83 @cputemp = Hash.new
84 @start_time = Time.now
85 end
86
87 boot
88 end
89 end
+0
-29
devel/chiskel/core/plugin/template less more
0 #-*- coding: utf-8 -*-
1 #
2 # プラグインテンプレート
3 #
4
5 # これを基にプラグインを作って行く
6 # 別にこれを基にしなくてもいい
7 # 以下、MyPluginプラグインを作る例
8
9 require 'utils'
10 require 'plugin/plugin'
11
12 module Plugin
13 class MyPlugin < Plugin
14
15 # 毎分のイベントハンドラ
16 def onperiod(watch)
17 return watch.post(Message.new('hello, MyPlugin!', [self.name]))
18 end
19
20 def oncall(watch, message, tag)
21 return watch.post(Message.new("called by you", [self.name], message))
22 end
23 end
24
25 end
26
27 # プラグインの登録
28 Plugin::Ring.push Plugin::MyPlugin.new,[:period, :call]
+0
-44
devel/chiskel/core/plugin/uptime.rb less more
0 #-*- coding: utf-8 -*-
1 #
2 # 起動時間プラグイン
3 #
4
5 # システムの起動・終了などをTweetする
6
7 require File.expand_path(File.join(File.dirname(__FILE__),'..', 'utils'))
8 require 'plugin/plugin'
9 require_if_exist 'sys/uptime'
10
11 Module.new do
12
13 store = ConfigLoader.create("Plugin::Uptime")
14 plugin = Plugin::create(:uptime)
15
16 plugin.add_event(:boot){ |service|
17 if FileTest.exist?('/tmp/computer_tweet_uptime') then
18 false
19 else
20 open('/tmp/computer_tweet_uptime','w')
21 service.post(:message => "おはよー。 #uptime #period")
22 end }
23
24 if defined?(Sys::Uptime)
25 plugin.add_event(:period){ |service|
26 uptime = Sys::Uptime.seconds
27 last = store.at(:last, 0)
28 store.store(:last, uptime)
29 notice "last=#{dayof(last)}, uptime=#{dayof(uptime)}\n"
30 service.post(:message => on_nextday(uptime, last)) if(dayof(uptime) > dayof(last)) }
31 end
32
33 def self.dayof(s)
34 (s / 86400).to_i
35 end
36
37 def self.on_nextday(uptime, last)
38 unless dayof(uptime) then return false end
39 "連続起動#{dayof(uptime)+1}日目。 #uptime"
40 end
41
42 end
43
+0
-181
devel/debian/build.sh less more
0 #!/bin/bash
1
2 #**************************************************************
3 # mikutter environment builder for developer (debian & ubuntu)
4 #**************************************************************
5
6 RUBY_SERVER='http://ftp.ruby-lang.org/pub/ruby'
7 INSTALL_DIR='/opt/miku'
8 SRC_DIR="${INSTALL_DIR}/src"
9 DEPENDS='gcc make bzip2 wget pkg-config subversion
10 libgtk2.0-dev libyaml-dev libssl-dev zlib1g-dev'
11
12 if [ -e ${INSTALL_DIR} ] && [ -d ${INSTALL_DIR} ]; then
13 echo "mikutter setup to ${INSTALL_DIR}"
14 elif [ -e ${INSTALL_DIR} ] && [ ! -d ${INSTALL_DIR} ]; then
15 echo "${INSTALL_DIR} is not directory."
16 exit 1
17 else
18 mkdir -p ${INSTALL_DIR}
19 fi
20
21 if [ ! -w ${INSTALL_DIR} ]; then
22 echo "${INSTALL_DIR} is not writable."
23 exit 1
24 fi
25
26 if [ ! -w ${HOME} ]; then
27 echo "${HOME} is not writable."
28 exit 1
29 fi
30
31 mkdir -p ${SRC_DIR}
32 cd ${INSTALL_DIR}
33 cat > mikutter-update.sh <<EOF
34 #!/bin/bash
35 #- mikutter environment updater -
36
37 EOF
38
39 #-------------------------------------------------------
40 # Setup build environment
41 #-------------------------------------------------------
42 OLDLANG=${LANG}
43 LANG=C
44 LOG=`apt-get -sy install ${DEPENDS}`
45 LANG=${OLDLANG}
46
47 if [ ! `echo ${LOG} | grep "No packages will be" | sed 's/ //g'` ]; then
48 echo apt-get install ${DEPENDS}
49 if [ $UID = '0' ]; then
50 apt-get update
51 apt-get -y install ${DEPENDS}
52 else
53 sudo apt-get update
54 sudo apt-get -y install ${DEPENDS}
55 fi
56 fi
57
58 #-------------------------------------------------------
59 # Expand rubygems
60 #-------------------------------------------------------
61 echo 'download rubygems.'
62 GEMS_PATH=`wget -O- 'http://rubyforge.org/frs/?group_id=126&release_id=45671' | \
63 egrep -o 'href=".*.tgz"' | head -n 1 | egrep -o '/.*.tgz'`
64 GEMS_SRC=`echo ${GEMS_PATH} | sed 's/.*\///'`
65 GEMS_DIR=`echo ${GEMS_SRC} | sed 's/\.tgz//'`
66 if [ ! -e ${SRC_DIR}/${GEMS_SRC} ]; then
67 wget -P ${SRC_DIR} http://rubyforge.org${GEMS_PATH}
68 fi
69 tar xzf ${SRC_DIR}/${GEMS_SRC} -C ${SRC_DIR}
70
71
72 #-------------------------------------------------------
73 # Setup ruby environment
74 #-------------------------------------------------------
75 wget -O- ${RUBY_SERVER} |
76 egrep -o '1..?..?-p[0-9]{1,3}' | sort | uniq |
77 while read RUBY_VERSION; do
78 S_VERSION=`echo ${RUBY_VERSION} | sed 's/-p.*//' | sed 's/\.//g'`
79
80 # ruby 1.8.6 is not supported.
81 [ ${S_VERSION} = '186' ] && continue
82 # [ ${S_VERSION} = '191' ] && continue
83
84 RUBY_SRC=ruby-${RUBY_VERSION}.tar.bz2
85 # RUBY_SUFFIX=${S_VERSION}
86 RUBY_SUFFIX=""
87
88
89 # Download ruby source
90 if [ ! -e ${SRC_DIR}/${RUBY_SRC} ]; then
91 echo "download ${RUBY_SRC}"
92 wget ${RUBY_SERVER}/${RUBY_SRC} -P ${SRC_DIR}
93 else
94 echo "${RUBY_SRC} is already exist."
95 fi
96
97
98 # Build ruby
99 echo build ${RUBY_VERSION}
100 cd ${SRC_DIR}
101 tar xpf ${RUBY_SRC}
102 cd ruby-${RUBY_VERSION}
103 ./configure --prefix=${INSTALL_DIR}/rb${S_VERSION} \
104 --program-suffix="${RUBY_SUFFIX}" \
105 --enable-shared && \
106 make && make install
107
108
109 # Install rubygems(for 1.8.x)
110 echo "setup gems."
111 cd ${INSTALL_DIR}
112 if [ `echo ${RUBY_VERSION} | grep '1.8.'` ]; then
113 ${INSTALL_DIR}/rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} ${SRC_DIR}/${GEMS_DIR}/setup.rb
114 fi
115
116
117 # Install require libs
118 echo 'gem update --system'
119 ${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} update --system
120 echo 'gem install pkg-config'
121 ${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} install pkg-config
122 echo 'gem install ruby-hmac'
123 ${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} install ruby-hmac
124 echo 'gem install gtk2'
125 ${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} install gtk2
126
127
128 # Install scripts
129 cd ${INSTALL_DIR}
130 echo 'create start, debug, test scripts'
131 cat > mikutter-start${S_VERSION}.sh << EOS
132 #!/bin/bash
133
134 cd ${INSTALL_DIR}/mikutter
135 ../rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} \\
136 -rubygems mikutter.rb
137 EOS
138
139 cat > mikutter-debug${S_VERSION}.sh << EOS
140 #!/bin/bash
141
142 cd ${INSTALL_DIR}/mikutter
143 ../rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} -d \\
144 -rubygems mikutter.rb --debug
145 EOS
146
147 cat > mikutter-test${S_VERSION}.sh <<EOF
148 #!/bin/bash
149 #- mikutter test script -
150
151 cd ${INSTALL_DIR}/mikutter
152 ../rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} -v
153 ../rb${S_VERSION}/bin/gem${RUBY_SUFFIX} -v
154 ../rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} -rubygems \\
155 -e 'require "gtk2"; printf("Gtk2: %s\n", Gtk::VERSION.join("."))'
156 ../rb${S_VERSION}/bin/ruby${RUBY_SUFFIX} -rubygems \\
157 -e 'require "hmac"; printf("HMAC: %s\n", HMAC::VERSION)'
158 EOF
159
160 echo "${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} update --system" >> mikutter-update.sh
161 echo "${INSTALL_DIR}/rb${S_VERSION}/bin/gem${RUBY_SUFFIX} update" >> mikutter-update.sh
162 done
163
164
165 #-------------------------------------------------------
166 # Setup mikutter
167 #-------------------------------------------------------
168 cd ${INSTALL_DIR}
169 echo 'checkout mikutter'
170 svn co svn://mikutter.hachune.net/mikutter/trunk mikutter
171
172 cat >> mikutter-update.sh << EOS
173 cd ${INSTALL_DIR}/mikutter
174 svn up
175 EOS
176
177 chmod +x mikutter-*.sh
178
179 echo 'done.'
180
+0
-58
devel/makechi.rb less more
0 #!/usr/bin/ruby
1 # -*- coding: utf-8 -*-
2
3 =begin rdoc
4 Mikutterのプラグインをすべて削除した空のCHIを作成する
5 =end
6
7 require 'fileutils'
8
9 def get_config_data(name)
10 case name
11 when "NAME"
12 "chi"
13 when "ACRO"
14 "chi"
15 else
16 CHIConfig.const_get(name) end end
17
18 BASE = File.expand_path(File.dirname($0))
19 SRC = File.expand_path(File.join(File.dirname($0), '..'))
20 DEST = File.expand_path(File.join(File.dirname($0), 'src'))
21
22 Dir.chdir(BASE)
23
24 if FileTest.exist?(DEST)
25 FileUtils.rm_rf DEST
26 puts "directory #{DEST} already exist."
27 end
28
29 FileUtils.mkdir_p File.join(DEST, 'plugin')
30 FileUtils.cp File.join(SRC, 'mikutter.rb'), DEST
31 FileUtils.cp_r File.join(SRC, 'core'), DEST
32 FileUtils.rm_rf File.join(DEST, 'core', 'plugin', 'gui.rb')
33 FileUtils.rm_rf Dir.glob(File.join(DEST, 'core', 'addon', '*'))
34 FileUtils.cp_r Dir.glob(File.join(BASE, 'chiskel', '*')), DEST
35
36 LOAD_PLUGIN = File.join(DEST, 'core', 'boot', 'load_plugin.rb')
37 File.open(LOAD_PLUGIN){|f|
38 f = f.read
39 File.open(LOAD_PLUGIN, 'w'){|w|
40 f.each_line{|l| w.puts(l.gsub(/miquire :addon, 'addon'/, ''))}
41 }
42 }
43
44
45 Dir.chdir(File.join(DEST, 'core'))
46 require './config'
47 Dir.chdir(BASE)
48
49 open(File.join(DEST, "core/config.rb"), 'w'){ |out|
50 out.write([ '# -*- coding: utf-8 -*-','','module CHIConfig',
51 CHIConfig.constants.map{ |name|
52 value = get_config_data(name)
53 value.gsub!('mikutter', 'chi') if value.is_a? String
54 " #{name} = #{value.inspect}"
55 },
56 'end'].join("\n")) }
57
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 require "idn"
19
20 module Addressable
21 module IDNA
22 def self.punycode_encode(value)
23 IDN::Punycode.encode(value.to_s)
24 end
25
26 def self.punycode_decode(value)
27 IDN::Punycode.decode(value.to_s)
28 end
29
30 def self.unicode_normalize_kc(value)
31 IDN::Stringprep.nfkc_normalize(value.to_s)
32 end
33
34 def self.to_ascii(value)
35 value.to_s.split('.', -1).map do |segment|
36 if segment.size > 0 && segment.size < 64
37 IDN::Idna.toASCII(segment, IDN::Idna::ALLOW_UNASSIGNED)
38 elsif segment.size >= 64
39 segment
40 else
41 ''
42 end
43 end.join('.')
44 end
45
46 def self.to_unicode(value)
47 value.to_s.split('.', -1).map do |segment|
48 if segment.size > 0 && segment.size < 64
49 IDN::Idna.toUnicode(segment, IDN::Idna::ALLOW_UNASSIGNED)
50 elsif segment.size >= 64
51 segment
52 else
53 ''
54 end
55 end.join('.')
56 end
57 end
58 end
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 module Addressable
19 module IDNA
20 # This module is loosely based on idn_actionmailer by Mick Staugaard,
21 # the unicode library by Yoshida Masato, and the punycode implementation
22 # by Kazuhiro Nishiyama. Most of the code was copied verbatim, but
23 # some reformatting was done, and some translation from C was done.
24 #
25 # Without their code to work from as a base, we'd all still be relying
26 # on the presence of libidn. Which nobody ever seems to have installed.
27 #
28 # Original sources:
29 # http://github.com/staugaard/idn_actionmailer
30 # http://www.yoshidam.net/Ruby.html#unicode
31 # http://rubyforge.org/frs/?group_id=2550
32
33
34 UNICODE_TABLE = File.expand_path(
35 File.join(File.dirname(__FILE__), '../../..', 'data/unicode.data')
36 )
37
38 ACE_PREFIX = "xn--"
39
40 UTF8_REGEX = /\A(?:
41 [\x09\x0A\x0D\x20-\x7E] # ASCII
42 | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
43 | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
44 | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
45 | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
46 | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
47 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5
48 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
49 )*\z/mnx
50
51 UTF8_REGEX_MULTIBYTE = /(?:
52 [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
53 | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
54 | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
55 | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
56 | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
57 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4nil5
58 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
59 )/mnx
60
61 # :startdoc:
62
63 # Converts from a Unicode internationalized domain name to an ASCII
64 # domain name as described in RFC 3490.
65 def self.to_ascii(input)
66 input = input.to_s unless input.is_a?(String)
67 input = input.dup
68 if input.respond_to?(:force_encoding)
69 input.force_encoding(Encoding::ASCII_8BIT)
70 end
71 if input =~ UTF8_REGEX && input =~ UTF8_REGEX_MULTIBYTE
72 parts = unicode_downcase(input).split('.')
73 parts.map! do |part|
74 if part.respond_to?(:force_encoding)
75 part.force_encoding(Encoding::ASCII_8BIT)
76 end
77 if part =~ UTF8_REGEX && part =~ UTF8_REGEX_MULTIBYTE
78 ACE_PREFIX + punycode_encode(unicode_normalize_kc(part))
79 else
80 part
81 end
82 end
83 parts.join('.')
84 else
85 input
86 end
87 end
88
89 # Converts from an ASCII domain name to a Unicode internationalized
90 # domain name as described in RFC 3490.
91 def self.to_unicode(input)
92 input = input.to_s unless input.is_a?(String)
93 parts = input.split('.')
94 parts.map! do |part|
95 if part =~ /^#{ACE_PREFIX}(.+)/
96 begin
97 punycode_decode(part[/^#{ACE_PREFIX}(.+)/, 1])
98 rescue Addressable::IDNA::PunycodeBadInput
99 # toUnicode is explicitly defined as never-fails by the spec
100 part
101 end
102 else
103 part
104 end
105 end
106 output = parts.join('.')
107 if output.respond_to?(:force_encoding)
108 output.force_encoding(Encoding::UTF_8)
109 end
110 output
111 end
112
113 # Unicode normalization form KC.
114 def self.unicode_normalize_kc(input)
115 input = input.to_s unless input.is_a?(String)
116 unpacked = input.unpack("U*")
117 unpacked =
118 unicode_compose(unicode_sort_canonical(unicode_decompose(unpacked)))
119 return unpacked.pack("U*")
120 end
121
122 ##
123 # Unicode aware downcase method.
124 #
125 # @api private
126 # @param [String] input
127 # The input string.
128 # @return [String] The downcased result.
129 def self.unicode_downcase(input)
130 input = input.to_s unless input.is_a?(String)
131 unpacked = input.unpack("U*")
132 unpacked.map! { |codepoint| lookup_unicode_lowercase(codepoint) }
133 return unpacked.pack("U*")
134 end
135 (class <<self; private :unicode_downcase; end)
136
137 def self.unicode_compose(unpacked)
138 unpacked_result = []
139 length = unpacked.length
140
141 return unpacked if length == 0
142
143 starter = unpacked[0]
144 starter_cc = lookup_unicode_combining_class(starter)
145 starter_cc = 256 if starter_cc != 0
146 for i in 1...length
147 ch = unpacked[i]
148 cc = lookup_unicode_combining_class(ch)
149
150 if (starter_cc == 0 &&
151 (composite = unicode_compose_pair(starter, ch)) != nil)
152 starter = composite
153 startercc = lookup_unicode_combining_class(composite)
154 else
155 unpacked_result << starter
156 starter = ch
157 startercc = cc
158 end
159 end
160 unpacked_result << starter
161 return unpacked_result
162 end
163 (class <<self; private :unicode_compose; end)
164
165 def self.unicode_compose_pair(ch_one, ch_two)
166 if ch_one >= HANGUL_LBASE && ch_one < HANGUL_LBASE + HANGUL_LCOUNT &&
167 ch_two >= HANGUL_VBASE && ch_two < HANGUL_VBASE + HANGUL_VCOUNT
168 # Hangul L + V
169 return HANGUL_SBASE + (
170 (ch_one - HANGUL_LBASE) * HANGUL_VCOUNT + (ch_two - HANGUL_VBASE)
171 ) * HANGUL_TCOUNT
172 elsif ch_one >= HANGUL_SBASE &&
173 ch_one < HANGUL_SBASE + HANGUL_SCOUNT &&
174 (ch_one - HANGUL_SBASE) % HANGUL_TCOUNT == 0 &&
175 ch_two >= HANGUL_TBASE && ch_two < HANGUL_TBASE + HANGUL_TCOUNT
176 # Hangul LV + T
177 return ch_one + (ch_two - HANGUL_TBASE)
178 end
179
180 p = []
181 ucs4_to_utf8 = lambda do |ch|
182 if ch < 128
183 p << ch
184 elsif ch < 2048
185 p << (ch >> 6 | 192)
186 p << (ch & 63 | 128)
187 elsif ch < 0x10000
188 p << (ch >> 12 | 224)
189 p << (ch >> 6 & 63 | 128)
190 p << (ch & 63 | 128)
191 elsif ch < 0x200000
192 p << (ch >> 18 | 240)
193 p << (ch >> 12 & 63 | 128)
194 p << (ch >> 6 & 63 | 128)
195 p << (ch & 63 | 128)
196 elsif ch < 0x4000000
197 p << (ch >> 24 | 248)
198 p << (ch >> 18 & 63 | 128)
199 p << (ch >> 12 & 63 | 128)
200 p << (ch >> 6 & 63 | 128)
201 p << (ch & 63 | 128)
202 elsif ch < 0x80000000
203 p << (ch >> 30 | 252)
204 p << (ch >> 24 & 63 | 128)
205 p << (ch >> 18 & 63 | 128)
206 p << (ch >> 12 & 63 | 128)
207 p << (ch >> 6 & 63 | 128)
208 p << (ch & 63 | 128)
209 end
210 end
211
212 ucs4_to_utf8.call(ch_one)
213 ucs4_to_utf8.call(ch_two)
214
215 return lookup_unicode_composition(p)
216 end
217 (class <<self; private :unicode_compose_pair; end)
218
219 def self.unicode_sort_canonical(unpacked)
220 unpacked = unpacked.dup
221 i = 1
222 length = unpacked.length
223
224 return unpacked if length < 2
225
226 while i < length
227 last = unpacked[i-1]
228 ch = unpacked[i]
229 last_cc = lookup_unicode_combining_class(last)
230 cc = lookup_unicode_combining_class(ch)
231 if cc != 0 && last_cc != 0 && last_cc > cc
232 unpacked[i] = last
233 unpacked[i-1] = ch
234 i -= 1 if i > 1
235 else
236 i += 1
237 end
238 end
239 return unpacked
240 end
241 (class <<self; private :unicode_sort_canonical; end)
242
243 def self.unicode_decompose(unpacked)
244 unpacked_result = []
245 for cp in unpacked
246 if cp >= HANGUL_SBASE && cp < HANGUL_SBASE + HANGUL_SCOUNT
247 l, v, t = unicode_decompose_hangul(cp)
248 unpacked_result << l
249 unpacked_result << v if v
250 unpacked_result << t if t
251 else
252 dc = lookup_unicode_compatibility(cp)
253 unless dc
254 unpacked_result << cp
255 else
256 unpacked_result.concat(unicode_decompose(dc.unpack("U*")))
257 end
258 end
259 end
260 return unpacked_result
261 end
262 (class <<self; private :unicode_decompose; end)
263
264 def self.unicode_decompose_hangul(codepoint)
265 sindex = codepoint - HANGUL_SBASE;
266 if sindex < 0 || sindex >= HANGUL_SCOUNT
267 l = codepoint
268 v = t = nil
269 return l, v, t
270 end
271 l = HANGUL_LBASE + sindex / HANGUL_NCOUNT
272 v = HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
273 t = HANGUL_TBASE + sindex % HANGUL_TCOUNT
274 if t == HANGUL_TBASE
275 t = nil
276 end
277 return l, v, t
278 end
279 (class <<self; private :unicode_decompose_hangul; end)
280
281 def self.lookup_unicode_combining_class(codepoint)
282 codepoint_data = UNICODE_DATA[codepoint]
283 (codepoint_data ?
284 (codepoint_data[UNICODE_DATA_COMBINING_CLASS] || 0) :
285 0)
286 end
287 (class <<self; private :lookup_unicode_combining_class; end)
288
289 def self.lookup_unicode_compatibility(codepoint)
290 codepoint_data = UNICODE_DATA[codepoint]
291 (codepoint_data ?
292 codepoint_data[UNICODE_DATA_COMPATIBILITY] : nil)
293 end
294 (class <<self; private :lookup_unicode_compatibility; end)
295
296 def self.lookup_unicode_lowercase(codepoint)
297 codepoint_data = UNICODE_DATA[codepoint]
298 (codepoint_data ?
299 (codepoint_data[UNICODE_DATA_LOWERCASE] || codepoint) :
300 codepoint)
301 end
302 (class <<self; private :lookup_unicode_lowercase; end)
303
304 def self.lookup_unicode_composition(unpacked)
305 return COMPOSITION_TABLE[unpacked]
306 end
307 (class <<self; private :lookup_unicode_composition; end)
308
309 HANGUL_SBASE = 0xac00
310 HANGUL_LBASE = 0x1100
311 HANGUL_LCOUNT = 19
312 HANGUL_VBASE = 0x1161
313 HANGUL_VCOUNT = 21
314 HANGUL_TBASE = 0x11a7
315 HANGUL_TCOUNT = 28
316 HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT # 588
317 HANGUL_SCOUNT = HANGUL_LCOUNT * HANGUL_NCOUNT # 11172
318
319 UNICODE_DATA_COMBINING_CLASS = 0
320 UNICODE_DATA_EXCLUSION = 1
321 UNICODE_DATA_CANONICAL = 2
322 UNICODE_DATA_COMPATIBILITY = 3
323 UNICODE_DATA_UPPERCASE = 4
324 UNICODE_DATA_LOWERCASE = 5
325 UNICODE_DATA_TITLECASE = 6
326
327 begin
328 if defined?(FakeFS)
329 fakefs_state = FakeFS.activated?
330 FakeFS.deactivate!
331 end
332 # This is a sparse Unicode table. Codepoints without entries are
333 # assumed to have the value: [0, 0, nil, nil, nil, nil, nil]
334 UNICODE_DATA = File.open(UNICODE_TABLE, "rb") do |file|
335 Marshal.load(file.read)
336 end
337 ensure
338 if defined?(FakeFS)
339 FakeFS.activate! if fakefs_state
340 end
341 end
342
343 COMPOSITION_TABLE = {}
344 for codepoint, data in UNICODE_DATA
345 canonical = data[UNICODE_DATA_CANONICAL]
346 exclusion = data[UNICODE_DATA_EXCLUSION]
347
348 if canonical && exclusion == 0
349 COMPOSITION_TABLE[canonical.unpack("C*")] = codepoint
350 end
351 end
352
353 UNICODE_MAX_LENGTH = 256
354 ACE_MAX_LENGTH = 256
355
356 PUNYCODE_BASE = 36
357 PUNYCODE_TMIN = 1
358 PUNYCODE_TMAX = 26
359 PUNYCODE_SKEW = 38
360 PUNYCODE_DAMP = 700
361 PUNYCODE_INITIAL_BIAS = 72
362 PUNYCODE_INITIAL_N = 0x80
363 PUNYCODE_DELIMITER = 0x2D
364
365 PUNYCODE_MAXINT = 1 << 64
366
367 PUNYCODE_PRINT_ASCII =
368 "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
369 "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n" +
370 " !\"\#$%&'()*+,-./" +
371 "0123456789:;<=>?" +
372 "@ABCDEFGHIJKLMNO" +
373 "PQRSTUVWXYZ[\\]^_" +
374 "`abcdefghijklmno" +
375 "pqrstuvwxyz{|}~\n"
376
377 # Input is invalid.
378 class PunycodeBadInput < StandardError; end
379 # Output would exceed the space provided.
380 class PunycodeBigOutput < StandardError; end
381 # Input needs wider integers to process.
382 class PunycodeOverflow < StandardError; end
383
384 def self.punycode_encode(unicode)
385 unicode = unicode.to_s unless unicode.is_a?(String)
386 input = unicode.unpack("U*")
387 output = [0] * (ACE_MAX_LENGTH + 1)
388 input_length = input.size
389 output_length = [ACE_MAX_LENGTH]
390
391 # Initialize the state
392 n = PUNYCODE_INITIAL_N
393 delta = out = 0
394 max_out = output_length[0]
395 bias = PUNYCODE_INITIAL_BIAS
396
397 # Handle the basic code points:
398 input_length.times do |j|
399 if punycode_basic?(input[j])
400 if max_out - out < 2
401 raise PunycodeBigOutput,
402 "Output would exceed the space provided."
403 end
404 output[out] = input[j]
405 out += 1
406 end
407 end
408
409 h = b = out
410
411 # h is the number of code points that have been handled, b is the
412 # number of basic code points, and out is the number of characters
413 # that have been output.
414
415 if b > 0
416 output[out] = PUNYCODE_DELIMITER
417 out += 1
418 end
419
420 # Main encoding loop:
421
422 while h < input_length
423 # All non-basic code points < n have been
424 # handled already. Find the next larger one:
425
426 m = PUNYCODE_MAXINT
427 input_length.times do |j|
428 m = input[j] if (n...m) === input[j]
429 end
430
431 # Increase delta enough to advance the decoder's
432 # <n,i> state to <m,0>, but guard against overflow:
433
434 if m - n > (PUNYCODE_MAXINT - delta) / (h + 1)
435 raise PunycodeOverflow, "Input needs wider integers to process."
436 end
437 delta += (m - n) * (h + 1)
438 n = m
439
440 input_length.times do |j|
441 # Punycode does not need to check whether input[j] is basic:
442 if input[j] < n
443 delta += 1
444 if delta == 0
445 raise PunycodeOverflow,
446 "Input needs wider integers to process."
447 end
448 end
449
450 if input[j] == n
451 # Represent delta as a generalized variable-length integer:
452
453 q = delta; k = PUNYCODE_BASE
454 while true
455 if out >= max_out
456 raise PunycodeBigOutput,
457 "Output would exceed the space provided."
458 end
459 t = (
460 if k <= bias
461 PUNYCODE_TMIN
462 elsif k >= bias + PUNYCODE_TMAX
463 PUNYCODE_TMAX
464 else
465 k - bias
466 end
467 )
468 break if q < t
469 output[out] =
470 punycode_encode_digit(t + (q - t) % (PUNYCODE_BASE - t))
471 out += 1
472 q = (q - t) / (PUNYCODE_BASE - t)
473 k += PUNYCODE_BASE
474 end
475
476 output[out] = punycode_encode_digit(q)
477 out += 1
478 bias = punycode_adapt(delta, h + 1, h == b)
479 delta = 0
480 h += 1
481 end
482 end
483
484 delta += 1
485 n += 1
486 end
487
488 output_length[0] = out
489
490 outlen = out
491 outlen.times do |j|
492 c = output[j]
493 unless c >= 0 && c <= 127
494 raise StandardError, "Invalid output char."
495 end
496 unless PUNYCODE_PRINT_ASCII[c]
497 raise PunycodeBadInput, "Input is invalid."
498 end
499 end
500
501 output[0..outlen].map { |x| x.chr }.join("").sub(/\0+\z/, "")
502 end
503 (class <<self; private :punycode_encode; end)
504
505 def self.punycode_decode(punycode)
506 input = []
507 output = []
508
509 if ACE_MAX_LENGTH * 2 < punycode.size
510 raise PunycodeBigOutput, "Output would exceed the space provided."
511 end
512 punycode.each_byte do |c|
513 unless c >= 0 && c <= 127
514 raise PunycodeBadInput, "Input is invalid."
515 end
516 input.push(c)
517 end
518
519 input_length = input.length
520 output_length = [UNICODE_MAX_LENGTH]
521
522 # Initialize the state
523 n = PUNYCODE_INITIAL_N
524
525 out = i = 0
526 max_out = output_length[0]
527 bias = PUNYCODE_INITIAL_BIAS
528
529 # Handle the basic code points: Let b be the number of input code
530 # points before the last delimiter, or 0 if there is none, then
531 # copy the first b code points to the output.
532
533 b = 0
534 input_length.times do |j|
535 b = j if punycode_delimiter?(input[j])
536 end
537 if b > max_out
538 raise PunycodeBigOutput, "Output would exceed the space provided."
539 end
540
541 b.times do |j|
542 unless punycode_basic?(input[j])
543 raise PunycodeBadInput, "Input is invalid."
544 end
545 output[out] = input[j]
546 out+=1
547 end
548
549 # Main decoding loop: Start just after the last delimiter if any
550 # basic code points were copied; start at the beginning otherwise.
551
552 in_ = b > 0 ? b + 1 : 0
553 while in_ < input_length
554
555 # in_ is the index of the next character to be consumed, and
556 # out is the number of code points in the output array.
557
558 # Decode a generalized variable-length integer into delta,
559 # which gets added to i. The overflow checking is easier
560 # if we increase i as we go, then subtract off its starting
561 # value at the end to obtain delta.
562
563 oldi = i; w = 1; k = PUNYCODE_BASE
564 while true
565 if in_ >= input_length
566 raise PunycodeBadInput, "Input is invalid."
567 end
568 digit = punycode_decode_digit(input[in_])
569 in_+=1
570 if digit >= PUNYCODE_BASE
571 raise PunycodeBadInput, "Input is invalid."
572 end
573 if digit > (PUNYCODE_MAXINT - i) / w
574 raise PunycodeOverflow, "Input needs wider integers to process."
575 end
576 i += digit * w
577 t = (
578 if k <= bias
579 PUNYCODE_TMIN
580 elsif k >= bias + PUNYCODE_TMAX
581 PUNYCODE_TMAX
582 else
583 k - bias
584 end
585 )
586 break if digit < t
587 if w > PUNYCODE_MAXINT / (PUNYCODE_BASE - t)
588 raise PunycodeOverflow, "Input needs wider integers to process."
589 end
590 w *= PUNYCODE_BASE - t
591 k += PUNYCODE_BASE
592 end
593
594 bias = punycode_adapt(i - oldi, out + 1, oldi == 0)
595
596 # I was supposed to wrap around from out + 1 to 0,
597 # incrementing n each time, so we'll fix that now:
598
599 if i / (out + 1) > PUNYCODE_MAXINT - n
600 raise PunycodeOverflow, "Input needs wider integers to process."
601 end
602 n += i / (out + 1)
603 i %= out + 1
604
605 # Insert n at position i of the output:
606
607 # not needed for Punycode:
608 # raise PUNYCODE_INVALID_INPUT if decode_digit(n) <= base
609 if out >= max_out
610 raise PunycodeBigOutput, "Output would exceed the space provided."
611 end
612
613 #memmove(output + i + 1, output + i, (out - i) * sizeof *output)
614 output[i + 1, out - i] = output[i, out - i]
615 output[i] = n
616 i += 1
617
618 out += 1
619 end
620
621 output_length[0] = out
622
623 output.pack("U*")
624 end
625 (class <<self; private :punycode_decode; end)
626
627 def self.punycode_basic?(codepoint)
628 codepoint < 0x80
629 end
630 (class <<self; private :punycode_basic?; end)
631
632 def self.punycode_delimiter?(codepoint)
633 codepoint == PUNYCODE_DELIMITER
634 end
635 (class <<self; private :punycode_delimiter?; end)
636
637 def self.punycode_encode_digit(d)
638 d + 22 + 75 * ((d < 26) ? 1 : 0)
639 end
640 (class <<self; private :punycode_encode_digit; end)
641
642 # Returns the numeric value of a basic codepoint
643 # (for use in representing integers) in the range 0 to
644 # base - 1, or PUNYCODE_BASE if codepoint does not represent a value.
645 def self.punycode_decode_digit(codepoint)
646 if codepoint - 48 < 10
647 codepoint - 22
648 elsif codepoint - 65 < 26
649 codepoint - 65
650 elsif codepoint - 97 < 26
651 codepoint - 97
652 else
653 PUNYCODE_BASE
654 end
655 end
656 (class <<self; private :punycode_decode_digit; end)
657
658 # Bias adaptation method
659 def self.punycode_adapt(delta, numpoints, firsttime)
660 delta = firsttime ? delta / PUNYCODE_DAMP : delta >> 1
661 # delta >> 1 is a faster way of doing delta / 2
662 delta += delta / numpoints
663 difference = PUNYCODE_BASE - PUNYCODE_TMIN
664
665 k = 0
666 while delta > (difference * PUNYCODE_TMAX) / 2
667 delta /= difference
668 k += PUNYCODE_BASE
669 end
670
671 k + (difference + 1) * delta / (delta + PUNYCODE_SKEW)
672 end
673 (class <<self; private :punycode_adapt; end)
674 end
675 # :startdoc:
676 end
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 begin
19 require "addressable/idna/native"
20 rescue LoadError
21 # libidn or the idn gem was not available, fall back on a pure-Ruby
22 # implementation...
23 require "addressable/idna/pure"
24 end
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 require "addressable/version"
19 require "addressable/uri"
20
21 module Addressable
22 ##
23 # This is an implementation of a URI template based on
24 # RFC 6570 (http://tools.ietf.org/html/rfc6570).
25 class Template
26 # Constants used throughout the template code.
27 anything =
28 Addressable::URI::CharacterClasses::RESERVED +
29 Addressable::URI::CharacterClasses::UNRESERVED
30
31
32 variable_char_class =
33 Addressable::URI::CharacterClasses::ALPHA +
34 Addressable::URI::CharacterClasses::DIGIT + '_'
35
36 var_char =
37 "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
38 RESERVED =
39 "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
40 UNRESERVED =
41 "(?:[#{
42 Addressable::URI::CharacterClasses::UNRESERVED
43 }]|%[a-fA-F0-9][a-fA-F0-9])"
44 variable =
45 "(?:#{var_char}(?:\\.?#{var_char})*)"
46 varspec =
47 "(?:(#{variable})(\\*|:\\d+)?)"
48 VARNAME =
49 /^#{variable}$/
50 VARSPEC =
51 /^#{varspec}$/
52 VARIABLE_LIST =
53 /^#{varspec}(?:,#{varspec})*$/
54 operator =
55 "+#./;?&=,!@|"
56 EXPRESSION =
57 /\{([#{operator}])?(#{varspec}(?:,#{varspec})*)\}/
58
59
60 LEADERS = {
61 '?' => '?',
62 '/' => '/',
63 '#' => '#',
64 '.' => '.',
65 ';' => ';',
66 '&' => '&'
67 }
68 JOINERS = {
69 '?' => '&',
70 '.' => '.',
71 ';' => ';',
72 '&' => '&',
73 '/' => '/'
74 }
75
76 ##
77 # Raised if an invalid template value is supplied.
78 class InvalidTemplateValueError < StandardError
79 end
80
81 ##
82 # Raised if an invalid template operator is used in a pattern.
83 class InvalidTemplateOperatorError < StandardError
84 end
85
86 ##
87 # Raised if an invalid template operator is used in a pattern.
88 class TemplateOperatorAbortedError < StandardError
89 end
90
91 ##
92 # This class represents the data that is extracted when a Template
93 # is matched against a URI.
94 class MatchData
95 ##
96 # Creates a new MatchData object.
97 # MatchData objects should never be instantiated directly.
98 #
99 # @param [Addressable::URI] uri
100 # The URI that the template was matched against.
101 def initialize(uri, template, mapping)
102 @uri = uri.dup.freeze
103 @template = template
104 @mapping = mapping.dup.freeze
105 end
106
107 ##
108 # @return [Addressable::URI]
109 # The URI that the Template was matched against.
110 attr_reader :uri
111
112 ##
113 # @return [Addressable::Template]
114 # The Template used for the match.
115 attr_reader :template
116
117 ##
118 # @return [Hash]
119 # The mapping that resulted from the match.
120 # Note that this mapping does not include keys or values for
121 # variables that appear in the Template, but are not present
122 # in the URI.
123 attr_reader :mapping
124
125 ##
126 # @return [Array]
127 # The list of variables that were present in the Template.
128 # Note that this list will include variables which do not appear
129 # in the mapping because they were not present in URI.
130 def variables
131 self.template.variables
132 end
133 alias_method :keys, :variables
134 alias_method :names, :variables
135
136 ##
137 # @return [Array]
138 # The list of values that were captured by the Template.
139 # Note that this list will include nils for any variables which
140 # were in the Template, but did not appear in the URI.
141 def values
142 @values ||= self.variables.inject([]) do |accu, key|
143 accu << self.mapping[key]
144 accu
145 end
146 end
147 alias_method :captures, :values
148
149 ##
150 # Accesses captured values by name or by index.
151 #
152 # @param [String, Symbol, Fixnum] key
153 # Capture index or name. Note that when accessing by with index
154 # of 0, the full URI will be returned. The intention is to mimic
155 # the ::MatchData#[] behavior.
156 #
157 # @param [#to_int, nil] len
158 # If provided, an array of values will be returend with the given
159 # parameter used as length.
160 #
161 # @return [Array, String, nil]
162 # The captured value corresponding to the index or name. If the
163 # value was not provided or the key is unknown, nil will be
164 # returned.
165 #
166 # If the second parameter is provided, an array of that length will
167 # be returned instead.
168 def [](key, len = nil)
169 if len
170 to_a[key, len]
171 elsif String === key or Symbol === key
172 mapping[key.to_s]
173 else
174 to_a[key]
175 end
176 end
177
178 ##
179 # @return [Array]
180 # Array with the matched URI as first element followed by the captured
181 # values.
182 def to_a
183 [to_s, *values]
184 end
185
186 ##
187 # @return [String]
188 # The matched URI as String.
189 def to_s
190 uri.to_s
191 end
192 alias_method :string, :to_s
193
194 # Returns multiple captured values at once.
195 #
196 # @param [String, Symbol, Fixnum] *indexes
197 # Indices of the captures to be returned
198 #
199 # @return [Array]
200 # Values corresponding to given indices.
201 #
202 # @see Addressable::Template::MatchData#[]
203 def values_at(*indexes)
204 indexes.map { |i| self[i] }
205 end
206
207 ##
208 # Returns a <tt>String</tt> representation of the MatchData's state.
209 #
210 # @return [String] The MatchData's state, as a <tt>String</tt>.
211 def inspect
212 sprintf("#<%s:%#0x RESULT:%s>",
213 self.class.to_s, self.object_id, self.mapping.inspect)
214 end
215
216 ##
217 # Dummy method for code expecting a ::MatchData instance
218 #
219 # @return [String] An empty string.
220 def pre_match
221 ""
222 end
223 alias_method :post_match, :pre_match
224 end
225
226 ##
227 # Creates a new <tt>Addressable::Template</tt> object.
228 #
229 # @param [#to_str] pattern The URI Template pattern.
230 #
231 # @return [Addressable::Template] The initialized Template object.
232 def initialize(pattern)
233 if !pattern.respond_to?(:to_str)
234 raise TypeError, "Can't convert #{pattern.class} into String."
235 end
236 @pattern = pattern.to_str.dup.freeze
237 end
238
239 ##
240 # Freeze URI, initializing instance variables.
241 #
242 # @return [Addressable::URI] The frozen URI object.
243 def freeze
244 self.variables
245 self.variable_defaults
246 self.named_captures
247 super
248 end
249
250 ##
251 # @return [String] The Template object's pattern.
252 attr_reader :pattern
253
254 ##
255 # Returns a <tt>String</tt> representation of the Template object's state.
256 #
257 # @return [String] The Template object's state, as a <tt>String</tt>.
258 def inspect
259 sprintf("#<%s:%#0x PATTERN:%s>",
260 self.class.to_s, self.object_id, self.pattern)
261 end
262
263 ##
264 # Returns <code>true</code> if the Template objects are equal. This method
265 # does NOT normalize either Template before doing the comparison.
266 #
267 # @param [Object] template The Template to compare.
268 #
269 # @return [TrueClass, FalseClass]
270 # <code>true</code> if the Templates are equivalent, <code>false</code>
271 # otherwise.
272 def ==(template)
273 return false unless template.kind_of?(Template)
274 return self.pattern == template.pattern
275 end
276
277 ##
278 # Addressable::Template makes no distinction between `==` and `eql?`.
279 #
280 # @see #==
281 alias_method :eql?, :==
282
283 ##
284 # Extracts a mapping from the URI using a URI Template pattern.
285 #
286 # @param [Addressable::URI, #to_str] uri
287 # The URI to extract from.
288 #
289 # @param [#restore, #match] processor
290 # A template processor object may optionally be supplied.
291 #
292 # The object should respond to either the <tt>restore</tt> or
293 # <tt>match</tt> messages or both. The <tt>restore</tt> method should
294 # take two parameters: `[String] name` and `[String] value`.
295 # The <tt>restore</tt> method should reverse any transformations that
296 # have been performed on the value to ensure a valid URI.
297 # The <tt>match</tt> method should take a single
298 # parameter: `[String] name`. The <tt>match</tt> method should return
299 # a <tt>String</tt> containing a regular expression capture group for
300 # matching on that particular variable. The default value is `".*?"`.
301 # The <tt>match</tt> method has no effect on multivariate operator
302 # expansions.
303 #
304 # @return [Hash, NilClass]
305 # The <tt>Hash</tt> mapping that was extracted from the URI, or
306 # <tt>nil</tt> if the URI didn't match the template.
307 #
308 # @example
309 # class ExampleProcessor
310 # def self.restore(name, value)
311 # return value.gsub(/\+/, " ") if name == "query"
312 # return value
313 # end
314 #
315 # def self.match(name)
316 # return ".*?" if name == "first"
317 # return ".*"
318 # end
319 # end
320 #
321 # uri = Addressable::URI.parse(
322 # "http://example.com/search/an+example+search+query/"
323 # )
324 # Addressable::Template.new(
325 # "http://example.com/search/{query}/"
326 # ).extract(uri, ExampleProcessor)
327 # #=> {"query" => "an example search query"}
328 #
329 # uri = Addressable::URI.parse("http://example.com/a/b/c/")
330 # Addressable::Template.new(
331 # "http://example.com/{first}/{second}/"
332 # ).extract(uri, ExampleProcessor)
333 # #=> {"first" => "a", "second" => "b/c"}
334 #
335 # uri = Addressable::URI.parse("http://example.com/a/b/c/")
336 # Addressable::Template.new(
337 # "http://example.com/{first}/{-list|/|second}/"
338 # ).extract(uri)
339 # #=> {"first" => "a", "second" => ["b", "c"]}
340 def extract(uri, processor=nil)
341 match_data = self.match(uri, processor)
342 return (match_data ? match_data.mapping : nil)
343 end
344
345 ##
346 # Extracts match data from the URI using a URI Template pattern.
347 #
348 # @param [Addressable::URI, #to_str] uri
349 # The URI to extract from.
350 #
351 # @param [#restore, #match] processor
352 # A template processor object may optionally be supplied.
353 #
354 # The object should respond to either the <tt>restore</tt> or
355 # <tt>match</tt> messages or both. The <tt>restore</tt> method should
356 # take two parameters: `[String] name` and `[String] value`.
357 # The <tt>restore</tt> method should reverse any transformations that
358 # have been performed on the value to ensure a valid URI.
359 # The <tt>match</tt> method should take a single
360 # parameter: `[String] name`. The <tt>match</tt> method should return
361 # a <tt>String</tt> containing a regular expression capture group for
362 # matching on that particular variable. The default value is `".*?"`.
363 # The <tt>match</tt> method has no effect on multivariate operator
364 # expansions.
365 #
366 # @return [Hash, NilClass]
367 # The <tt>Hash</tt> mapping that was extracted from the URI, or
368 # <tt>nil</tt> if the URI didn't match the template.
369 #
370 # @example
371 # class ExampleProcessor
372 # def self.restore(name, value)
373 # return value.gsub(/\+/, " ") if name == "query"
374 # return value
375 # end
376 #
377 # def self.match(name)
378 # return ".*?" if name == "first"
379 # return ".*"
380 # end
381 # end
382 #
383 # uri = Addressable::URI.parse(
384 # "http://example.com/search/an+example+search+query/"
385 # )
386 # match = Addressable::Template.new(
387 # "http://example.com/search/{query}/"
388 # ).match(uri, ExampleProcessor)
389 # match.variables
390 # #=> ["query"]
391 # match.captures
392 # #=> ["an example search query"]
393 #
394 # uri = Addressable::URI.parse("http://example.com/a/b/c/")
395 # match = Addressable::Template.new(
396 # "http://example.com/{first}/{+second}/"
397 # ).match(uri, ExampleProcessor)
398 # match.variables
399 # #=> ["first", "second"]
400 # match.captures
401 # #=> ["a", "b/c"]
402 #
403 # uri = Addressable::URI.parse("http://example.com/a/b/c/")
404 # match = Addressable::Template.new(
405 # "http://example.com/{first}{/second*}/"
406 # ).match(uri)
407 # match.variables
408 # #=> ["first", "second"]
409 # match.captures
410 # #=> ["a", ["b", "c"]]
411 def match(uri, processor=nil)
412 uri = Addressable::URI.parse(uri)
413 mapping = {}
414
415 # First, we need to process the pattern, and extract the values.
416 expansions, expansion_regexp =
417 parse_template_pattern(pattern, processor)
418
419 return nil unless uri.to_str.match(expansion_regexp)
420 unparsed_values = uri.to_str.scan(expansion_regexp).flatten
421
422 if uri.to_str == pattern
423 return Addressable::Template::MatchData.new(uri, self, mapping)
424 elsif expansions.size > 0
425 index = 0
426 expansions.each do |expansion|
427 _, operator, varlist = *expansion.match(EXPRESSION)
428 varlist.split(',').each do |varspec|
429 _, name, modifier = *varspec.match(VARSPEC)
430 mapping[name] ||= nil
431 case operator
432 when nil, '+', '#', '/', '.'
433 unparsed_value = unparsed_values[index]
434 name = varspec[VARSPEC, 1]
435 value = unparsed_value
436 value = value.split(JOINERS[operator]) if value && modifier == '*'
437 when ';', '?', '&'
438 if modifier == '*'
439 if unparsed_values[index]
440 value = unparsed_values[index].split(JOINERS[operator])
441 value = value.inject({}) do |acc, v|
442 key, val = v.split('=')
443 val = "" if val.nil?
444 acc[key] = val
445 acc
446 end
447 end
448 else
449 if (unparsed_values[index])
450 name, value = unparsed_values[index].split('=')
451 value = "" if value.nil?
452 end
453 end
454 end
455 if processor != nil && processor.respond_to?(:restore)
456 value = processor.restore(name, value)
457 end
458 if processor == nil
459 if value.is_a?(Hash)
460 value = value.inject({}){|acc, (k, v)|
461 acc[Addressable::URI.unencode_component(k)] =
462 Addressable::URI.unencode_component(v)
463 acc
464 }
465 elsif value.is_a?(Array)
466 value = value.map{|v| Addressable::URI.unencode_component(v) }
467 else
468 value = Addressable::URI.unencode_component(value)
469 end
470 end
471 if !mapping.has_key?(name) || mapping[name].nil?
472 # Doesn't exist, set to value (even if value is nil)
473 mapping[name] = value
474 end
475 index = index + 1
476 end
477 end
478 return Addressable::Template::MatchData.new(uri, self, mapping)
479 else
480 return nil
481 end
482 end
483
484 ##
485 # Expands a URI template into another URI template.
486 #
487 # @param [Hash] mapping The mapping that corresponds to the pattern.
488 # @param [#validate, #transform] processor
489 # An optional processor object may be supplied.
490 # @param [Boolean] normalize_values
491 # Optional flag to enable/disable unicode normalization. Default: true
492 #
493 # The object should respond to either the <tt>validate</tt> or
494 # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
495 # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
496 # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
497 # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
498 # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
499 # exception will be raised if the value is invalid. The <tt>transform</tt>
500 # method should return the transformed variable value as a <tt>String</tt>.
501 # If a <tt>transform</tt> method is used, the value will not be percent
502 # encoded automatically. Unicode normalization will be performed both
503 # before and after sending the value to the transform method.
504 #
505 # @return [Addressable::Template] The partially expanded URI template.
506 #
507 # @example
508 # Addressable::Template.new(
509 # "http://example.com/{one}/{two}/"
510 # ).partial_expand({"one" => "1"}).pattern
511 # #=> "http://example.com/1/{two}/"
512 #
513 # Addressable::Template.new(
514 # "http://example.com/{?one,two}/"
515 # ).partial_expand({"one" => "1"}).pattern
516 # #=> "http://example.com/?one=1{&two}/"
517 #
518 # Addressable::Template.new(
519 # "http://example.com/{?one,two,three}/"
520 # ).partial_expand({"one" => "1", "three" => 3}).pattern
521 # #=> "http://example.com/?one=1{&two}&three=3"
522 def partial_expand(mapping, processor=nil, normalize_values=true)
523 result = self.pattern.dup
524 mapping = normalize_keys(mapping)
525 result.gsub!( EXPRESSION ) do |capture|
526 transform_partial_capture(mapping, capture, processor, normalize_values)
527 end
528 return Addressable::Template.new(result)
529 end
530
531 ##
532 # Expands a URI template into a full URI.
533 #
534 # @param [Hash] mapping The mapping that corresponds to the pattern.
535 # @param [#validate, #transform] processor
536 # An optional processor object may be supplied.
537 # @param [Boolean] normalize_values
538 # Optional flag to enable/disable unicode normalization. Default: true
539 #
540 # The object should respond to either the <tt>validate</tt> or
541 # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
542 # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
543 # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
544 # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
545 # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt>
546 # exception will be raised if the value is invalid. The <tt>transform</tt>
547 # method should return the transformed variable value as a <tt>String</tt>.
548 # If a <tt>transform</tt> method is used, the value will not be percent
549 # encoded automatically. Unicode normalization will be performed both
550 # before and after sending the value to the transform method.
551 #
552 # @return [Addressable::URI] The expanded URI template.
553 #
554 # @example
555 # class ExampleProcessor
556 # def self.validate(name, value)
557 # return !!(value =~ /^[\w ]+$/) if name == "query"
558 # return true
559 # end
560 #
561 # def self.transform(name, value)
562 # return value.gsub(/ /, "+") if name == "query"
563 # return value
564 # end
565 # end
566 #
567 # Addressable::Template.new(
568 # "http://example.com/search/{query}/"
569 # ).expand(
570 # {"query" => "an example search query"},
571 # ExampleProcessor
572 # ).to_str
573 # #=> "http://example.com/search/an+example+search+query/"
574 #
575 # Addressable::Template.new(
576 # "http://example.com/search/{query}/"
577 # ).expand(
578 # {"query" => "an example search query"}
579 # ).to_str
580 # #=> "http://example.com/search/an%20example%20search%20query/"
581 #
582 # Addressable::Template.new(
583 # "http://example.com/search/{query}/"
584 # ).expand(
585 # {"query" => "bogus!"},
586 # ExampleProcessor
587 # ).to_str
588 # #=> Addressable::Template::InvalidTemplateValueError
589 def expand(mapping, processor=nil, normalize_values=true)
590 result = self.pattern.dup
591 mapping = normalize_keys(mapping)
592 result.gsub!( EXPRESSION ) do |capture|
593 transform_capture(mapping, capture, processor, normalize_values)
594 end
595 return Addressable::URI.parse(result)
596 end
597
598 ##
599 # Returns an Array of variables used within the template pattern.
600 # The variables are listed in the Array in the order they appear within
601 # the pattern. Multiple occurrences of a variable within a pattern are
602 # not represented in this Array.
603 #
604 # @return [Array] The variables present in the template's pattern.
605 def variables
606 @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
607 end
608 alias_method :keys, :variables
609 alias_method :names, :variables
610
611 ##
612 # Returns a mapping of variables to their default values specified
613 # in the template. Variables without defaults are not returned.
614 #
615 # @return [Hash] Mapping of template variables to their defaults
616 def variable_defaults
617 @variable_defaults ||=
618 Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
619 end
620
621 ##
622 # Coerces a template into a `Regexp` object. This regular expression will
623 # behave very similarly to the actual template, and should match the same
624 # URI values, but it cannot fully handle, for example, values that would
625 # extract to an `Array`.
626 #
627 # @return [Regexp] A regular expression which should match the template.
628 def to_regexp
629 _, source = parse_template_pattern(pattern)
630 Regexp.new(source)
631 end
632
633 ##
634 # Returns the source of the coerced `Regexp`.
635 #
636 # @return [String] The source of the `Regexp` given by {#to_regexp}.
637 #
638 # @api private
639 def source
640 self.to_regexp.source
641 end
642
643 ##
644 # Returns the named captures of the coerced `Regexp`.
645 #
646 # @return [Hash] The named captures of the `Regexp` given by {#to_regexp}.
647 #
648 # @api private
649 def named_captures
650 self.to_regexp.named_captures
651 end
652
653 ##
654 # Generates a route result for a given set of parameters.
655 # Should only be used by rack-mount.
656 #
657 # @param params [Hash] The set of parameters used to expand the template.
658 # @param recall [Hash] Default parameters used to expand the template.
659 # @param options [Hash] Either a `:processor` or a `:parameterize` block.
660 #
661 # @api private
662 def generate(params={}, recall={}, options={})
663 merged = recall.merge(params)
664 if options[:processor]
665 processor = options[:processor]
666 elsif options[:parameterize]
667 # TODO: This is sending me into fits trying to shoe-horn this into
668 # the existing API. I think I've got this backwards and processors
669 # should be a set of 4 optional blocks named :validate, :transform,
670 # :match, and :restore. Having to use a singleton here is a huge
671 # code smell.
672 processor = Object.new
673 class <<processor
674 attr_accessor :block
675 def transform(name, value)
676 block.call(name, value)
677 end
678 end
679 processor.block = options[:parameterize]
680 else
681 processor = nil
682 end
683 result = self.expand(merged, processor)
684 result.to_s if result
685 end
686
687 private
688 def ordered_variable_defaults
689 @ordered_variable_defaults ||= begin
690 expansions, _ = parse_template_pattern(pattern)
691 expansions.map do |capture|
692 _, _, varlist = *capture.match(EXPRESSION)
693 varlist.split(',').map do |varspec|
694 varspec[VARSPEC, 1]
695 end
696 end.flatten
697 end
698 end
699
700
701 ##
702 # Loops through each capture and expands any values available in mapping
703 #
704 # @param [Hash] mapping
705 # Set of keys to expand
706 # @param [String] capture
707 # The expression to expand
708 # @param [#validate, #transform] processor
709 # An optional processor object may be supplied.
710 # @param [Boolean] normalize_values
711 # Optional flag to enable/disable unicode normalization. Default: true
712 #
713 # The object should respond to either the <tt>validate</tt> or
714 # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
715 # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
716 # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
717 # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
718 # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
719 # will be raised if the value is invalid. The <tt>transform</tt> method
720 # should return the transformed variable value as a <tt>String</tt>. If a
721 # <tt>transform</tt> method is used, the value will not be percent encoded
722 # automatically. Unicode normalization will be performed both before and
723 # after sending the value to the transform method.
724 #
725 # @return [String] The expanded expression
726 def transform_partial_capture(mapping, capture, processor = nil,
727 normalize_values = true)
728 _, operator, varlist = *capture.match(EXPRESSION)
729
730 vars = varlist.split(',')
731
732 if '?' == operator
733 # partial expansion of form style query variables sometimes requires a
734 # slight reordering of the variables to produce a valid url.
735 first_to_expand = vars.find { |varspec|
736 _, name, _ = *varspec.match(VARSPEC)
737 mapping.key? name
738 }
739
740 vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand
741 end
742
743 vars
744 .zip(operator_sequence(operator).take(vars.length))
745 .reduce("".dup) do |acc, (varspec, op)|
746 _, name, _ = *varspec.match(VARSPEC)
747
748 acc << if mapping.key? name
749 transform_capture(mapping, "{#{op}#{varspec}}",
750 processor, normalize_values)
751 else
752 "{#{op}#{varspec}}"
753 end
754 end
755 end
756
757 ##
758 # Creates a lazy Enumerator of the operators that should be used to expand
759 # variables in a varlist starting with `operator`. For example, an operator
760 # `"?"` results in the sequence `"?","&","&"...`
761 #
762 # @param [String] operator from which to generate a sequence
763 #
764 # @return [Enumerator] sequence of operators
765 def operator_sequence(operator)
766 rest_operator = if "?" == operator
767 "&"
768 else
769 operator
770 end
771 head_operator = operator
772
773 Enumerator.new do |y|
774 y << head_operator.to_s
775 while true
776 y << rest_operator.to_s
777 end
778 end
779 end
780
781 ##
782 # Transforms a mapped value so that values can be substituted into the
783 # template.
784 #
785 # @param [Hash] mapping The mapping to replace captures
786 # @param [String] capture
787 # The expression to replace
788 # @param [#validate, #transform] processor
789 # An optional processor object may be supplied.
790 # @param [Boolean] normalize_values
791 # Optional flag to enable/disable unicode normalization. Default: true
792 #
793 #
794 # The object should respond to either the <tt>validate</tt> or
795 # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
796 # <tt>transform</tt> methods should take two parameters: <tt>name</tt> and
797 # <tt>value</tt>. The <tt>validate</tt> method should return <tt>true</tt>
798 # or <tt>false</tt>; <tt>true</tt> if the value of the variable is valid,
799 # <tt>false</tt> otherwise. An <tt>InvalidTemplateValueError</tt> exception
800 # will be raised if the value is invalid. The <tt>transform</tt> method
801 # should return the transformed variable value as a <tt>String</tt>. If a
802 # <tt>transform</tt> method is used, the value will not be percent encoded
803 # automatically. Unicode normalization will be performed both before and
804 # after sending the value to the transform method.
805 #
806 # @return [String] The expanded expression
807 def transform_capture(mapping, capture, processor=nil,
808 normalize_values=true)
809 _, operator, varlist = *capture.match(EXPRESSION)
810 return_value = varlist.split(',').inject([]) do |acc, varspec|
811 _, name, modifier = *varspec.match(VARSPEC)
812 value = mapping[name]
813 unless value == nil || value == {}
814 allow_reserved = %w(+ #).include?(operator)
815 # Common primitives where the .to_s output is well-defined
816 if Numeric === value || Symbol === value ||
817 value == true || value == false
818 value = value.to_s
819 end
820 length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/
821
822 unless (Hash === value) ||
823 value.respond_to?(:to_ary) || value.respond_to?(:to_str)
824 raise TypeError,
825 "Can't convert #{value.class} into String or Array."
826 end
827
828 value = normalize_value(value) if normalize_values
829
830 if processor == nil || !processor.respond_to?(:transform)
831 # Handle percent escaping
832 if allow_reserved
833 encode_map =
834 Addressable::URI::CharacterClasses::RESERVED +
835 Addressable::URI::CharacterClasses::UNRESERVED
836 else
837 encode_map = Addressable::URI::CharacterClasses::UNRESERVED
838 end
839 if value.kind_of?(Array)
840 transformed_value = value.map do |val|
841 if length
842 Addressable::URI.encode_component(val[0...length], encode_map)
843 else
844 Addressable::URI.encode_component(val, encode_map)
845 end
846 end
847 unless modifier == "*"
848 transformed_value = transformed_value.join(',')
849 end
850 elsif value.kind_of?(Hash)
851 transformed_value = value.map do |key, val|
852 if modifier == "*"
853 "#{
854 Addressable::URI.encode_component( key, encode_map)
855 }=#{
856 Addressable::URI.encode_component( val, encode_map)
857 }"
858 else
859 "#{
860 Addressable::URI.encode_component( key, encode_map)
861 },#{
862 Addressable::URI.encode_component( val, encode_map)
863 }"
864 end
865 end
866 unless modifier == "*"
867 transformed_value = transformed_value.join(',')
868 end
869 else
870 if length
871 transformed_value = Addressable::URI.encode_component(
872 value[0...length], encode_map)
873 else
874 transformed_value = Addressable::URI.encode_component(
875 value, encode_map)
876 end
877 end
878 end
879
880 # Process, if we've got a processor
881 if processor != nil
882 if processor.respond_to?(:validate)
883 if !processor.validate(name, value)
884 display_value = value.kind_of?(Array) ? value.inspect : value
885 raise InvalidTemplateValueError,
886 "#{name}=#{display_value} is an invalid template value."
887 end
888 end
889 if processor.respond_to?(:transform)
890 transformed_value = processor.transform(name, value)
891 if normalize_values
892 transformed_value = normalize_value(transformed_value)
893 end
894 end
895 end
896 acc << [name, transformed_value]
897 end
898 acc
899 end
900 return "" if return_value.empty?
901 join_values(operator, return_value)
902 end
903
904 ##
905 # Takes a set of values, and joins them together based on the
906 # operator.
907 #
908 # @param [String, Nil] operator One of the operators from the set
909 # (?,&,+,#,;,/,.), or nil if there wasn't one.
910 # @param [Array] return_value
911 # The set of return values (as [variable_name, value] tuples) that will
912 # be joined together.
913 #
914 # @return [String] The transformed mapped value
915 def join_values(operator, return_value)
916 leader = LEADERS.fetch(operator, '')
917 joiner = JOINERS.fetch(operator, ',')
918 case operator
919 when '&', '?'
920 leader + return_value.map{|k,v|
921 if v.is_a?(Array) && v.first =~ /=/
922 v.join(joiner)
923 elsif v.is_a?(Array)
924 v.map{|inner_value| "#{k}=#{inner_value}"}.join(joiner)
925 else
926 "#{k}=#{v}"
927 end
928 }.join(joiner)
929 when ';'
930 return_value.map{|k,v|
931 if v.is_a?(Array) && v.first =~ /=/
932 ';' + v.join(";")
933 elsif v.is_a?(Array)
934 ';' + v.map{|inner_value| "#{k}=#{inner_value}"}.join(";")
935 else
936 v && v != '' ? ";#{k}=#{v}" : ";#{k}"
937 end
938 }.join
939 else
940 leader + return_value.map{|k,v| v}.join(joiner)
941 end
942 end
943
944 ##
945 # Takes a set of values, and joins them together based on the
946 # operator.
947 #
948 # @param [Hash, Array, String] value
949 # Normalizes keys and values with IDNA#unicode_normalize_kc
950 #
951 # @return [Hash, Array, String] The normalized values
952 def normalize_value(value)
953 unless value.is_a?(Hash)
954 value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
955 end
956
957 # Handle unicode normalization
958 if value.kind_of?(Array)
959 value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
960 elsif value.kind_of?(Hash)
961 value = value.inject({}) { |acc, (k, v)|
962 acc[Addressable::IDNA.unicode_normalize_kc(k)] =
963 Addressable::IDNA.unicode_normalize_kc(v)
964 acc
965 }
966 else
967 value = Addressable::IDNA.unicode_normalize_kc(value)
968 end
969 value
970 end
971
972 ##
973 # Generates a hash with string keys
974 #
975 # @param [Hash] mapping A mapping hash to normalize
976 #
977 # @return [Hash]
978 # A hash with stringified keys
979 def normalize_keys(mapping)
980 return mapping.inject({}) do |accu, pair|
981 name, value = pair
982 if Symbol === name
983 name = name.to_s
984 elsif name.respond_to?(:to_str)
985 name = name.to_str
986 else
987 raise TypeError,
988 "Can't convert #{name.class} into String."
989 end
990 accu[name] = value
991 accu
992 end
993 end
994
995 ##
996 # Generates the <tt>Regexp</tt> that parses a template pattern.
997 #
998 # @param [String] pattern The URI template pattern.
999 # @param [#match] processor The template processor to use.
1000 #
1001 # @return [Regexp]
1002 # A regular expression which may be used to parse a template pattern.
1003 def parse_template_pattern(pattern, processor=nil)
1004 # Escape the pattern. The two gsubs restore the escaped curly braces
1005 # back to their original form. Basically, escape everything that isn't
1006 # within an expansion.
1007 escaped_pattern = Regexp.escape(
1008 pattern
1009 ).gsub(/\\\{(.*?)\\\}/) do |escaped|
1010 escaped.gsub(/\\(.)/, "\\1")
1011 end
1012
1013 expansions = []
1014
1015 # Create a regular expression that captures the values of the
1016 # variables in the URI.
1017 regexp_string = escaped_pattern.gsub( EXPRESSION ) do |expansion|
1018
1019 expansions << expansion
1020 _, operator, varlist = *expansion.match(EXPRESSION)
1021 leader = Regexp.escape(LEADERS.fetch(operator, ''))
1022 joiner = Regexp.escape(JOINERS.fetch(operator, ','))
1023 combined = varlist.split(',').map do |varspec|
1024 _, name, modifier = *varspec.match(VARSPEC)
1025
1026 result = processor && processor.respond_to?(:match) ? processor.match(name) : nil
1027 if result
1028 "(?<#{name}>#{ result })"
1029 else
1030 group = case operator
1031 when '+'
1032 "#{ RESERVED }*?"
1033 when '#'
1034 "#{ RESERVED }*?"
1035 when '/'
1036 "#{ UNRESERVED }*?"
1037 when '.'
1038 "#{ UNRESERVED.gsub('\.', '') }*?"
1039 when ';'
1040 "#{ UNRESERVED }*=?#{ UNRESERVED }*?"
1041 when '?'
1042 "#{ UNRESERVED }*=#{ UNRESERVED }*?"
1043 when '&'
1044 "#{ UNRESERVED }*=#{ UNRESERVED }*?"
1045 else
1046 "#{ UNRESERVED }*?"
1047 end
1048 if modifier == '*'
1049 "(?<#{name}>#{group}(?:#{joiner}?#{group})*)?"
1050 else
1051 "(?<#{name}>#{group})?"
1052 end
1053 end
1054 end.join("#{joiner}?")
1055 "(?:|#{leader}#{combined})"
1056 end
1057
1058 # Ensure that the regular expression matches the whole URI.
1059 regexp_string = "^#{regexp_string}$"
1060 return expansions, Regexp.new(regexp_string)
1061 end
1062
1063 end
1064 end
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 require "addressable/version"
19 require "addressable/idna"
20 require "public_suffix"
21
22 ##
23 # Addressable is a library for processing links and URIs.
24 module Addressable
25 ##
26 # This is an implementation of a URI parser based on
27 # <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>,
28 # <a href="http://www.ietf.org/rfc/rfc3987.txt">RFC 3987</a>.
29 class URI
30 ##
31 # Raised if something other than a uri is supplied.
32 class InvalidURIError < StandardError
33 end
34
35 ##
36 # Container for the character classes specified in
37 # <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
38 module CharacterClasses
39 ALPHA = "a-zA-Z"
40 DIGIT = "0-9"
41 GEN_DELIMS = "\\:\\/\\?\\#\\[\\]\\@"
42 SUB_DELIMS = "\\!\\$\\&\\'\\(\\)\\*\\+\\,\\;\\="
43 RESERVED = GEN_DELIMS + SUB_DELIMS
44 UNRESERVED = ALPHA + DIGIT + "\\-\\.\\_\\~"
45 PCHAR = UNRESERVED + SUB_DELIMS + "\\:\\@"
46 SCHEME = ALPHA + DIGIT + "\\-\\+\\."
47 HOST = UNRESERVED + SUB_DELIMS + "\\[\\:\\]"
48 AUTHORITY = PCHAR
49 PATH = PCHAR + "\\/"
50 QUERY = PCHAR + "\\/\\?"
51 FRAGMENT = PCHAR + "\\/\\?"
52 end
53
54 SLASH = '/'
55 EMPTY_STR = ''
56
57 URIREGEX = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/
58
59 PORT_MAPPING = {
60 "http" => 80,
61 "https" => 443,
62 "ftp" => 21,
63 "tftp" => 69,
64 "sftp" => 22,
65 "ssh" => 22,
66 "svn+ssh" => 22,
67 "telnet" => 23,
68 "nntp" => 119,
69 "gopher" => 70,
70 "wais" => 210,
71 "ldap" => 389,
72 "prospero" => 1525
73 }
74
75 ##
76 # Returns a URI object based on the parsed string.
77 #
78 # @param [String, Addressable::URI, #to_str] uri
79 # The URI string to parse.
80 # No parsing is performed if the object is already an
81 # <code>Addressable::URI</code>.
82 #
83 # @return [Addressable::URI] The parsed URI.
84 def self.parse(uri)
85 # If we were given nil, return nil.
86 return nil unless uri
87 # If a URI object is passed, just return itself.
88 return uri.dup if uri.kind_of?(self)
89
90 # If a URI object of the Ruby standard library variety is passed,
91 # convert it to a string, then parse the string.
92 # We do the check this way because we don't want to accidentally
93 # cause a missing constant exception to be thrown.
94 if uri.class.name =~ /^URI\b/
95 uri = uri.to_s
96 end
97
98 # Otherwise, convert to a String
99 begin
100 uri = uri.to_str
101 rescue TypeError, NoMethodError
102 raise TypeError, "Can't convert #{uri.class} into String."
103 end if not uri.is_a? String
104
105 # This Regexp supplied as an example in RFC 3986, and it works great.
106 scan = uri.scan(URIREGEX)
107 fragments = scan[0]
108 scheme = fragments[1]
109 authority = fragments[3]
110 path = fragments[4]
111 query = fragments[6]
112 fragment = fragments[8]
113 user = nil
114 password = nil
115 host = nil
116 port = nil
117 if authority != nil
118 # The Regexp above doesn't split apart the authority.
119 userinfo = authority[/^([^\[\]]*)@/, 1]
120 if userinfo != nil
121 user = userinfo.strip[/^([^:]*):?/, 1]
122 password = userinfo.strip[/:(.*)$/, 1]
123 end
124 host = authority.gsub(
125 /^([^\[\]]*)@/, EMPTY_STR
126 ).gsub(
127 /:([^:@\[\]]*?)$/, EMPTY_STR
128 )
129 port = authority[/:([^:@\[\]]*?)$/, 1]
130 end
131 if port == EMPTY_STR
132 port = nil
133 end
134
135 return new(
136 :scheme => scheme,
137 :user => user,
138 :password => password,
139 :host => host,
140 :port => port,
141 :path => path,
142 :query => query,
143 :fragment => fragment
144 )
145 end
146
147 ##
148 # Converts an input to a URI. The input does not have to be a valid
149 # URI — the method will use heuristics to guess what URI was intended.
150 # This is not standards-compliant, merely user-friendly.
151 #
152 # @param [String, Addressable::URI, #to_str] uri
153 # The URI string to parse.
154 # No parsing is performed if the object is already an
155 # <code>Addressable::URI</code>.
156 # @param [Hash] hints
157 # A <code>Hash</code> of hints to the heuristic parser.
158 # Defaults to <code>{:scheme => "http"}</code>.
159 #
160 # @return [Addressable::URI] The parsed URI.
161 def self.heuristic_parse(uri, hints={})
162 # If we were given nil, return nil.
163 return nil unless uri
164 # If a URI object is passed, just return itself.
165 return uri.dup if uri.kind_of?(self)
166
167 # If a URI object of the Ruby standard library variety is passed,
168 # convert it to a string, then parse the string.
169 # We do the check this way because we don't want to accidentally
170 # cause a missing constant exception to be thrown.
171 if uri.class.name =~ /^URI\b/
172 uri = uri.to_s
173 end
174
175 if !uri.respond_to?(:to_str)
176 raise TypeError, "Can't convert #{uri.class} into String."
177 end
178 # Otherwise, convert to a String
179 uri = uri.to_str.dup.strip
180 hints = {
181 :scheme => "http"
182 }.merge(hints)
183 case uri
184 when /^http:\/+/
185 uri.gsub!(/^http:\/+/, "http://")
186 when /^https:\/+/
187 uri.gsub!(/^https:\/+/, "https://")
188 when /^feed:\/+http:\/+/
189 uri.gsub!(/^feed:\/+http:\/+/, "feed:http://")
190 when /^feed:\/+/
191 uri.gsub!(/^feed:\/+/, "feed://")
192 when /^file:\/+/
193 uri.gsub!(/^file:\/+/, "file:///")
194 when /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
195 uri.gsub!(/^/, hints[:scheme] + "://")
196 end
197 match = uri.match(URIREGEX)
198 fragments = match.captures
199 authority = fragments[3]
200 if authority && authority.length > 0
201 new_authority = authority.gsub(/\\/, '/').gsub(/ /, '%20')
202 # NOTE: We want offset 4, not 3!
203 offset = match.offset(4)
204 uri[offset[0]...offset[1]] = new_authority
205 end
206 parsed = self.parse(uri)
207 if parsed.scheme =~ /^[^\/?#\.]+\.[^\/?#]+$/
208 parsed = self.parse(hints[:scheme] + "://" + uri)
209 end
210 if parsed.path.include?(".")
211 new_host = parsed.path[/^([^\/]+\.[^\/]*)/, 1]
212 if new_host
213 parsed.defer_validation do
214 new_path = parsed.path.gsub(
215 Regexp.new("^" + Regexp.escape(new_host)), EMPTY_STR)
216 parsed.host = new_host
217 parsed.path = new_path
218 parsed.scheme = hints[:scheme] unless parsed.scheme
219 end
220 end
221 end
222 return parsed
223 end
224
225 ##
226 # Converts a path to a file scheme URI. If the path supplied is
227 # relative, it will be returned as a relative URI. If the path supplied
228 # is actually a non-file URI, it will parse the URI as if it had been
229 # parsed with <code>Addressable::URI.parse</code>. Handles all of the
230 # various Microsoft-specific formats for specifying paths.
231 #
232 # @param [String, Addressable::URI, #to_str] path
233 # Typically a <code>String</code> path to a file or directory, but
234 # will return a sensible return value if an absolute URI is supplied
235 # instead.
236 #
237 # @return [Addressable::URI]
238 # The parsed file scheme URI or the original URI if some other URI
239 # scheme was provided.
240 #
241 # @example
242 # base = Addressable::URI.convert_path("/absolute/path/")
243 # uri = Addressable::URI.convert_path("relative/path")
244 # (base + uri).to_s
245 # #=> "file:///absolute/path/relative/path"
246 #
247 # Addressable::URI.convert_path(
248 # "c:\\windows\\My Documents 100%20\\foo.txt"
249 # ).to_s
250 # #=> "file:///c:/windows/My%20Documents%20100%20/foo.txt"
251 #
252 # Addressable::URI.convert_path("http://example.com/").to_s
253 # #=> "http://example.com/"
254 def self.convert_path(path)
255 # If we were given nil, return nil.
256 return nil unless path
257 # If a URI object is passed, just return itself.
258 return path if path.kind_of?(self)
259 if !path.respond_to?(:to_str)
260 raise TypeError, "Can't convert #{path.class} into String."
261 end
262 # Otherwise, convert to a String
263 path = path.to_str.strip
264
265 path.gsub!(/^file:\/?\/?/, EMPTY_STR) if path =~ /^file:\/?\/?/
266 path = SLASH + path if path =~ /^([a-zA-Z])[\|:]/
267 uri = self.parse(path)
268
269 if uri.scheme == nil
270 # Adjust windows-style uris
271 uri.path.gsub!(/^\/?([a-zA-Z])[\|:][\\\/]/) do
272 "/#{$1.downcase}:/"
273 end
274 uri.path.gsub!(/\\/, SLASH)
275 if File.exist?(uri.path) &&
276 File.stat(uri.path).directory?
277 uri.path.gsub!(/\/$/, EMPTY_STR)
278 uri.path = uri.path + '/'
279 end
280
281 # If the path is absolute, set the scheme and host.
282 if uri.path =~ /^\//
283 uri.scheme = "file"
284 uri.host = EMPTY_STR
285 end
286 uri.normalize!
287 end
288
289 return uri
290 end
291
292 ##
293 # Joins several URIs together.
294 #
295 # @param [String, Addressable::URI, #to_str] *uris
296 # The URIs to join.
297 #
298 # @return [Addressable::URI] The joined URI.
299 #
300 # @example
301 # base = "http://example.com/"
302 # uri = Addressable::URI.parse("relative/path")
303 # Addressable::URI.join(base, uri)
304 # #=> #<Addressable::URI:0xcab390 URI:http://example.com/relative/path>
305 def self.join(*uris)
306 uri_objects = uris.collect do |uri|
307 if !uri.respond_to?(:to_str)
308 raise TypeError, "Can't convert #{uri.class} into String."
309 end
310 uri.kind_of?(self) ? uri : self.parse(uri.to_str)
311 end
312 result = uri_objects.shift.dup
313 for uri in uri_objects
314 result.join!(uri)
315 end
316 return result
317 end
318
319 ##
320 # Percent encodes a URI component.
321 #
322 # @param [String, #to_str] component The URI component to encode.
323 #
324 # @param [String, Regexp] character_class
325 # The characters which are not percent encoded. If a <code>String</code>
326 # is passed, the <code>String</code> must be formatted as a regular
327 # expression character class. (Do not include the surrounding square
328 # brackets.) For example, <code>"b-zB-Z0-9"</code> would cause
329 # everything but the letters 'b' through 'z' and the numbers '0' through
330 # '9' to be percent encoded. If a <code>Regexp</code> is passed, the
331 # value <code>/[^b-zB-Z0-9]/</code> would have the same effect. A set of
332 # useful <code>String</code> values may be found in the
333 # <code>Addressable::URI::CharacterClasses</code> module. The default
334 # value is the reserved plus unreserved character classes specified in
335 # <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
336 #
337 # @param [Regexp] upcase_encoded
338 # A string of characters that may already be percent encoded, and whose
339 # encodings should be upcased. This allows normalization of percent
340 # encodings for characters not included in the
341 # <code>character_class</code>.
342 #
343 # @return [String] The encoded component.
344 #
345 # @example
346 # Addressable::URI.encode_component("simple/example", "b-zB-Z0-9")
347 # => "simple%2Fex%61mple"
348 # Addressable::URI.encode_component("simple/example", /[^b-zB-Z0-9]/)
349 # => "simple%2Fex%61mple"
350 # Addressable::URI.encode_component(
351 # "simple/example", Addressable::URI::CharacterClasses::UNRESERVED
352 # )
353 # => "simple%2Fexample"
354 def self.encode_component(component, character_class=
355 CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
356 upcase_encoded='')
357 return nil if component.nil?
358
359 begin
360 if component.kind_of?(Symbol) ||
361 component.kind_of?(Numeric) ||
362 component.kind_of?(TrueClass) ||
363 component.kind_of?(FalseClass)
364 component = component.to_s
365 else
366 component = component.to_str
367 end
368 rescue TypeError, NoMethodError
369 raise TypeError, "Can't convert #{component.class} into String."
370 end if !component.is_a? String
371
372 if ![String, Regexp].include?(character_class.class)
373 raise TypeError,
374 "Expected String or Regexp, got #{character_class.inspect}"
375 end
376 if character_class.kind_of?(String)
377 character_class = /[^#{character_class}]/
378 end
379 # We can't perform regexps on invalid UTF sequences, but
380 # here we need to, so switch to ASCII.
381 component = component.dup
382 component.force_encoding(Encoding::ASCII_8BIT)
383 # Avoiding gsub! because there are edge cases with frozen strings
384 component = component.gsub(character_class) do |sequence|
385 (sequence.unpack('C*').map { |c| "%" + ("%02x" % c).upcase }).join
386 end
387 if upcase_encoded.length > 0
388 component = component.gsub(/%(#{upcase_encoded.chars.map do |char|
389 char.unpack('C*').map { |c| '%02x' % c }.join
390 end.join('|')})/i) { |s| s.upcase }
391 end
392 return component
393 end
394
395 class << self
396 alias_method :encode_component, :encode_component
397 end
398
399 ##
400 # Unencodes any percent encoded characters within a URI component.
401 # This method may be used for unencoding either components or full URIs,
402 # however, it is recommended to use the <code>unencode_component</code>
403 # alias when unencoding components.
404 #
405 # @param [String, Addressable::URI, #to_str] uri
406 # The URI or component to unencode.
407 #
408 # @param [Class] return_type
409 # The type of object to return.
410 # This value may only be set to <code>String</code> or
411 # <code>Addressable::URI</code>. All other values are invalid. Defaults
412 # to <code>String</code>.
413 #
414 # @param [String] leave_encoded
415 # A string of characters to leave encoded. If a percent encoded character
416 # in this list is encountered then it will remain percent encoded.
417 #
418 # @return [String, Addressable::URI]
419 # The unencoded component or URI.
420 # The return type is determined by the <code>return_type</code>
421 # parameter.
422 def self.unencode(uri, return_type=String, leave_encoded='')
423 return nil if uri.nil?
424
425 begin
426 uri = uri.to_str
427 rescue NoMethodError, TypeError
428 raise TypeError, "Can't convert #{uri.class} into String."
429 end if !uri.is_a? String
430 if ![String, ::Addressable::URI].include?(return_type)
431 raise TypeError,
432 "Expected Class (String or Addressable::URI), " +
433 "got #{return_type.inspect}"
434 end
435 uri = uri.dup
436 # Seriously, only use UTF-8. I'm really not kidding!
437 uri.force_encoding("utf-8")
438 leave_encoded = leave_encoded.dup.force_encoding("utf-8")
439 result = uri.gsub(/%[0-9a-f]{2}/iu) do |sequence|
440 c = sequence[1..3].to_i(16).chr
441 c.force_encoding("utf-8")
442 leave_encoded.include?(c) ? sequence : c
443 end
444 result.force_encoding("utf-8")
445 if return_type == String
446 return result
447 elsif return_type == ::Addressable::URI
448 return ::Addressable::URI.parse(result)
449 end
450 end
451
452 class << self
453 alias_method :unescape, :unencode
454 alias_method :unencode_component, :unencode
455 alias_method :unescape_component, :unencode
456 end
457
458
459 ##
460 # Normalizes the encoding of a URI component.
461 #
462 # @param [String, #to_str] component The URI component to encode.
463 #
464 # @param [String, Regexp] character_class
465 # The characters which are not percent encoded. If a <code>String</code>
466 # is passed, the <code>String</code> must be formatted as a regular
467 # expression character class. (Do not include the surrounding square
468 # brackets.) For example, <code>"b-zB-Z0-9"</code> would cause
469 # everything but the letters 'b' through 'z' and the numbers '0'
470 # through '9' to be percent encoded. If a <code>Regexp</code> is passed,
471 # the value <code>/[^b-zB-Z0-9]/</code> would have the same effect. A
472 # set of useful <code>String</code> values may be found in the
473 # <code>Addressable::URI::CharacterClasses</code> module. The default
474 # value is the reserved plus unreserved character classes specified in
475 # <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
476 #
477 # @param [String] leave_encoded
478 # When <code>character_class</code> is a <code>String</code> then
479 # <code>leave_encoded</code> is a string of characters that should remain
480 # percent encoded while normalizing the component; if they appear percent
481 # encoded in the original component, then they will be upcased ("%2f"
482 # normalized to "%2F") but otherwise left alone.
483 #
484 # @return [String] The normalized component.
485 #
486 # @example
487 # Addressable::URI.normalize_component("simpl%65/%65xampl%65", "b-zB-Z")
488 # => "simple%2Fex%61mple"
489 # Addressable::URI.normalize_component(
490 # "simpl%65/%65xampl%65", /[^b-zB-Z]/
491 # )
492 # => "simple%2Fex%61mple"
493 # Addressable::URI.normalize_component(
494 # "simpl%65/%65xampl%65",
495 # Addressable::URI::CharacterClasses::UNRESERVED
496 # )
497 # => "simple%2Fexample"
498 # Addressable::URI.normalize_component(
499 # "one%20two%2fthree%26four",
500 # "0-9a-zA-Z &/",
501 # "/"
502 # )
503 # => "one two%2Fthree&four"
504 def self.normalize_component(component, character_class=
505 CharacterClasses::RESERVED + CharacterClasses::UNRESERVED,
506 leave_encoded='')
507 return nil if component.nil?
508
509 begin
510 component = component.to_str
511 rescue NoMethodError, TypeError
512 raise TypeError, "Can't convert #{component.class} into String."
513 end if !component.is_a? String
514
515 if ![String, Regexp].include?(character_class.class)
516 raise TypeError,
517 "Expected String or Regexp, got #{character_class.inspect}"
518 end
519 if character_class.kind_of?(String)
520 leave_re = if leave_encoded.length > 0
521 character_class = "#{character_class}%" unless character_class.include?('%')
522
523 "|%(?!#{leave_encoded.chars.map do |char|
524 seq = char.unpack('C*').map { |c| '%02x' % c }.join
525 [seq.upcase, seq.downcase]
526 end.flatten.join('|')})"
527 end
528
529 character_class = /[^#{character_class}]#{leave_re}/
530 end
531 # We can't perform regexps on invalid UTF sequences, but
532 # here we need to, so switch to ASCII.
533 component = component.dup
534 component.force_encoding(Encoding::ASCII_8BIT)
535 unencoded = self.unencode_component(component, String, leave_encoded)
536 begin
537 encoded = self.encode_component(
538 Addressable::IDNA.unicode_normalize_kc(unencoded),
539 character_class,
540 leave_encoded
541 )
542 rescue ArgumentError
543 encoded = self.encode_component(unencoded)
544 end
545 encoded.force_encoding(Encoding::UTF_8)
546 return encoded
547 end
548
549 ##
550 # Percent encodes any special characters in the URI.
551 #
552 # @param [String, Addressable::URI, #to_str] uri
553 # The URI to encode.
554 #
555 # @param [Class] return_type
556 # The type of object to return.
557 # This value may only be set to <code>String</code> or
558 # <code>Addressable::URI</code>. All other values are invalid. Defaults
559 # to <code>String</code>.
560 #
561 # @return [String, Addressable::URI]
562 # The encoded URI.
563 # The return type is determined by the <code>return_type</code>
564 # parameter.
565 def self.encode(uri, return_type=String)
566 return nil if uri.nil?
567
568 begin
569 uri = uri.to_str
570 rescue NoMethodError, TypeError
571 raise TypeError, "Can't convert #{uri.class} into String."
572 end if !uri.is_a? String
573
574 if ![String, ::Addressable::URI].include?(return_type)
575 raise TypeError,
576 "Expected Class (String or Addressable::URI), " +
577 "got #{return_type.inspect}"
578 end
579 uri_object = uri.kind_of?(self) ? uri : self.parse(uri)
580 encoded_uri = Addressable::URI.new(
581 :scheme => self.encode_component(uri_object.scheme,
582 Addressable::URI::CharacterClasses::SCHEME),
583 :authority => self.encode_component(uri_object.authority,
584 Addressable::URI::CharacterClasses::AUTHORITY),
585 :path => self.encode_component(uri_object.path,
586 Addressable::URI::CharacterClasses::PATH),
587 :query => self.encode_component(uri_object.query,
588 Addressable::URI::CharacterClasses::QUERY),
589 :fragment => self.encode_component(uri_object.fragment,
590 Addressable::URI::CharacterClasses::FRAGMENT)
591 )
592 if return_type == String
593 return encoded_uri.to_s
594 elsif return_type == ::Addressable::URI
595 return encoded_uri
596 end
597 end
598
599 class << self
600 alias_method :escape, :encode
601 end
602
603 ##
604 # Normalizes the encoding of a URI. Characters within a hostname are
605 # not percent encoded to allow for internationalized domain names.
606 #
607 # @param [String, Addressable::URI, #to_str] uri
608 # The URI to encode.
609 #
610 # @param [Class] return_type
611 # The type of object to return.
612 # This value may only be set to <code>String</code> or
613 # <code>Addressable::URI</code>. All other values are invalid. Defaults
614 # to <code>String</code>.
615 #
616 # @return [String, Addressable::URI]
617 # The encoded URI.
618 # The return type is determined by the <code>return_type</code>
619 # parameter.
620 def self.normalized_encode(uri, return_type=String)
621 begin
622 uri = uri.to_str
623 rescue NoMethodError, TypeError
624 raise TypeError, "Can't convert #{uri.class} into String."
625 end if !uri.is_a? String
626
627 if ![String, ::Addressable::URI].include?(return_type)
628 raise TypeError,
629 "Expected Class (String or Addressable::URI), " +
630 "got #{return_type.inspect}"
631 end
632 uri_object = uri.kind_of?(self) ? uri : self.parse(uri)
633 components = {
634 :scheme => self.unencode_component(uri_object.scheme),
635 :user => self.unencode_component(uri_object.user),
636 :password => self.unencode_component(uri_object.password),
637 :host => self.unencode_component(uri_object.host),
638 :port => (uri_object.port.nil? ? nil : uri_object.port.to_s),
639 :path => self.unencode_component(uri_object.path),
640 :query => self.unencode_component(uri_object.query),
641 :fragment => self.unencode_component(uri_object.fragment)
642 }
643 components.each do |key, value|
644 if value != nil
645 begin
646 components[key] =
647 Addressable::IDNA.unicode_normalize_kc(value.to_str)
648 rescue ArgumentError
649 # Likely a malformed UTF-8 character, skip unicode normalization
650 components[key] = value.to_str
651 end
652 end
653 end
654 encoded_uri = Addressable::URI.new(
655 :scheme => self.encode_component(components[:scheme],
656 Addressable::URI::CharacterClasses::SCHEME),
657 :user => self.encode_component(components[:user],
658 Addressable::URI::CharacterClasses::UNRESERVED),
659 :password => self.encode_component(components[:password],
660 Addressable::URI::CharacterClasses::UNRESERVED),
661 :host => components[:host],
662 :port => components[:port],
663 :path => self.encode_component(components[:path],
664 Addressable::URI::CharacterClasses::PATH),
665 :query => self.encode_component(components[:query],
666 Addressable::URI::CharacterClasses::QUERY),
667 :fragment => self.encode_component(components[:fragment],
668 Addressable::URI::CharacterClasses::FRAGMENT)
669 )
670 if return_type == String
671 return encoded_uri.to_s
672 elsif return_type == ::Addressable::URI
673 return encoded_uri
674 end
675 end
676
677 ##
678 # Encodes a set of key/value pairs according to the rules for the
679 # <code>application/x-www-form-urlencoded</code> MIME type.
680 #
681 # @param [#to_hash, #to_ary] form_values
682 # The form values to encode.
683 #
684 # @param [TrueClass, FalseClass] sort
685 # Sort the key/value pairs prior to encoding.
686 # Defaults to <code>false</code>.
687 #
688 # @return [String]
689 # The encoded value.
690 def self.form_encode(form_values, sort=false)
691 if form_values.respond_to?(:to_hash)
692 form_values = form_values.to_hash.to_a
693 elsif form_values.respond_to?(:to_ary)
694 form_values = form_values.to_ary
695 else
696 raise TypeError, "Can't convert #{form_values.class} into Array."
697 end
698
699 form_values = form_values.inject([]) do |accu, (key, value)|
700 if value.kind_of?(Array)
701 value.each do |v|
702 accu << [key.to_s, v.to_s]
703 end
704 else
705 accu << [key.to_s, value.to_s]
706 end
707 accu
708 end
709
710 if sort
711 # Useful for OAuth and optimizing caching systems
712 form_values = form_values.sort
713 end
714 escaped_form_values = form_values.map do |(key, value)|
715 # Line breaks are CRLF pairs
716 [
717 self.encode_component(
718 key.gsub(/(\r\n|\n|\r)/, "\r\n"),
719 CharacterClasses::UNRESERVED
720 ).gsub("%20", "+"),
721 self.encode_component(
722 value.gsub(/(\r\n|\n|\r)/, "\r\n"),
723 CharacterClasses::UNRESERVED
724 ).gsub("%20", "+")
725 ]
726 end
727 return escaped_form_values.map do |(key, value)|
728 "#{key}=#{value}"
729 end.join("&")
730 end
731
732 ##
733 # Decodes a <code>String</code> according to the rules for the
734 # <code>application/x-www-form-urlencoded</code> MIME type.
735 #
736 # @param [String, #to_str] encoded_value
737 # The form values to decode.
738 #
739 # @return [Array]
740 # The decoded values.
741 # This is not a <code>Hash</code> because of the possibility for
742 # duplicate keys.
743 def self.form_unencode(encoded_value)
744 if !encoded_value.respond_to?(:to_str)
745 raise TypeError, "Can't convert #{encoded_value.class} into String."
746 end
747 encoded_value = encoded_value.to_str
748 split_values = encoded_value.split("&").map do |pair|
749 pair.split("=", 2)
750 end
751 return split_values.map do |(key, value)|
752 [
753 key ? self.unencode_component(
754 key.gsub("+", "%20")).gsub(/(\r\n|\n|\r)/, "\n") : nil,
755 value ? (self.unencode_component(
756 value.gsub("+", "%20")).gsub(/(\r\n|\n|\r)/, "\n")) : nil
757 ]
758 end
759 end
760
761 ##
762 # Creates a new uri object from component parts.
763 #
764 # @option [String, #to_str] scheme The scheme component.
765 # @option [String, #to_str] user The user component.
766 # @option [String, #to_str] password The password component.
767 # @option [String, #to_str] userinfo
768 # The userinfo component. If this is supplied, the user and password
769 # components must be omitted.
770 # @option [String, #to_str] host The host component.
771 # @option [String, #to_str] port The port component.
772 # @option [String, #to_str] authority
773 # The authority component. If this is supplied, the user, password,
774 # userinfo, host, and port components must be omitted.
775 # @option [String, #to_str] path The path component.
776 # @option [String, #to_str] query The query component.
777 # @option [String, #to_str] fragment The fragment component.
778 #
779 # @return [Addressable::URI] The constructed URI object.
780 def initialize(options={})
781 if options.has_key?(:authority)
782 if (options.keys & [:userinfo, :user, :password, :host, :port]).any?
783 raise ArgumentError,
784 "Cannot specify both an authority and any of the components " +
785 "within the authority."
786 end
787 end
788 if options.has_key?(:userinfo)
789 if (options.keys & [:user, :password]).any?
790 raise ArgumentError,
791 "Cannot specify both a userinfo and either the user or password."
792 end
793 end
794
795 self.defer_validation do
796 # Bunch of crazy logic required because of the composite components
797 # like userinfo and authority.
798 self.scheme = options[:scheme] if options[:scheme]
799 self.user = options[:user] if options[:user]
800 self.password = options[:password] if options[:password]
801 self.userinfo = options[:userinfo] if options[:userinfo]
802 self.host = options[:host] if options[:host]
803 self.port = options[:port] if options[:port]
804 self.authority = options[:authority] if options[:authority]
805 self.path = options[:path] if options[:path]
806 self.query = options[:query] if options[:query]
807 self.query_values = options[:query_values] if options[:query_values]
808 self.fragment = options[:fragment] if options[:fragment]
809 end
810 self.to_s
811 end
812
813 ##
814 # Freeze URI, initializing instance variables.
815 #
816 # @return [Addressable::URI] The frozen URI object.
817 def freeze
818 self.normalized_scheme
819 self.normalized_user
820 self.normalized_password
821 self.normalized_userinfo
822 self.normalized_host
823 self.normalized_port
824 self.normalized_authority
825 self.normalized_site
826 self.normalized_path
827 self.normalized_query
828 self.normalized_fragment
829 self.hash
830 super
831 end
832
833 ##
834 # The scheme component for this URI.
835 #
836 # @return [String] The scheme component.
837 def scheme
838 return defined?(@scheme) ? @scheme : nil
839 end
840
841 ##
842 # The scheme component for this URI, normalized.
843 #
844 # @return [String] The scheme component, normalized.
845 def normalized_scheme
846 return nil unless self.scheme
847 @normalized_scheme ||= begin
848 if self.scheme =~ /^\s*ssh\+svn\s*$/i
849 "svn+ssh".dup
850 else
851 Addressable::URI.normalize_component(
852 self.scheme.strip.downcase,
853 Addressable::URI::CharacterClasses::SCHEME
854 )
855 end
856 end
857 # All normalized values should be UTF-8
858 @normalized_scheme.force_encoding(Encoding::UTF_8) if @normalized_scheme
859 @normalized_scheme
860 end
861
862 ##
863 # Sets the scheme component for this URI.
864 #
865 # @param [String, #to_str] new_scheme The new scheme component.
866 def scheme=(new_scheme)
867 if new_scheme && !new_scheme.respond_to?(:to_str)
868 raise TypeError, "Can't convert #{new_scheme.class} into String."
869 elsif new_scheme
870 new_scheme = new_scheme.to_str
871 end
872 if new_scheme && new_scheme !~ /\A[a-z][a-z0-9\.\+\-]*\z/i
873 raise InvalidURIError, "Invalid scheme format: #{new_scheme}"
874 end
875 @scheme = new_scheme
876 @scheme = nil if @scheme.to_s.strip.empty?
877
878 # Reset dependent values
879 remove_instance_variable(:@normalized_scheme) if defined?(@normalized_scheme)
880 remove_composite_values
881
882 # Ensure we haven't created an invalid URI
883 validate()
884 end
885
886 ##
887 # The user component for this URI.
888 #
889 # @return [String] The user component.
890 def user
891 return defined?(@user) ? @user : nil
892 end
893
894 ##
895 # The user component for this URI, normalized.
896 #
897 # @return [String] The user component, normalized.
898 def normalized_user
899 return nil unless self.user
900 return @normalized_user if defined?(@normalized_user)
901 @normalized_user ||= begin
902 if normalized_scheme =~ /https?/ && self.user.strip.empty? &&
903 (!self.password || self.password.strip.empty?)
904 nil
905 else
906 Addressable::URI.normalize_component(
907 self.user.strip,
908 Addressable::URI::CharacterClasses::UNRESERVED
909 )
910 end
911 end
912 # All normalized values should be UTF-8
913 @normalized_user.force_encoding(Encoding::UTF_8) if @normalized_user
914 @normalized_user
915 end
916
917 ##
918 # Sets the user component for this URI.
919 #
920 # @param [String, #to_str] new_user The new user component.
921 def user=(new_user)
922 if new_user && !new_user.respond_to?(:to_str)
923 raise TypeError, "Can't convert #{new_user.class} into String."
924 end
925 @user = new_user ? new_user.to_str : nil
926
927 # You can't have a nil user with a non-nil password
928 if password != nil
929 @user = EMPTY_STR if @user.nil?
930 end
931
932 # Reset dependent values
933 remove_instance_variable(:@userinfo) if defined?(@userinfo)
934 remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
935 remove_instance_variable(:@authority) if defined?(@authority)
936 remove_instance_variable(:@normalized_user) if defined?(@normalized_user)
937 remove_composite_values
938
939 # Ensure we haven't created an invalid URI
940 validate()
941 end
942
943 ##
944 # The password component for this URI.
945 #
946 # @return [String] The password component.
947 def password
948 return defined?(@password) ? @password : nil
949 end
950
951 ##
952 # The password component for this URI, normalized.
953 #
954 # @return [String] The password component, normalized.
955 def normalized_password
956 return nil unless self.password
957 return @normalized_password if defined?(@normalized_password)
958 @normalized_password ||= begin
959 if self.normalized_scheme =~ /https?/ && self.password.strip.empty? &&
960 (!self.user || self.user.strip.empty?)
961 nil
962 else
963 Addressable::URI.normalize_component(
964 self.password.strip,
965 Addressable::URI::CharacterClasses::UNRESERVED
966 )
967 end
968 end
969 # All normalized values should be UTF-8
970 if @normalized_password
971 @normalized_password.force_encoding(Encoding::UTF_8)
972 end
973 @normalized_password
974 end
975
976 ##
977 # Sets the password component for this URI.
978 #
979 # @param [String, #to_str] new_password The new password component.
980 def password=(new_password)
981 if new_password && !new_password.respond_to?(:to_str)
982 raise TypeError, "Can't convert #{new_password.class} into String."
983 end
984 @password = new_password ? new_password.to_str : nil
985
986 # You can't have a nil user with a non-nil password
987 @password ||= nil
988 @user ||= nil
989 if @password != nil
990 @user = EMPTY_STR if @user.nil?
991 end
992
993 # Reset dependent values
994 remove_instance_variable(:@userinfo) if defined?(@userinfo)
995 remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
996 remove_instance_variable(:@authority) if defined?(@authority)
997 remove_instance_variable(:@normalized_password) if defined?(@normalized_password)
998 remove_composite_values
999
1000 # Ensure we haven't created an invalid URI
1001 validate()
1002 end
1003
1004 ##
1005 # The userinfo component for this URI.
1006 # Combines the user and password components.
1007 #
1008 # @return [String] The userinfo component.
1009 def userinfo
1010 current_user = self.user
1011 current_password = self.password
1012 (current_user || current_password) && @userinfo ||= begin
1013 if current_user && current_password
1014 "#{current_user}:#{current_password}"
1015 elsif current_user && !current_password
1016 "#{current_user}"
1017 end
1018 end
1019 end
1020
1021 ##
1022 # The userinfo component for this URI, normalized.
1023 #
1024 # @return [String] The userinfo component, normalized.
1025 def normalized_userinfo
1026 return nil unless self.userinfo
1027 return @normalized_userinfo if defined?(@normalized_userinfo)
1028 @normalized_userinfo ||= begin
1029 current_user = self.normalized_user
1030 current_password = self.normalized_password
1031 if !current_user && !current_password
1032 nil
1033 elsif current_user && current_password
1034 "#{current_user}:#{current_password}".dup
1035 elsif current_user && !current_password
1036 "#{current_user}".dup
1037 end
1038 end
1039 # All normalized values should be UTF-8
1040 if @normalized_userinfo
1041 @normalized_userinfo.force_encoding(Encoding::UTF_8)
1042 end
1043 @normalized_userinfo
1044 end
1045
1046 ##
1047 # Sets the userinfo component for this URI.
1048 #
1049 # @param [String, #to_str] new_userinfo The new userinfo component.
1050 def userinfo=(new_userinfo)
1051 if new_userinfo && !new_userinfo.respond_to?(:to_str)
1052 raise TypeError, "Can't convert #{new_userinfo.class} into String."
1053 end
1054 new_user, new_password = if new_userinfo
1055 [
1056 new_userinfo.to_str.strip[/^(.*):/, 1],
1057 new_userinfo.to_str.strip[/:(.*)$/, 1]
1058 ]
1059 else
1060 [nil, nil]
1061 end
1062
1063 # Password assigned first to ensure validity in case of nil
1064 self.password = new_password
1065 self.user = new_user
1066
1067 # Reset dependent values
1068 remove_instance_variable(:@authority) if defined?(@authority)
1069 remove_composite_values
1070
1071 # Ensure we haven't created an invalid URI
1072 validate()
1073 end
1074
1075 ##
1076 # The host component for this URI.
1077 #
1078 # @return [String] The host component.
1079 def host
1080 return defined?(@host) ? @host : nil
1081 end
1082
1083 ##
1084 # The host component for this URI, normalized.
1085 #
1086 # @return [String] The host component, normalized.
1087 def normalized_host
1088 return nil unless self.host
1089 @normalized_host ||= begin
1090 if !self.host.strip.empty?
1091 result = ::Addressable::IDNA.to_ascii(
1092 URI.unencode_component(self.host.strip.downcase)
1093 )
1094 if result =~ /[^\.]\.$/
1095 # Single trailing dots are unnecessary.
1096 result = result[0...-1]
1097 end
1098 result = Addressable::URI.normalize_component(
1099 result,
1100 CharacterClasses::HOST)
1101 result
1102 else
1103 EMPTY_STR.dup
1104 end
1105 end
1106 # All normalized values should be UTF-8
1107 @normalized_host.force_encoding(Encoding::UTF_8) if @normalized_host
1108 @normalized_host
1109 end
1110
1111 ##
1112 # Sets the host component for this URI.
1113 #
1114 # @param [String, #to_str] new_host The new host component.
1115 def host=(new_host)
1116 if new_host && !new_host.respond_to?(:to_str)
1117 raise TypeError, "Can't convert #{new_host.class} into String."
1118 end
1119 @host = new_host ? new_host.to_str : nil
1120
1121 # Reset dependent values
1122 remove_instance_variable(:@authority) if defined?(@authority)
1123 remove_instance_variable(:@normalized_host) if defined?(@normalized_host)
1124 remove_composite_values
1125
1126 # Ensure we haven't created an invalid URI
1127 validate()
1128 end
1129
1130 ##
1131 # This method is same as URI::Generic#host except
1132 # brackets for IPv6 (and 'IPvFuture') addresses are removed.
1133 #
1134 # @see Addressable::URI#host
1135 #
1136 # @return [String] The hostname for this URI.
1137 def hostname
1138 v = self.host
1139 /\A\[(.*)\]\z/ =~ v ? $1 : v
1140 end
1141
1142 ##
1143 # This method is same as URI::Generic#host= except
1144 # the argument can be a bare IPv6 address (or 'IPvFuture').
1145 #
1146 # @see Addressable::URI#host=
1147 #
1148 # @param [String, #to_str] new_hostname The new hostname for this URI.
1149 def hostname=(new_hostname)
1150 if new_hostname &&
1151 (new_hostname.respond_to?(:ipv4?) || new_hostname.respond_to?(:ipv6?))
1152 new_hostname = new_hostname.to_s
1153 elsif new_hostname && !new_hostname.respond_to?(:to_str)
1154 raise TypeError, "Can't convert #{new_hostname.class} into String."
1155 end
1156 v = new_hostname ? new_hostname.to_str : nil
1157 v = "[#{v}]" if /\A\[.*\]\z/ !~ v && /:/ =~ v
1158 self.host = v
1159 end
1160
1161 ##
1162 # Returns the top-level domain for this host.
1163 #
1164 # @example
1165 # Addressable::URI.parse("www.example.co.uk").tld # => "co.uk"
1166 def tld
1167 PublicSuffix.parse(self.host, ignore_private: true).tld
1168 end
1169
1170 ##
1171 # Returns the public suffix domain for this host.
1172 #
1173 # @example
1174 # Addressable::URI.parse("www.example.co.uk").domain # => "example.co.uk"
1175 def domain
1176 PublicSuffix.domain(self.host, ignore_private: true)
1177 end
1178
1179 ##
1180 # The authority component for this URI.
1181 # Combines the user, password, host, and port components.
1182 #
1183 # @return [String] The authority component.
1184 def authority
1185 self.host && @authority ||= begin
1186 authority = String.new
1187 if self.userinfo != nil
1188 authority << "#{self.userinfo}@"
1189 end
1190 authority << self.host
1191 if self.port != nil
1192 authority << ":#{self.port}"
1193 end
1194 authority
1195 end
1196 end
1197
1198 ##
1199 # The authority component for this URI, normalized.
1200 #
1201 # @return [String] The authority component, normalized.
1202 def normalized_authority
1203 return nil unless self.authority
1204 @normalized_authority ||= begin
1205 authority = String.new
1206 if self.normalized_userinfo != nil
1207 authority << "#{self.normalized_userinfo}@"
1208 end
1209 authority << self.normalized_host
1210 if self.normalized_port != nil
1211 authority << ":#{self.normalized_port}"
1212 end
1213 authority
1214 end
1215 # All normalized values should be UTF-8
1216 if @normalized_authority
1217 @normalized_authority.force_encoding(Encoding::UTF_8)
1218 end
1219 @normalized_authority
1220 end
1221
1222 ##
1223 # Sets the authority component for this URI.
1224 #
1225 # @param [String, #to_str] new_authority The new authority component.
1226 def authority=(new_authority)
1227 if new_authority
1228 if !new_authority.respond_to?(:to_str)
1229 raise TypeError, "Can't convert #{new_authority.class} into String."
1230 end
1231 new_authority = new_authority.to_str
1232 new_userinfo = new_authority[/^([^\[\]]*)@/, 1]
1233 if new_userinfo
1234 new_user = new_userinfo.strip[/^([^:]*):?/, 1]
1235 new_password = new_userinfo.strip[/:(.*)$/, 1]
1236 end
1237 new_host = new_authority.gsub(
1238 /^([^\[\]]*)@/, EMPTY_STR
1239 ).gsub(
1240 /:([^:@\[\]]*?)$/, EMPTY_STR
1241 )
1242 new_port =
1243 new_authority[/:([^:@\[\]]*?)$/, 1]
1244 end
1245
1246 # Password assigned first to ensure validity in case of nil
1247 self.password = defined?(new_password) ? new_password : nil
1248 self.user = defined?(new_user) ? new_user : nil
1249 self.host = defined?(new_host) ? new_host : nil
1250 self.port = defined?(new_port) ? new_port : nil
1251
1252 # Reset dependent values
1253 remove_instance_variable(:@userinfo) if defined?(@userinfo)
1254 remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
1255 remove_composite_values
1256
1257 # Ensure we haven't created an invalid URI
1258 validate()
1259 end
1260
1261 ##
1262 # The origin for this URI, serialized to ASCII, as per
1263 # RFC 6454, section 6.2.
1264 #
1265 # @return [String] The serialized origin.
1266 def origin
1267 if self.scheme && self.authority
1268 if self.normalized_port
1269 "#{self.normalized_scheme}://#{self.normalized_host}" +
1270 ":#{self.normalized_port}"
1271 else
1272 "#{self.normalized_scheme}://#{self.normalized_host}"
1273 end
1274 else
1275 "null"
1276 end
1277 end
1278
1279 ##
1280 # Sets the origin for this URI, serialized to ASCII, as per
1281 # RFC 6454, section 6.2. This assignment will reset the `userinfo`
1282 # component.
1283 #
1284 # @param [String, #to_str] new_origin The new origin component.
1285 def origin=(new_origin)
1286 if new_origin
1287 if !new_origin.respond_to?(:to_str)
1288 raise TypeError, "Can't convert #{new_origin.class} into String."
1289 end
1290 new_origin = new_origin.to_str
1291 new_scheme = new_origin[/^([^:\/?#]+):\/\//, 1]
1292 unless new_scheme
1293 raise InvalidURIError, 'An origin cannot omit the scheme.'
1294 end
1295 new_host = new_origin[/:\/\/([^\/?#:]+)/, 1]
1296 unless new_host
1297 raise InvalidURIError, 'An origin cannot omit the host.'
1298 end
1299 new_port = new_origin[/:([^:@\[\]\/]*?)$/, 1]
1300 end
1301
1302 self.scheme = defined?(new_scheme) ? new_scheme : nil
1303 self.host = defined?(new_host) ? new_host : nil
1304 self.port = defined?(new_port) ? new_port : nil
1305 self.userinfo = nil
1306
1307 # Reset dependent values
1308 remove_instance_variable(:@userinfo) if defined?(@userinfo)
1309 remove_instance_variable(:@normalized_userinfo) if defined?(@normalized_userinfo)
1310 remove_instance_variable(:@authority) if defined?(@authority)
1311 remove_instance_variable(:@normalized_authority) if defined?(@normalized_authority)
1312 remove_composite_values
1313
1314 # Ensure we haven't created an invalid URI
1315 validate()
1316 end
1317
1318 # Returns an array of known ip-based schemes. These schemes typically
1319 # use a similar URI form:
1320 # <code>//<user>:<password>@<host>:<port>/<url-path></code>
1321 def self.ip_based_schemes
1322 return self.port_mapping.keys
1323 end
1324
1325 # Returns a hash of common IP-based schemes and their default port
1326 # numbers. Adding new schemes to this hash, as necessary, will allow
1327 # for better URI normalization.
1328 def self.port_mapping
1329 PORT_MAPPING
1330 end
1331
1332 ##
1333 # The port component for this URI.
1334 # This is the port number actually given in the URI. This does not
1335 # infer port numbers from default values.
1336 #
1337 # @return [Integer] The port component.
1338 def port
1339 return defined?(@port) ? @port : nil
1340 end
1341
1342 ##
1343 # The port component for this URI, normalized.
1344 #
1345 # @return [Integer] The port component, normalized.
1346 def normalized_port
1347 return nil unless self.port
1348 return @normalized_port if defined?(@normalized_port)
1349 @normalized_port ||= begin
1350 if URI.port_mapping[self.normalized_scheme] == self.port
1351 nil
1352 else
1353 self.port
1354 end
1355 end
1356 end
1357
1358 ##
1359 # Sets the port component for this URI.
1360 #
1361 # @param [String, Integer, #to_s] new_port The new port component.
1362 def port=(new_port)
1363 if new_port != nil && new_port.respond_to?(:to_str)
1364 new_port = Addressable::URI.unencode_component(new_port.to_str)
1365 end
1366
1367 if new_port.respond_to?(:valid_encoding?) && !new_port.valid_encoding?
1368 raise InvalidURIError, "Invalid encoding in port"
1369 end
1370
1371 if new_port != nil && !(new_port.to_s =~ /^\d+$/)
1372 raise InvalidURIError,
1373 "Invalid port number: #{new_port.inspect}"
1374 end
1375
1376 @port = new_port.to_s.to_i
1377 @port = nil if @port == 0
1378
1379 # Reset dependent values
1380 remove_instance_variable(:@authority) if defined?(@authority)
1381 remove_instance_variable(:@normalized_port) if defined?(@normalized_port)
1382 remove_composite_values
1383
1384 # Ensure we haven't created an invalid URI
1385 validate()
1386 end
1387
1388 ##
1389 # The inferred port component for this URI.
1390 # This method will normalize to the default port for the URI's scheme if
1391 # the port isn't explicitly specified in the URI.
1392 #
1393 # @return [Integer] The inferred port component.
1394 def inferred_port
1395 if self.port.to_i == 0
1396 self.default_port
1397 else
1398 self.port.to_i
1399 end
1400 end
1401
1402 ##
1403 # The default port for this URI's scheme.
1404 # This method will always returns the default port for the URI's scheme
1405 # regardless of the presence of an explicit port in the URI.
1406 #
1407 # @return [Integer] The default port.
1408 def default_port
1409 URI.port_mapping[self.scheme.strip.downcase] if self.scheme
1410 end
1411
1412 ##
1413 # The combination of components that represent a site.
1414 # Combines the scheme, user, password, host, and port components.
1415 # Primarily useful for HTTP and HTTPS.
1416 #
1417 # For example, <code>"http://example.com/path?query"</code> would have a
1418 # <code>site</code> value of <code>"http://example.com"</code>.
1419 #
1420 # @return [String] The components that identify a site.
1421 def site
1422 (self.scheme || self.authority) && @site ||= begin
1423 site_string = "".dup
1424 site_string << "#{self.scheme}:" if self.scheme != nil
1425 site_string << "//#{self.authority}" if self.authority != nil
1426 site_string
1427 end
1428 end
1429
1430 ##
1431 # The normalized combination of components that represent a site.
1432 # Combines the scheme, user, password, host, and port components.
1433 # Primarily useful for HTTP and HTTPS.
1434 #
1435 # For example, <code>"http://example.com/path?query"</code> would have a
1436 # <code>site</code> value of <code>"http://example.com"</code>.
1437 #
1438 # @return [String] The normalized components that identify a site.
1439 def normalized_site
1440 return nil unless self.site
1441 @normalized_site ||= begin
1442 site_string = "".dup
1443 if self.normalized_scheme != nil
1444 site_string << "#{self.normalized_scheme}:"
1445 end
1446 if self.normalized_authority != nil
1447 site_string << "//#{self.normalized_authority}"
1448 end
1449 site_string
1450 end
1451 # All normalized values should be UTF-8
1452 @normalized_site.force_encoding(Encoding::UTF_8) if @normalized_site
1453 @normalized_site
1454 end
1455
1456 ##
1457 # Sets the site value for this URI.
1458 #
1459 # @param [String, #to_str] new_site The new site value.
1460 def site=(new_site)
1461 if new_site
1462 if !new_site.respond_to?(:to_str)
1463 raise TypeError, "Can't convert #{new_site.class} into String."
1464 end
1465 new_site = new_site.to_str
1466 # These two regular expressions derived from the primary parsing
1467 # expression
1468 self.scheme = new_site[/^(?:([^:\/?#]+):)?(?:\/\/(?:[^\/?#]*))?$/, 1]
1469 self.authority = new_site[
1470 /^(?:(?:[^:\/?#]+):)?(?:\/\/([^\/?#]*))?$/, 1
1471 ]
1472 else
1473 self.scheme = nil
1474 self.authority = nil
1475 end
1476 end
1477
1478 ##
1479 # The path component for this URI.
1480 #
1481 # @return [String] The path component.
1482 def path
1483 return defined?(@path) ? @path : EMPTY_STR
1484 end
1485
1486 NORMPATH = /^(?!\/)[^\/:]*:.*$/
1487 ##
1488 # The path component for this URI, normalized.
1489 #
1490 # @return [String] The path component, normalized.
1491 def normalized_path
1492 @normalized_path ||= begin
1493 path = self.path.to_s
1494 if self.scheme == nil && path =~ NORMPATH
1495 # Relative paths with colons in the first segment are ambiguous.
1496 path = path.sub(":", "%2F")
1497 end
1498 # String#split(delimeter, -1) uses the more strict splitting behavior
1499 # found by default in Python.
1500 result = path.strip.split(SLASH, -1).map do |segment|
1501 Addressable::URI.normalize_component(
1502 segment,
1503 Addressable::URI::CharacterClasses::PCHAR
1504 )
1505 end.join(SLASH)
1506
1507 result = URI.normalize_path(result)
1508 if result.empty? &&
1509 ["http", "https", "ftp", "tftp"].include?(self.normalized_scheme)
1510 result = SLASH.dup
1511 end
1512 result
1513 end
1514 # All normalized values should be UTF-8
1515 @normalized_path.force_encoding(Encoding::UTF_8) if @normalized_path
1516 @normalized_path
1517 end
1518
1519 ##
1520 # Sets the path component for this URI.
1521 #
1522 # @param [String, #to_str] new_path The new path component.
1523 def path=(new_path)
1524 if new_path && !new_path.respond_to?(:to_str)
1525 raise TypeError, "Can't convert #{new_path.class} into String."
1526 end
1527 @path = (new_path || EMPTY_STR).to_str
1528 if !@path.empty? && @path[0..0] != SLASH && host != nil
1529 @path = "/#{@path}"
1530 end
1531
1532 # Reset dependent values
1533 remove_instance_variable(:@normalized_path) if defined?(@normalized_path)
1534 remove_composite_values
1535
1536 # Ensure we haven't created an invalid URI
1537 validate()
1538 end
1539
1540 ##
1541 # The basename, if any, of the file in the path component.
1542 #
1543 # @return [String] The path's basename.
1544 def basename
1545 # Path cannot be nil
1546 return File.basename(self.path).gsub(/;[^\/]*$/, EMPTY_STR)
1547 end
1548
1549 ##
1550 # The extname, if any, of the file in the path component.
1551 # Empty string if there is no extension.
1552 #
1553 # @return [String] The path's extname.
1554 def extname
1555 return nil unless self.path
1556 return File.extname(self.basename)
1557 end
1558
1559 ##
1560 # The query component for this URI.
1561 #
1562 # @return [String] The query component.
1563 def query
1564 return defined?(@query) ? @query : nil
1565 end
1566
1567 ##
1568 # The query component for this URI, normalized.
1569 #
1570 # @return [String] The query component, normalized.
1571 def normalized_query(*flags)
1572 return nil unless self.query
1573 return @normalized_query if defined?(@normalized_query)
1574 @normalized_query ||= begin
1575 modified_query_class = Addressable::URI::CharacterClasses::QUERY.dup
1576 # Make sure possible key-value pair delimiters are escaped.
1577 modified_query_class.sub!("\\&", "").sub!("\\;", "")
1578 pairs = (self.query || "").split("&", -1)
1579 pairs.sort! if flags.include?(:sorted)
1580 component = pairs.map do |pair|
1581 Addressable::URI.normalize_component(pair, modified_query_class, "+")
1582 end.join("&")
1583 component == "" ? nil : component
1584 end
1585 # All normalized values should be UTF-8
1586 @normalized_query.force_encoding(Encoding::UTF_8) if @normalized_query
1587 @normalized_query
1588 end
1589
1590 ##
1591 # Sets the query component for this URI.
1592 #
1593 # @param [String, #to_str] new_query The new query component.
1594 def query=(new_query)
1595 if new_query && !new_query.respond_to?(:to_str)
1596 raise TypeError, "Can't convert #{new_query.class} into String."
1597 end
1598 @query = new_query ? new_query.to_str : nil
1599
1600 # Reset dependent values
1601 remove_instance_variable(:@normalized_query) if defined?(@normalized_query)
1602 remove_composite_values
1603 end
1604
1605 ##
1606 # Converts the query component to a Hash value.
1607 #
1608 # @param [Class] return_type The return type desired. Value must be either
1609 # `Hash` or `Array`.
1610 #
1611 # @return [Hash, Array, nil] The query string parsed as a Hash or Array
1612 # or nil if the query string is blank.
1613 #
1614 # @example
1615 # Addressable::URI.parse("?one=1&two=2&three=3").query_values
1616 # #=> {"one" => "1", "two" => "2", "three" => "3"}
1617 # Addressable::URI.parse("?one=two&one=three").query_values(Array)
1618 # #=> [["one", "two"], ["one", "three"]]
1619 # Addressable::URI.parse("?one=two&one=three").query_values(Hash)
1620 # #=> {"one" => "three"}
1621 # Addressable::URI.parse("?").query_values
1622 # #=> {}
1623 # Addressable::URI.parse("").query_values
1624 # #=> nil
1625 def query_values(return_type=Hash)
1626 empty_accumulator = Array == return_type ? [] : {}
1627 if return_type != Hash && return_type != Array
1628 raise ArgumentError, "Invalid return type. Must be Hash or Array."
1629 end
1630 return nil if self.query == nil
1631 split_query = self.query.split("&").map do |pair|
1632 pair.split("=", 2) if pair && !pair.empty?
1633 end.compact
1634 return split_query.inject(empty_accumulator.dup) do |accu, pair|
1635 # I'd rather use key/value identifiers instead of array lookups,
1636 # but in this case I really want to maintain the exact pair structure,
1637 # so it's best to make all changes in-place.
1638 pair[0] = URI.unencode_component(pair[0])
1639 if pair[1].respond_to?(:to_str)
1640 # I loathe the fact that I have to do this. Stupid HTML 4.01.
1641 # Treating '+' as a space was just an unbelievably bad idea.
1642 # There was nothing wrong with '%20'!
1643 # If it ain't broke, don't fix it!
1644 pair[1] = URI.unencode_component(pair[1].to_str.gsub(/\+/, " "))
1645 end
1646 if return_type == Hash
1647 accu[pair[0]] = pair[1]
1648 else
1649 accu << pair
1650 end
1651 accu
1652 end
1653 end
1654
1655 ##
1656 # Sets the query component for this URI from a Hash object.
1657 # An empty Hash or Array will result in an empty query string.
1658 #
1659 # @param [Hash, #to_hash, Array] new_query_values The new query values.
1660 #
1661 # @example
1662 # uri.query_values = {:a => "a", :b => ["c", "d", "e"]}
1663 # uri.query
1664 # # => "a=a&b=c&b=d&b=e"
1665 # uri.query_values = [['a', 'a'], ['b', 'c'], ['b', 'd'], ['b', 'e']]
1666 # uri.query
1667 # # => "a=a&b=c&b=d&b=e"
1668 # uri.query_values = [['a', 'a'], ['b', ['c', 'd', 'e']]]
1669 # uri.query
1670 # # => "a=a&b=c&b=d&b=e"
1671 # uri.query_values = [['flag'], ['key', 'value']]
1672 # uri.query
1673 # # => "flag&key=value"
1674 def query_values=(new_query_values)
1675 if new_query_values == nil
1676 self.query = nil
1677 return nil
1678 end
1679
1680 if !new_query_values.is_a?(Array)
1681 if !new_query_values.respond_to?(:to_hash)
1682 raise TypeError,
1683 "Can't convert #{new_query_values.class} into Hash."
1684 end
1685 new_query_values = new_query_values.to_hash
1686 new_query_values = new_query_values.map do |key, value|
1687 key = key.to_s if key.kind_of?(Symbol)
1688 [key, value]
1689 end
1690 # Useful default for OAuth and caching.
1691 # Only to be used for non-Array inputs. Arrays should preserve order.
1692 new_query_values.sort!
1693 end
1694
1695 # new_query_values have form [['key1', 'value1'], ['key2', 'value2']]
1696 buffer = "".dup
1697 new_query_values.each do |key, value|
1698 encoded_key = URI.encode_component(
1699 key, CharacterClasses::UNRESERVED
1700 )
1701 if value == nil
1702 buffer << "#{encoded_key}&"
1703 elsif value.kind_of?(Array)
1704 value.each do |sub_value|
1705 encoded_value = URI.encode_component(
1706 sub_value, CharacterClasses::UNRESERVED
1707 )
1708 buffer << "#{encoded_key}=#{encoded_value}&"
1709 end
1710 else
1711 encoded_value = URI.encode_component(
1712 value, CharacterClasses::UNRESERVED
1713 )
1714 buffer << "#{encoded_key}=#{encoded_value}&"
1715 end
1716 end
1717 self.query = buffer.chop
1718 end
1719
1720 ##
1721 # The HTTP request URI for this URI. This is the path and the
1722 # query string.
1723 #
1724 # @return [String] The request URI required for an HTTP request.
1725 def request_uri
1726 return nil if self.absolute? && self.scheme !~ /^https?$/i
1727 return (
1728 (!self.path.empty? ? self.path : SLASH) +
1729 (self.query ? "?#{self.query}" : EMPTY_STR)
1730 )
1731 end
1732
1733 ##
1734 # Sets the HTTP request URI for this URI.
1735 #
1736 # @param [String, #to_str] new_request_uri The new HTTP request URI.
1737 def request_uri=(new_request_uri)
1738 if !new_request_uri.respond_to?(:to_str)
1739 raise TypeError, "Can't convert #{new_request_uri.class} into String."
1740 end
1741 if self.absolute? && self.scheme !~ /^https?$/i
1742 raise InvalidURIError,
1743 "Cannot set an HTTP request URI for a non-HTTP URI."
1744 end
1745 new_request_uri = new_request_uri.to_str
1746 path_component = new_request_uri[/^([^\?]*)\?(?:.*)$/, 1]
1747 query_component = new_request_uri[/^(?:[^\?]*)\?(.*)$/, 1]
1748 path_component = path_component.to_s
1749 path_component = (!path_component.empty? ? path_component : SLASH)
1750 self.path = path_component
1751 self.query = query_component
1752
1753 # Reset dependent values
1754 remove_composite_values
1755 end
1756
1757 ##
1758 # The fragment component for this URI.
1759 #
1760 # @return [String] The fragment component.
1761 def fragment
1762 return defined?(@fragment) ? @fragment : nil
1763 end
1764
1765 ##
1766 # The fragment component for this URI, normalized.
1767 #
1768 # @return [String] The fragment component, normalized.
1769 def normalized_fragment
1770 return nil unless self.fragment
1771 return @normalized_fragment if defined?(@normalized_fragment)
1772 @normalized_fragment ||= begin
1773 component = Addressable::URI.normalize_component(
1774 self.fragment,
1775 Addressable::URI::CharacterClasses::FRAGMENT
1776 )
1777 component == "" ? nil : component
1778 end
1779 # All normalized values should be UTF-8
1780 if @normalized_fragment
1781 @normalized_fragment.force_encoding(Encoding::UTF_8)
1782 end
1783 @normalized_fragment
1784 end
1785
1786 ##
1787 # Sets the fragment component for this URI.
1788 #
1789 # @param [String, #to_str] new_fragment The new fragment component.
1790 def fragment=(new_fragment)
1791 if new_fragment && !new_fragment.respond_to?(:to_str)
1792 raise TypeError, "Can't convert #{new_fragment.class} into String."
1793 end
1794 @fragment = new_fragment ? new_fragment.to_str : nil
1795
1796 # Reset dependent values
1797 remove_instance_variable(:@normalized_fragment) if defined?(@normalized_fragment)
1798 remove_composite_values
1799
1800 # Ensure we haven't created an invalid URI
1801 validate()
1802 end
1803
1804 ##
1805 # Determines if the scheme indicates an IP-based protocol.
1806 #
1807 # @return [TrueClass, FalseClass]
1808 # <code>true</code> if the scheme indicates an IP-based protocol.
1809 # <code>false</code> otherwise.
1810 def ip_based?
1811 if self.scheme
1812 return URI.ip_based_schemes.include?(
1813 self.scheme.strip.downcase)
1814 end
1815 return false
1816 end
1817
1818 ##
1819 # Determines if the URI is relative.
1820 #
1821 # @return [TrueClass, FalseClass]
1822 # <code>true</code> if the URI is relative. <code>false</code>
1823 # otherwise.
1824 def relative?
1825 return self.scheme.nil?
1826 end
1827
1828 ##
1829 # Determines if the URI is absolute.
1830 #
1831 # @return [TrueClass, FalseClass]
1832 # <code>true</code> if the URI is absolute. <code>false</code>
1833 # otherwise.
1834 def absolute?
1835 return !relative?
1836 end
1837
1838 ##
1839 # Joins two URIs together.
1840 #
1841 # @param [String, Addressable::URI, #to_str] The URI to join with.
1842 #
1843 # @return [Addressable::URI] The joined URI.
1844 def join(uri)
1845 if !uri.respond_to?(:to_str)
1846 raise TypeError, "Can't convert #{uri.class} into String."
1847 end
1848 if !uri.kind_of?(URI)
1849 # Otherwise, convert to a String, then parse.
1850 uri = URI.parse(uri.to_str)
1851 end
1852 if uri.to_s.empty?
1853 return self.dup
1854 end
1855
1856 joined_scheme = nil
1857 joined_user = nil
1858 joined_password = nil
1859 joined_host = nil
1860 joined_port = nil
1861 joined_path = nil
1862 joined_query = nil
1863 joined_fragment = nil
1864
1865 # Section 5.2.2 of RFC 3986
1866 if uri.scheme != nil
1867 joined_scheme = uri.scheme
1868 joined_user = uri.user
1869 joined_password = uri.password
1870 joined_host = uri.host
1871 joined_port = uri.port
1872 joined_path = URI.normalize_path(uri.path)
1873 joined_query = uri.query
1874 else
1875 if uri.authority != nil
1876 joined_user = uri.user
1877 joined_password = uri.password
1878 joined_host = uri.host
1879 joined_port = uri.port
1880 joined_path = URI.normalize_path(uri.path)
1881 joined_query = uri.query
1882 else
1883 if uri.path == nil || uri.path.empty?
1884 joined_path = self.path
1885 if uri.query != nil
1886 joined_query = uri.query
1887 else
1888 joined_query = self.query
1889 end
1890 else
1891 if uri.path[0..0] == SLASH
1892 joined_path = URI.normalize_path(uri.path)
1893 else
1894 base_path = self.path.dup
1895 base_path = EMPTY_STR if base_path == nil
1896 base_path = URI.normalize_path(base_path)
1897
1898 # Section 5.2.3 of RFC 3986
1899 #
1900 # Removes the right-most path segment from the base path.
1901 if base_path =~ /\//
1902 base_path.gsub!(/\/[^\/]+$/, SLASH)
1903 else
1904 base_path = EMPTY_STR
1905 end
1906
1907 # If the base path is empty and an authority segment has been
1908 # defined, use a base path of SLASH
1909 if base_path.empty? && self.authority != nil
1910 base_path = SLASH
1911 end
1912
1913 joined_path = URI.normalize_path(base_path + uri.path)
1914 end
1915 joined_query = uri.query
1916 end
1917 joined_user = self.user
1918 joined_password = self.password
1919 joined_host = self.host
1920 joined_port = self.port
1921 end
1922 joined_scheme = self.scheme
1923 end
1924 joined_fragment = uri.fragment
1925
1926 return self.class.new(
1927 :scheme => joined_scheme,
1928 :user => joined_user,
1929 :password => joined_password,
1930 :host => joined_host,
1931 :port => joined_port,
1932 :path => joined_path,
1933 :query => joined_query,
1934 :fragment => joined_fragment
1935 )
1936 end
1937 alias_method :+, :join
1938
1939 ##
1940 # Destructive form of <code>join</code>.
1941 #
1942 # @param [String, Addressable::URI, #to_str] The URI to join with.
1943 #
1944 # @return [Addressable::URI] The joined URI.
1945 #
1946 # @see Addressable::URI#join
1947 def join!(uri)
1948 replace_self(self.join(uri))
1949 end
1950
1951 ##
1952 # Merges a URI with a <code>Hash</code> of components.
1953 # This method has different behavior from <code>join</code>. Any
1954 # components present in the <code>hash</code> parameter will override the
1955 # original components. The path component is not treated specially.
1956 #
1957 # @param [Hash, Addressable::URI, #to_hash] The components to merge with.
1958 #
1959 # @return [Addressable::URI] The merged URI.
1960 #
1961 # @see Hash#merge
1962 def merge(hash)
1963 if !hash.respond_to?(:to_hash)
1964 raise TypeError, "Can't convert #{hash.class} into Hash."
1965 end
1966 hash = hash.to_hash
1967
1968 if hash.has_key?(:authority)
1969 if (hash.keys & [:userinfo, :user, :password, :host, :port]).any?
1970 raise ArgumentError,
1971 "Cannot specify both an authority and any of the components " +
1972 "within the authority."
1973 end
1974 end
1975 if hash.has_key?(:userinfo)
1976 if (hash.keys & [:user, :password]).any?
1977 raise ArgumentError,
1978 "Cannot specify both a userinfo and either the user or password."
1979 end
1980 end
1981
1982 uri = self.class.new
1983 uri.defer_validation do
1984 # Bunch of crazy logic required because of the composite components
1985 # like userinfo and authority.
1986 uri.scheme =
1987 hash.has_key?(:scheme) ? hash[:scheme] : self.scheme
1988 if hash.has_key?(:authority)
1989 uri.authority =
1990 hash.has_key?(:authority) ? hash[:authority] : self.authority
1991 end
1992 if hash.has_key?(:userinfo)
1993 uri.userinfo =
1994 hash.has_key?(:userinfo) ? hash[:userinfo] : self.userinfo
1995 end
1996 if !hash.has_key?(:userinfo) && !hash.has_key?(:authority)
1997 uri.user =
1998 hash.has_key?(:user) ? hash[:user] : self.user
1999 uri.password =
2000 hash.has_key?(:password) ? hash[:password] : self.password
2001 end
2002 if !hash.has_key?(:authority)
2003 uri.host =
2004 hash.has_key?(:host) ? hash[:host] : self.host
2005 uri.port =
2006 hash.has_key?(:port) ? hash[:port] : self.port
2007 end
2008 uri.path =
2009 hash.has_key?(:path) ? hash[:path] : self.path
2010 uri.query =
2011 hash.has_key?(:query) ? hash[:query] : self.query
2012 uri.fragment =
2013 hash.has_key?(:fragment) ? hash[:fragment] : self.fragment
2014 end
2015
2016 return uri
2017 end
2018
2019 ##
2020 # Destructive form of <code>merge</code>.
2021 #
2022 # @param [Hash, Addressable::URI, #to_hash] The components to merge with.
2023 #
2024 # @return [Addressable::URI] The merged URI.
2025 #
2026 # @see Addressable::URI#merge
2027 def merge!(uri)
2028 replace_self(self.merge(uri))
2029 end
2030
2031 ##
2032 # Returns the shortest normalized relative form of this URI that uses the
2033 # supplied URI as a base for resolution. Returns an absolute URI if
2034 # necessary. This is effectively the opposite of <code>route_to</code>.
2035 #
2036 # @param [String, Addressable::URI, #to_str] uri The URI to route from.
2037 #
2038 # @return [Addressable::URI]
2039 # The normalized relative URI that is equivalent to the original URI.
2040 def route_from(uri)
2041 uri = URI.parse(uri).normalize
2042 normalized_self = self.normalize
2043 if normalized_self.relative?
2044 raise ArgumentError, "Expected absolute URI, got: #{self.to_s}"
2045 end
2046 if uri.relative?
2047 raise ArgumentError, "Expected absolute URI, got: #{uri.to_s}"
2048 end
2049 if normalized_self == uri
2050 return Addressable::URI.parse("##{normalized_self.fragment}")
2051 end
2052 components = normalized_self.to_hash
2053 if normalized_self.scheme == uri.scheme
2054 components[:scheme] = nil
2055 if normalized_self.authority == uri.authority
2056 components[:user] = nil
2057 components[:password] = nil
2058 components[:host] = nil
2059 components[:port] = nil
2060 if normalized_self.path == uri.path
2061 components[:path] = nil
2062 if normalized_self.query == uri.query
2063 components[:query] = nil
2064 end
2065 else
2066 if uri.path != SLASH and components[:path]
2067 self_splitted_path = split_path(components[:path])
2068 uri_splitted_path = split_path(uri.path)
2069 self_dir = self_splitted_path.shift
2070 uri_dir = uri_splitted_path.shift
2071 while !self_splitted_path.empty? && !uri_splitted_path.empty? and self_dir == uri_dir
2072 self_dir = self_splitted_path.shift
2073 uri_dir = uri_splitted_path.shift
2074 end
2075 components[:path] = (uri_splitted_path.fill('..') + [self_dir] + self_splitted_path).join(SLASH)
2076 end
2077 end
2078 end
2079 end
2080 # Avoid network-path references.
2081 if components[:host] != nil
2082 components[:scheme] = normalized_self.scheme
2083 end
2084 return Addressable::URI.new(
2085 :scheme => components[:scheme],
2086 :user => components[:user],
2087 :password => components[:password],
2088 :host => components[:host],
2089 :port => components[:port],
2090 :path => components[:path],
2091 :query => components[:query],
2092 :fragment => components[:fragment]
2093 )
2094 end
2095
2096 ##
2097 # Returns the shortest normalized relative form of the supplied URI that
2098 # uses this URI as a base for resolution. Returns an absolute URI if
2099 # necessary. This is effectively the opposite of <code>route_from</code>.
2100 #
2101 # @param [String, Addressable::URI, #to_str] uri The URI to route to.
2102 #
2103 # @return [Addressable::URI]
2104 # The normalized relative URI that is equivalent to the supplied URI.
2105 def route_to(uri)
2106 return URI.parse(uri).route_from(self)
2107 end
2108
2109 ##
2110 # Returns a normalized URI object.
2111 #
2112 # NOTE: This method does not attempt to fully conform to specifications.
2113 # It exists largely to correct other people's failures to read the
2114 # specifications, and also to deal with caching issues since several
2115 # different URIs may represent the same resource and should not be
2116 # cached multiple times.
2117 #
2118 # @return [Addressable::URI] The normalized URI.
2119 def normalize
2120 # This is a special exception for the frequently misused feed
2121 # URI scheme.
2122 if normalized_scheme == "feed"
2123 if self.to_s =~ /^feed:\/*http:\/*/
2124 return URI.parse(
2125 self.to_s[/^feed:\/*(http:\/*.*)/, 1]
2126 ).normalize
2127 end
2128 end
2129
2130 return self.class.new(
2131 :scheme => normalized_scheme,
2132 :authority => normalized_authority,
2133 :path => normalized_path,
2134 :query => normalized_query,
2135 :fragment => normalized_fragment
2136 )
2137 end
2138
2139 ##
2140 # Destructively normalizes this URI object.
2141 #
2142 # @return [Addressable::URI] The normalized URI.
2143 #
2144 # @see Addressable::URI#normalize
2145 def normalize!
2146 replace_self(self.normalize)
2147 end
2148
2149 ##
2150 # Creates a URI suitable for display to users. If semantic attacks are
2151 # likely, the application should try to detect these and warn the user.
2152 # See <a href="http://www.ietf.org/rfc/rfc3986.txt">RFC 3986</a>,
2153 # section 7.6 for more information.
2154 #
2155 # @return [Addressable::URI] A URI suitable for display purposes.
2156 def display_uri
2157 display_uri = self.normalize
2158 display_uri.host = ::Addressable::IDNA.to_unicode(display_uri.host)
2159 return display_uri
2160 end
2161
2162 ##
2163 # Returns <code>true</code> if the URI objects are equal. This method
2164 # normalizes both URIs before doing the comparison, and allows comparison
2165 # against <code>Strings</code>.
2166 #
2167 # @param [Object] uri The URI to compare.
2168 #
2169 # @return [TrueClass, FalseClass]
2170 # <code>true</code> if the URIs are equivalent, <code>false</code>
2171 # otherwise.
2172 def ===(uri)
2173 if uri.respond_to?(:normalize)
2174 uri_string = uri.normalize.to_s
2175 else
2176 begin
2177 uri_string = ::Addressable::URI.parse(uri).normalize.to_s
2178 rescue InvalidURIError, TypeError
2179 return false
2180 end
2181 end
2182 return self.normalize.to_s == uri_string
2183 end
2184
2185 ##
2186 # Returns <code>true</code> if the URI objects are equal. This method
2187 # normalizes both URIs before doing the comparison.
2188 #
2189 # @param [Object] uri The URI to compare.
2190 #
2191 # @return [TrueClass, FalseClass]
2192 # <code>true</code> if the URIs are equivalent, <code>false</code>
2193 # otherwise.
2194 def ==(uri)
2195 return false unless uri.kind_of?(URI)
2196 return self.normalize.to_s == uri.normalize.to_s
2197 end
2198
2199 ##
2200 # Returns <code>true</code> if the URI objects are equal. This method
2201 # does NOT normalize either URI before doing the comparison.
2202 #
2203 # @param [Object] uri The URI to compare.
2204 #
2205 # @return [TrueClass, FalseClass]
2206 # <code>true</code> if the URIs are equivalent, <code>false</code>
2207 # otherwise.
2208 def eql?(uri)
2209 return false unless uri.kind_of?(URI)
2210 return self.to_s == uri.to_s
2211 end
2212
2213 ##
2214 # A hash value that will make a URI equivalent to its normalized
2215 # form.
2216 #
2217 # @return [Integer] A hash of the URI.
2218 def hash
2219 @hash ||= self.to_s.hash * -1
2220 end
2221
2222 ##
2223 # Clones the URI object.
2224 #
2225 # @return [Addressable::URI] The cloned URI.
2226 def dup
2227 duplicated_uri = self.class.new(
2228 :scheme => self.scheme ? self.scheme.dup : nil,
2229 :user => self.user ? self.user.dup : nil,
2230 :password => self.password ? self.password.dup : nil,
2231 :host => self.host ? self.host.dup : nil,
2232 :port => self.port,
2233 :path => self.path ? self.path.dup : nil,
2234 :query => self.query ? self.query.dup : nil,
2235 :fragment => self.fragment ? self.fragment.dup : nil
2236 )
2237 return duplicated_uri
2238 end
2239
2240 ##
2241 # Omits components from a URI.
2242 #
2243 # @param [Symbol] *components The components to be omitted.
2244 #
2245 # @return [Addressable::URI] The URI with components omitted.
2246 #
2247 # @example
2248 # uri = Addressable::URI.parse("http://example.com/path?query")
2249 # #=> #<Addressable::URI:0xcc5e7a URI:http://example.com/path?query>
2250 # uri.omit(:scheme, :authority)
2251 # #=> #<Addressable::URI:0xcc4d86 URI:/path?query>
2252 def omit(*components)
2253 invalid_components = components - [
2254 :scheme, :user, :password, :userinfo, :host, :port, :authority,
2255 :path, :query, :fragment
2256 ]
2257 unless invalid_components.empty?
2258 raise ArgumentError,
2259 "Invalid component names: #{invalid_components.inspect}."
2260 end
2261 duplicated_uri = self.dup
2262 duplicated_uri.defer_validation do
2263 components.each do |component|
2264 duplicated_uri.send((component.to_s + "=").to_sym, nil)
2265 end
2266 duplicated_uri.user = duplicated_uri.normalized_user
2267 end
2268 duplicated_uri
2269 end
2270
2271 ##
2272 # Destructive form of omit.
2273 #
2274 # @param [Symbol] *components The components to be omitted.
2275 #
2276 # @return [Addressable::URI] The URI with components omitted.
2277 #
2278 # @see Addressable::URI#omit
2279 def omit!(*components)
2280 replace_self(self.omit(*components))
2281 end
2282
2283 ##
2284 # Determines if the URI is an empty string.
2285 #
2286 # @return [TrueClass, FalseClass]
2287 # Returns <code>true</code> if empty, <code>false</code> otherwise.
2288 def empty?
2289 return self.to_s.empty?
2290 end
2291
2292 ##
2293 # Converts the URI to a <code>String</code>.
2294 #
2295 # @return [String] The URI's <code>String</code> representation.
2296 def to_s
2297 if self.scheme == nil && self.path != nil && !self.path.empty? &&
2298 self.path =~ NORMPATH
2299 raise InvalidURIError,
2300 "Cannot assemble URI string with ambiguous path: '#{self.path}'"
2301 end
2302 @uri_string ||= begin
2303 uri_string = String.new
2304 uri_string << "#{self.scheme}:" if self.scheme != nil
2305 uri_string << "//#{self.authority}" if self.authority != nil
2306 uri_string << self.path.to_s
2307 uri_string << "?#{self.query}" if self.query != nil
2308 uri_string << "##{self.fragment}" if self.fragment != nil
2309 uri_string.force_encoding(Encoding::UTF_8)
2310 uri_string
2311 end
2312 end
2313
2314 ##
2315 # URI's are glorified <code>Strings</code>. Allow implicit conversion.
2316 alias_method :to_str, :to_s
2317
2318 ##
2319 # Returns a Hash of the URI components.
2320 #
2321 # @return [Hash] The URI as a <code>Hash</code> of components.
2322 def to_hash
2323 return {
2324 :scheme => self.scheme,
2325 :user => self.user,
2326 :password => self.password,
2327 :host => self.host,
2328 :port => self.port,
2329 :path => self.path,
2330 :query => self.query,
2331 :fragment => self.fragment
2332 }
2333 end
2334
2335 ##
2336 # Returns a <code>String</code> representation of the URI object's state.
2337 #
2338 # @return [String] The URI object's state, as a <code>String</code>.
2339 def inspect
2340 sprintf("#<%s:%#0x URI:%s>", URI.to_s, self.object_id, self.to_s)
2341 end
2342
2343 ##
2344 # This method allows you to make several changes to a URI simultaneously,
2345 # which separately would cause validation errors, but in conjunction,
2346 # are valid. The URI will be revalidated as soon as the entire block has
2347 # been executed.
2348 #
2349 # @param [Proc] block
2350 # A set of operations to perform on a given URI.
2351 def defer_validation(&block)
2352 raise LocalJumpError, "No block given." unless block
2353 @validation_deferred = true
2354 block.call()
2355 @validation_deferred = false
2356 validate
2357 return nil
2358 end
2359
2360 protected
2361 SELF_REF = '.'
2362 PARENT = '..'
2363
2364 RULE_2A = /\/\.\/|\/\.$/
2365 RULE_2B_2C = /\/([^\/]*)\/\.\.\/|\/([^\/]*)\/\.\.$/
2366 RULE_2D = /^\.\.?\/?/
2367 RULE_PREFIXED_PARENT = /^\/\.\.?\/|^(\/\.\.?)+\/?$/
2368
2369 ##
2370 # Resolves paths to their simplest form.
2371 #
2372 # @param [String] path The path to normalize.
2373 #
2374 # @return [String] The normalized path.
2375 def self.normalize_path(path)
2376 # Section 5.2.4 of RFC 3986
2377
2378 return nil if path.nil?
2379 normalized_path = path.dup
2380 begin
2381 mod = nil
2382 mod ||= normalized_path.gsub!(RULE_2A, SLASH)
2383
2384 pair = normalized_path.match(RULE_2B_2C)
2385 parent, current = pair[1], pair[2] if pair
2386 if pair && ((parent != SELF_REF && parent != PARENT) ||
2387 (current != SELF_REF && current != PARENT))
2388 mod ||= normalized_path.gsub!(
2389 Regexp.new(
2390 "/#{Regexp.escape(parent.to_s)}/\\.\\./|" +
2391 "(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
2392 ), SLASH
2393 )
2394 end
2395
2396 mod ||= normalized_path.gsub!(RULE_2D, EMPTY_STR)
2397 # Non-standard, removes prefixed dotted segments from path.
2398 mod ||= normalized_path.gsub!(RULE_PREFIXED_PARENT, SLASH)
2399 end until mod.nil?
2400
2401 return normalized_path
2402 end
2403
2404 ##
2405 # Ensures that the URI is valid.
2406 def validate
2407 return if !!@validation_deferred
2408 if self.scheme != nil && self.ip_based? &&
2409 (self.host == nil || self.host.empty?) &&
2410 (self.path == nil || self.path.empty?)
2411 raise InvalidURIError,
2412 "Absolute URI missing hierarchical segment: '#{self.to_s}'"
2413 end
2414 if self.host == nil
2415 if self.port != nil ||
2416 self.user != nil ||
2417 self.password != nil
2418 raise InvalidURIError, "Hostname not supplied: '#{self.to_s}'"
2419 end
2420 end
2421 if self.path != nil && !self.path.empty? && self.path[0..0] != SLASH &&
2422 self.authority != nil
2423 raise InvalidURIError,
2424 "Cannot have a relative path with an authority set: '#{self.to_s}'"
2425 end
2426 if self.path != nil && !self.path.empty? &&
2427 self.path[0..1] == SLASH + SLASH && self.authority == nil
2428 raise InvalidURIError,
2429 "Cannot have a path with two leading slashes " +
2430 "without an authority set: '#{self.to_s}'"
2431 end
2432 unreserved = CharacterClasses::UNRESERVED
2433 sub_delims = CharacterClasses::SUB_DELIMS
2434 if !self.host.nil? && (self.host =~ /[<>{}\/\\\?\#\@"[[:space:]]]/ ||
2435 (self.host[/^\[(.*)\]$/, 1] != nil && self.host[/^\[(.*)\]$/, 1] !~
2436 Regexp.new("^[#{unreserved}#{sub_delims}:]*$")))
2437 raise InvalidURIError, "Invalid character in host: '#{self.host.to_s}'"
2438 end
2439 return nil
2440 end
2441
2442 ##
2443 # Replaces the internal state of self with the specified URI's state.
2444 # Used in destructive operations to avoid massive code repetition.
2445 #
2446 # @param [Addressable::URI] uri The URI to replace <code>self</code> with.
2447 #
2448 # @return [Addressable::URI] <code>self</code>.
2449 def replace_self(uri)
2450 # Reset dependent values
2451 instance_variables.each do |var|
2452 if instance_variable_defined?(var) && var != :@validation_deferred
2453 remove_instance_variable(var)
2454 end
2455 end
2456
2457 @scheme = uri.scheme
2458 @user = uri.user
2459 @password = uri.password
2460 @host = uri.host
2461 @port = uri.port
2462 @path = uri.path
2463 @query = uri.query
2464 @fragment = uri.fragment
2465 return self
2466 end
2467
2468 ##
2469 # Splits path string with "/" (slash).
2470 # It is considered that there is empty string after last slash when
2471 # path ends with slash.
2472 #
2473 # @param [String] path The path to split.
2474 #
2475 # @return [Array<String>] An array of parts of path.
2476 def split_path(path)
2477 splitted = path.split(SLASH)
2478 splitted << EMPTY_STR if path.end_with? SLASH
2479 splitted
2480 end
2481
2482 ##
2483 # Resets composite values for the entire URI
2484 #
2485 # @api private
2486 def remove_composite_values
2487 remove_instance_variable(:@uri_string) if defined?(@uri_string)
2488 remove_instance_variable(:@hash) if defined?(@hash)
2489 end
2490 end
2491 end
0 # encoding:utf-8
1 #--
2 # Copyright (C) Bob Aman
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #++
16
17
18 # Used to prevent the class/module from being loaded more than once
19 if !defined?(Addressable::VERSION)
20 module Addressable
21 module VERSION
22 MAJOR = 2
23 MINOR = 5
24 TINY = 2
25
26 STRING = [MAJOR, MINOR, TINY].join('.')
27 end
28 end
29 end
0 require 'addressable/uri'
1 require 'addressable/template'
0 ---
1 country:
2 - 한국
3 - 香港
4 - 澳門
5 - 新加坡
6 - 台灣
7 - 台湾
8 - 中國
9 - 中国
10 - გე
11 - ไทย
12 - ලංකා
13 - ഭാരതം
14 - ಭಾರತ
15 - భారత్
16 - சிங்கப்பூர்
17 - இலங்கை
18 - இந்தியா
19 - ଭାରତ
20 - ભારત
21 - ਭਾਰਤ
22 - ভাৰত
23 - ভারত
24 - বাংলা
25 - भारोत
26 - भारतम्
27 - भारत
28 - ڀارت
29 - پاکستان
30 - موريتانيا
31 - مليسيا
32 - مصر
33 - قطر
34 - فلسطين
35 - عمان
36 - عراق
37 - سورية
38 - سودان
39 - تونس
40 - بھارت
41 - بارت
42 - ایران
43 - امارات
44 - المغرب
45 - السعودية
46 - الجزائر
47 - الاردن
48 - հայ
49 - қаз
50 - укр
51 - срб
52 - рф
53 - мон
54 - мкд
55 - ею
56 - бел
57 - бг
58 - ελ
59 - zw
60 - zm
61 - za
62 - yt
63 - ye
64 - ws
65 - wf
66 - vu
67 - vn
68 - vi
69 - vg
70 - ve
71 - vc
72 - va
73 - uz
74 - uy
75 - us
76 - um
77 - uk
78 - ug
79 - ua
80 - tz
81 - tw
82 - tv
83 - tt
84 - tr
85 - tp
86 - to
87 - tn
88 - tm
89 - tl
90 - tk
91 - tj
92 - th
93 - tg
94 - tf
95 - td
96 - tc
97 - sz
98 - sy
99 - sx
100 - sv
101 - su
102 - st
103 - ss
104 - sr
105 - so
106 - sn
107 - sm
108 - sl
109 - sk
110 - sj
111 - si
112 - sh
113 - sg
114 - se
115 - sd
116 - sc
117 - sb
118 - sa
119 - rw
120 - ru
121 - rs
122 - ro
123 - re
124 - qa
125 - py
126 - pw
127 - pt
128 - ps
129 - pr
130 - pn
131 - pm
132 - pl
133 - pk
134 - ph
135 - pg
136 - pf
137 - pe
138 - pa
139 - om
140 - nz
141 - nu
142 - nr
143 - np
144 - 'no'
145 - nl
146 - ni
147 - ng
148 - nf
149 - ne
150 - nc
151 - na
152 - mz
153 - my
154 - mx
155 - mw
156 - mv
157 - mu
158 - mt
159 - ms
160 - mr
161 - mq
162 - mp
163 - mo
164 - mn
165 - mm
166 - ml
167 - mk
168 - mh
169 - mg
170 - mf
171 - me
172 - md
173 - mc
174 - ma
175 - ly
176 - lv
177 - lu
178 - lt
179 - ls
180 - lr
181 - lk
182 - li
183 - lc
184 - lb
185 - la
186 - kz
187 - ky
188 - kw
189 - kr
190 - kp
191 - kn
192 - km
193 - ki
194 - kh
195 - kg
196 - ke
197 - jp
198 - jo
199 - jm
200 - je
201 - it
202 - is
203 - ir
204 - iq
205 - io
206 - in
207 - im
208 - il
209 - ie
210 - id
211 - hu
212 - ht
213 - hr
214 - hn
215 - hm
216 - hk
217 - gy
218 - gw
219 - gu
220 - gt
221 - gs
222 - gr
223 - gq
224 - gp
225 - gn
226 - gm
227 - gl
228 - gi
229 - gh
230 - gg
231 - gf
232 - ge
233 - gd
234 - gb
235 - ga
236 - fr
237 - fo
238 - fm
239 - fk
240 - fj
241 - fi
242 - eu
243 - et
244 - es
245 - er
246 - eh
247 - eg
248 - ee
249 - ec
250 - dz
251 - do
252 - dm
253 - dk
254 - dj
255 - de
256 - cz
257 - cy
258 - cx
259 - cw
260 - cv
261 - cu
262 - cr
263 - co
264 - cn
265 - cm
266 - cl
267 - ck
268 - ci
269 - ch
270 - cg
271 - cf
272 - cd
273 - cc
274 - ca
275 - bz
276 - by
277 - bw
278 - bv
279 - bt
280 - bs
281 - br
282 - bq
283 - bo
284 - bn
285 - bm
286 - bl
287 - bj
288 - bi
289 - bh
290 - bg
291 - bf
292 - be
293 - bd
294 - bb
295 - ba
296 - az
297 - ax
298 - aw
299 - au
300 - at
301 - as
302 - ar
303 - aq
304 - ao
305 - an
306 - am
307 - al
308 - ai
309 - ag
310 - af
311 - ae
312 - ad
313 - ac
314 generic:
315 - 삼성
316 - 닷컴
317 - 닷넷
318 - 香格里拉
319 - 餐厅
320 - 食品
321 - 飞利浦
322 - 電訊盈科
323 - 集团
324 - 通販
325 - 购物
326 - 谷歌
327 - 诺基亚
328 - 联通
329 - 网络
330 - 网站
331 - 网店
332 - 网址
333 - 组织机构
334 - 移动
335 - 珠宝
336 - 点看
337 - 游戏
338 - 淡马锡
339 - 机构
340 - 書籍
341 - 时尚
342 - 新闻
343 - 政府
344 - 政务
345 - 手表
346 - 手机
347 - 我爱你
348 - 慈善
349 - 微博
350 - 广东
351 - 工行
352 - 家電
353 - 娱乐
354 - 天主教
355 - 大拿
356 - 大众汽车
357 - 在线
358 - 嘉里大酒店
359 - 嘉里
360 - 商标
361 - 商店
362 - 商城
363 - 公益
364 - 公司
365 - 八卦
366 - 健康
367 - 信息
368 - 佛山
369 - 企业
370 - 中文网
371 - 中信
372 - 世界
373 - ポイント
374 - ファッション
375 - セール
376 - ストア
377 - コム
378 - グーグル
379 - クラウド
380 - みんな
381 - คอม
382 - संगठन
383 - नेट
384 - कॉम
385 - همراه
386 - موقع
387 - موبايلي
388 - كوم
389 - كاثوليك
390 - عرب
391 - شبكة
392 - بيتك
393 - بازار
394 - العليان
395 - ارامكو
396 - اتصالات
397 - ابوظبي
398 - קום
399 - сайт
400 - рус
401 - орг
402 - онлайн
403 - москва
404 - ком
405 - католик
406 - дети
407 - zuerich
408 - zone
409 - zippo
410 - zip
411 - zero
412 - zara
413 - zappos
414 - yun
415 - youtube
416 - you
417 - yokohama
418 - yoga
419 - yodobashi
420 - yandex
421 - yamaxun
422 - yahoo
423 - yachts
424 - xyz
425 - xxx
426 - xperia
427 - xin
428 - xihuan
429 - xfinity
430 - xerox
431 - xbox
432 - wtf
433 - wtc
434 - wow
435 - world
436 - works
437 - work
438 - woodside
439 - wolterskluwer
440 - wme
441 - winners
442 - wine
443 - windows
444 - win
445 - williamhill
446 - wiki
447 - wien
448 - whoswho
449 - weir
450 - weibo
451 - wedding
452 - wed
453 - website
454 - weber
455 - webcam
456 - weatherchannel
457 - weather
458 - watches
459 - watch
460 - warman
461 - wanggou
462 - wang
463 - walter
464 - walmart
465 - wales
466 - vuelos
467 - voyage
468 - voto
469 - voting
470 - vote
471 - volvo
472 - volkswagen
473 - vodka
474 - vlaanderen
475 - vivo
476 - viva
477 - vistaprint
478 - vista
479 - vision
480 - visa
481 - virgin
482 - vip
483 - vin
484 - villas
485 - viking
486 - vig
487 - video
488 - viajes
489 - vet
490 - versicherung
491 - vermögensberatung
492 - vermögensberater
493 - verisign
494 - ventures
495 - vegas
496 - vanguard
497 - vana
498 - vacations
499 - ups
500 - uol
501 - uno
502 - university
503 - unicom
504 - uconnect
505 - ubs
506 - ubank
507 - tvs
508 - tushu
509 - tunes
510 - tui
511 - tube
512 - trv
513 - trust
514 - travelersinsurance
515 - travelers
516 - travelchannel
517 - travel
518 - training
519 - trading
520 - trade
521 - toys
522 - toyota
523 - town
524 - tours
525 - total
526 - toshiba
527 - toray
528 - top
529 - tools
530 - tokyo
531 - today
532 - tmall
533 - tkmaxx
534 - tjx
535 - tjmaxx
536 - tirol
537 - tires
538 - tips
539 - tiffany
540 - tienda
541 - tickets
542 - tiaa
543 - theatre
544 - theater
545 - thd
546 - teva
547 - tennis
548 - temasek
549 - telefonica
550 - telecity
551 - tel
552 - technology
553 - tech
554 - team
555 - tdk
556 - tci
557 - taxi
558 - tax
559 - tattoo
560 - tatar
561 - tatamotors
562 - target
563 - taobao
564 - talk
565 - taipei
566 - tab
567 - systems
568 - symantec
569 - sydney
570 - swiss
571 - swiftcover
572 - swatch
573 - suzuki
574 - surgery
575 - surf
576 - support
577 - supply
578 - supplies
579 - sucks
580 - style
581 - study
582 - studio
583 - stream
584 - store
585 - storage
586 - stockholm
587 - stcgroup
588 - stc
589 - statoil
590 - statefarm
591 - statebank
592 - starhub
593 - star
594 - staples
595 - stada
596 - srt
597 - srl
598 - spreadbetting
599 - spot
600 - spiegel
601 - space
602 - soy
603 - sony
604 - song
605 - solutions
606 - solar
607 - sohu
608 - software
609 - softbank
610 - social
611 - soccer
612 - sncf
613 - smile
614 - smart
615 - sling
616 - skype
617 - sky
618 - skin
619 - ski
620 - site
621 - singles
622 - sina
623 - silk
624 - shriram
625 - showtime
626 - show
627 - shouji
628 - shopping
629 - shop
630 - shoes
631 - shiksha
632 - shia
633 - shell
634 - shaw
635 - sharp
636 - shangrila
637 - sfr
638 - sexy
639 - sex
640 - sew
641 - seven
642 - ses
643 - services
644 - sener
645 - select
646 - seek
647 - security
648 - secure
649 - seat
650 - search
651 - scot
652 - scor
653 - scjohnson
654 - science
655 - schwarz
656 - schule
657 - school
658 - scholarships
659 - schmidt
660 - schaeffler
661 - scb
662 - sca
663 - sbs
664 - sbi
665 - saxo
666 - save
667 - sas
668 - sarl
669 - sapo
670 - sap
671 - sanofi
672 - sandvikcoromant
673 - sandvik
674 - samsung
675 - samsclub
676 - salon
677 - sale
678 - sakura
679 - safety
680 - safe
681 - saarland
682 - ryukyu
683 - rwe
684 - run
685 - ruhr
686 - rugby
687 - rsvp
688 - room
689 - rogers
690 - rodeo
691 - rocks
692 - rocher
693 - rmit
694 - rip
695 - rio
696 - ril
697 - rightathome
698 - ricoh
699 - richardli
700 - rich
701 - rexroth
702 - reviews
703 - review
704 - restaurant
705 - rest
706 - republican
707 - report
708 - repair
709 - rentals
710 - rent
711 - ren
712 - reliance
713 - reit
714 - reisen
715 - reise
716 - rehab
717 - redumbrella
718 - redstone
719 - red
720 - recipes
721 - realty
722 - realtor
723 - realestate
724 - read
725 - raid
726 - radio
727 - racing
728 - qvc
729 - quest
730 - quebec
731 - qpon
732 - pwc
733 - pub
734 - prudential
735 - pru
736 - protection
737 - property
738 - properties
739 - promo
740 - progressive
741 - prof
742 - productions
743 - prod
744 - pro
745 - prime
746 - press
747 - praxi
748 - pramerica
749 - post
750 - porn
751 - politie
752 - poker
753 - pohl
754 - pnc
755 - plus
756 - plumbing
757 - playstation
758 - play
759 - place
760 - pizza
761 - pioneer
762 - pink
763 - ping
764 - pin
765 - pid
766 - pictures
767 - pictet
768 - pics
769 - piaget
770 - physio
771 - photos
772 - photography
773 - photo
774 - phone
775 - philips
776 - phd
777 - pharmacy
778 - pfizer
779 - pet
780 - pccw
781 - pay
782 - passagens
783 - party
784 - parts
785 - partners
786 - pars
787 - paris
788 - panerai
789 - panasonic
790 - pamperedchef
791 - page
792 - ovh
793 - ott
794 - otsuka
795 - osaka
796 - origins
797 - orientexpress
798 - organic
799 - org
800 - orange
801 - oracle
802 - open
803 - ooo
804 - onyourside
805 - online
806 - onl
807 - ong
808 - one
809 - omega
810 - ollo
811 - oldnavy
812 - olayangroup
813 - olayan
814 - okinawa
815 - office
816 - 'off'
817 - observer
818 - obi
819 - nyc
820 - ntt
821 - nrw
822 - nra
823 - nowtv
824 - nowruz
825 - now
826 - norton
827 - northwesternmutual
828 - nokia
829 - nissay
830 - nissan
831 - ninja
832 - nikon
833 - nike
834 - nico
835 - nhk
836 - ngo
837 - nfl
838 - nexus
839 - nextdirect
840 - next
841 - news
842 - newholland
843 - new
844 - neustar
845 - network
846 - netflix
847 - netbank
848 - net
849 - nec
850 - nba
851 - navy
852 - natura
853 - nationwide
854 - name
855 - nagoya
856 - nadex
857 - nab
858 - mutuelle
859 - mutual
860 - museum
861 - mtr
862 - mtpc
863 - mtn
864 - msd
865 - movistar
866 - movie
867 - mov
868 - motorcycles
869 - moto
870 - moscow
871 - mortgage
872 - mormon
873 - mopar
874 - montblanc
875 - monster
876 - money
877 - monash
878 - mom
879 - moi
880 - moe
881 - moda
882 - mobily
883 - mobile
884 - mobi
885 - mma
886 - mls
887 - mlb
888 - mitsubishi
889 - mit
890 - mint
891 - mini
892 - mil
893 - microsoft
894 - miami
895 - metlife
896 - merckmsd
897 - meo
898 - menu
899 - men
900 - memorial
901 - meme
902 - melbourne
903 - meet
904 - media
905 - med
906 - mckinsey
907 - mcdonalds
908 - mcd
909 - mba
910 - mattel
911 - maserati
912 - marshalls
913 - marriott
914 - markets
915 - marketing
916 - market
917 - map
918 - mango
919 - management
920 - man
921 - makeup
922 - maison
923 - maif
924 - madrid
925 - macys
926 - luxury
927 - luxe
928 - lupin
929 - lundbeck
930 - ltda
931 - ltd
932 - lplfinancial
933 - lpl
934 - love
935 - lotto
936 - lotte
937 - london
938 - lol
939 - loft
940 - locus
941 - locker
942 - loans
943 - loan
944 - lixil
945 - living
946 - live
947 - lipsy
948 - link
949 - linde
950 - lincoln
951 - limo
952 - limited
953 - lilly
954 - like
955 - lighting
956 - lifestyle
957 - lifeinsurance
958 - life
959 - lidl
960 - liaison
961 - lgbt
962 - lexus
963 - lego
964 - legal
965 - lefrak
966 - leclerc
967 - lease
968 - lds
969 - lawyer
970 - law
971 - latrobe
972 - latino
973 - lat
974 - lasalle
975 - lanxess
976 - landrover
977 - land
978 - lancome
979 - lancia
980 - lancaster
981 - lamer
982 - lamborghini
983 - ladbrokes
984 - lacaixa
985 - kyoto
986 - kuokgroup
987 - kred
988 - krd
989 - kpn
990 - kpmg
991 - kosher
992 - komatsu
993 - koeln
994 - kiwi
995 - kitchen
996 - kindle
997 - kinder
998 - kim
999 - kia
1000 - kfh
1001 - kerryproperties
1002 - kerrylogistics
1003 - kerryhotels
1004 - kddi
1005 - kaufen
1006 - juniper
1007 - juegos
1008 - jprs
1009 - jpmorgan
1010 - joy
1011 - jot
1012 - joburg
1013 - jobs
1014 - jnj
1015 - jmp
1016 - jll
1017 - jlc
1018 - jio
1019 - jewelry
1020 - jetzt
1021 - jeep
1022 - jcp
1023 - jcb
1024 - java
1025 - jaguar
1026 - iwc
1027 - iveco
1028 - itv
1029 - itau
1030 - istanbul
1031 - ist
1032 - ismaili
1033 - iselect
1034 - irish
1035 - ipiranga
1036 - investments
1037 - intuit
1038 - international
1039 - intel
1040 - int
1041 - insure
1042 - insurance
1043 - institute
1044 - ink
1045 - ing
1046 - info
1047 - infiniti
1048 - industries
1049 - immobilien
1050 - immo
1051 - imdb
1052 - imamat
1053 - ikano
1054 - iinet
1055 - ifm
1056 - ieee
1057 - icu
1058 - ice
1059 - icbc
1060 - ibm
1061 - hyundai
1062 - hyatt
1063 - hughes
1064 - htc
1065 - hsbc
1066 - how
1067 - house
1068 - hotmail
1069 - hotels
1070 - hoteles
1071 - hot
1072 - hosting
1073 - host
1074 - hospital
1075 - horse
1076 - honeywell
1077 - honda
1078 - homesense
1079 - homes
1080 - homegoods
1081 - homedepot
1082 - holiday
1083 - holdings
1084 - hockey
1085 - hkt
1086 - hiv
1087 - hitachi
1088 - hisamitsu
1089 - hiphop
1090 - hgtv
1091 - hermes
1092 - here
1093 - helsinki
1094 - help
1095 - healthcare
1096 - health
1097 - hdfcbank
1098 - hdfc
1099 - hbo
1100 - haus
1101 - hangout
1102 - hamburg
1103 - hair
1104 - guru
1105 - guitars
1106 - guide
1107 - guge
1108 - gucci
1109 - guardian
1110 - group
1111 - grocery
1112 - gripe
1113 - green
1114 - gratis
1115 - graphics
1116 - grainger
1117 - gov
1118 - got
1119 - gop
1120 - google
1121 - goog
1122 - goodyear
1123 - goodhands
1124 - goo
1125 - golf
1126 - goldpoint
1127 - gold
1128 - godaddy
1129 - gmx
1130 - gmo
1131 - gmbh
1132 - gmail
1133 - globo
1134 - global
1135 - gle
1136 - glass
1137 - glade
1138 - giving
1139 - gives
1140 - gifts
1141 - gift
1142 - ggee
1143 - george
1144 - genting
1145 - gent
1146 - gea
1147 - gdn
1148 - gbiz
1149 - garden
1150 - gap
1151 - games
1152 - game
1153 - gallup
1154 - gallo
1155 - gallery
1156 - gal
1157 - fyi
1158 - futbol
1159 - furniture
1160 - fund
1161 - fun
1162 - fujixerox
1163 - fujitsu
1164 - ftr
1165 - frontier
1166 - frontdoor
1167 - frogans
1168 - frl
1169 - fresenius
1170 - free
1171 - fox
1172 - foundation
1173 - forum
1174 - forsale
1175 - forex
1176 - ford
1177 - football
1178 - foodnetwork
1179 - food
1180 - foo
1181 - fly
1182 - flsmidth
1183 - flowers
1184 - florist
1185 - flir
1186 - flights
1187 - flickr
1188 - fitness
1189 - fit
1190 - fishing
1191 - fish
1192 - firmdale
1193 - firestone
1194 - fire
1195 - financial
1196 - finance
1197 - final
1198 - film
1199 - fido
1200 - fidelity
1201 - fiat
1202 - ferrero
1203 - ferrari
1204 - feedback
1205 - fedex
1206 - fast
1207 - fashion
1208 - farmers
1209 - farm
1210 - fans
1211 - fan
1212 - family
1213 - faith
1214 - fairwinds
1215 - fail
1216 - fage
1217 - extraspace
1218 - express
1219 - exposed
1220 - expert
1221 - exchange
1222 - everbank
1223 - events
1224 - eus
1225 - eurovision
1226 - etisalat
1227 - esurance
1228 - estate
1229 - esq
1230 - erni
1231 - ericsson
1232 - equipment
1233 - epson
1234 - epost
1235 - enterprises
1236 - engineering
1237 - engineer
1238 - energy
1239 - emerck
1240 - email
1241 - education
1242 - edu
1243 - edeka
1244 - eco
1245 - eat
1246 - earth
1247 - dvr
1248 - dvag
1249 - durban
1250 - dupont
1251 - duns
1252 - dunlop
1253 - duck
1254 - dubai
1255 - dtv
1256 - drive
1257 - download
1258 - dot
1259 - doosan
1260 - domains
1261 - doha
1262 - dog
1263 - dodge
1264 - doctor
1265 - docs
1266 - dnp
1267 - diy
1268 - dish
1269 - discover
1270 - discount
1271 - directory
1272 - direct
1273 - digital
1274 - diet
1275 - diamonds
1276 - dhl
1277 - dev
1278 - design
1279 - desi
1280 - dentist
1281 - dental
1282 - democrat
1283 - delta
1284 - deloitte
1285 - dell
1286 - delivery
1287 - degree
1288 - deals
1289 - dealer
1290 - deal
1291 - dds
1292 - dclk
1293 - day
1294 - datsun
1295 - dating
1296 - date
1297 - data
1298 - dance
1299 - dad
1300 - dabur
1301 - cyou
1302 - cymru
1303 - cuisinella
1304 - csc
1305 - cruises
1306 - cruise
1307 - crs
1308 - crown
1309 - cricket
1310 - creditunion
1311 - creditcard
1312 - credit
1313 - courses
1314 - coupons
1315 - coupon
1316 - country
1317 - corsica
1318 - coop
1319 - cool
1320 - cookingchannel
1321 - cooking
1322 - contractors
1323 - contact
1324 - consulting
1325 - construction
1326 - condos
1327 - comsec
1328 - computer
1329 - compare
1330 - company
1331 - community
1332 - commbank
1333 - comcast
1334 - com
1335 - cologne
1336 - college
1337 - coffee
1338 - codes
1339 - coach
1340 - clubmed
1341 - club
1342 - cloud
1343 - clothing
1344 - clinique
1345 - clinic
1346 - click
1347 - cleaning
1348 - claims
1349 - cityeats
1350 - city
1351 - citic
1352 - citi
1353 - citadel
1354 - cisco
1355 - circle
1356 - cipriani
1357 - church
1358 - chrysler
1359 - chrome
1360 - christmas
1361 - chloe
1362 - chintai
1363 - cheap
1364 - chat
1365 - chase
1366 - channel
1367 - chanel
1368 - cfd
1369 - cfa
1370 - cern
1371 - ceo
1372 - center
1373 - ceb
1374 - cbs
1375 - cbre
1376 - cbn
1377 - cba
1378 - catholic
1379 - catering
1380 - cat
1381 - casino
1382 - cash
1383 - caseih
1384 - case
1385 - casa
1386 - cartier
1387 - cars
1388 - careers
1389 - career
1390 - care
1391 - cards
1392 - caravan
1393 - car
1394 - capitalone
1395 - capital
1396 - capetown
1397 - canon
1398 - cancerresearch
1399 - camp
1400 - camera
1401 - cam
1402 - calvinklein
1403 - call
1404 - cal
1405 - cafe
1406 - cab
1407 - bzh
1408 - buzz
1409 - buy
1410 - business
1411 - builders
1412 - build
1413 - bugatti
1414 - budapest
1415 - brussels
1416 - brother
1417 - broker
1418 - broadway
1419 - bridgestone
1420 - bradesco
1421 - box
1422 - boutique
1423 - bot
1424 - boston
1425 - bostik
1426 - bosch
1427 - boots
1428 - booking
1429 - book
1430 - boo
1431 - bond
1432 - bom
1433 - bofa
1434 - boehringer
1435 - boats
1436 - bnpparibas
1437 - bnl
1438 - bmw
1439 - bms
1440 - blue
1441 - bloomberg
1442 - blog
1443 - blockbuster
1444 - blanco
1445 - blackfriday
1446 - black
1447 - biz
1448 - bio
1449 - bingo
1450 - bing
1451 - bike
1452 - bid
1453 - bible
1454 - bharti
1455 - bet
1456 - bestbuy
1457 - best
1458 - berlin
1459 - bentley
1460 - beer
1461 - beauty
1462 - beats
1463 - bcn
1464 - bcg
1465 - bbva
1466 - bbt
1467 - bbc
1468 - bayern
1469 - bauhaus
1470 - basketball
1471 - baseball
1472 - bargains
1473 - barefoot
1474 - barclays
1475 - barclaycard
1476 - barcelona
1477 - bar
1478 - bank
1479 - band
1480 - bananarepublic
1481 - banamex
1482 - baidu
1483 - baby
1484 - azure
1485 - axa
1486 - aws
1487 - avianca
1488 - autos
1489 - auto
1490 - author
1491 - auspost
1492 - audio
1493 - audible
1494 - audi
1495 - auction
1496 - attorney
1497 - athleta
1498 - associates
1499 - asia
1500 - asda
1501 - arte
1502 - art
1503 - arpa
1504 - army
1505 - archi
1506 - aramco
1507 - arab
1508 - aquarelle
1509 - apple
1510 - app
1511 - apartments
1512 - aol
1513 - anz
1514 - anquan
1515 - android
1516 - analytics
1517 - amsterdam
1518 - amica
1519 - amfam
1520 - amex
1521 - americanfamily
1522 - americanexpress
1523 - alstom
1524 - alsace
1525 - ally
1526 - allstate
1527 - allfinanz
1528 - alipay
1529 - alibaba
1530 - alfaromeo
1531 - akdn
1532 - airtel
1533 - airforce
1534 - airbus
1535 - aigo
1536 - aig
1537 - agency
1538 - agakhan
1539 - africa
1540 - afl
1541 - afamilycompany
1542 - aetna
1543 - aero
1544 - aeg
1545 - adult
1546 - ads
1547 - adac
1548 - actor
1549 - active
1550 - aco
1551 - accountants
1552 - accountant
1553 - accenture
1554 - academy
1555 - abudhabi
1556 - abogado
1557 - able
1558 - abc
1559 - abbvie
1560 - abbott
1561 - abb
1562 - abarth
1563 - aarp
1564 - aaa
1565 - onion
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/chain/base"
2
3 module Delayer::Deferred::Chain
4 class Await < Base
5 def initialize(worker:, deferred:)
6 super()
7 @worker, @awaiting_deferred = worker, deferred
8 deferred.add_awaited(self)
9 end
10
11 def activate(response)
12 change_sequence(:activate)
13 @worker.give_response(response, @awaiting_deferred)
14 # TODO: 即座にspoilさせてよさそう
15 ensure
16 change_sequence(:complete)
17 end
18
19 def graph_child(output:)
20 output << graph_mynode
21 if has_child?
22 @child.graph_child(output: output)
23 @awaiting_deferred.graph_child(output: output)
24 output << "#{__id__} -> #{@child.__id__}"
25 end
26 nil
27 end
28
29 def node_name
30 "Await"
31 end
32
33 def graph_shape
34 'circle'.freeze
35 end
36
37 def graph_mynode
38 label = "#{node_name}\n(#{sequence.name})"
39 "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]"
40 end
41 end
42 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/deferredable/chainable"
2 require "delayer/deferred/deferredable/node_sequence"
3
4 module Delayer::Deferred::Chain
5 class Base
6 include Delayer::Deferred::Deferredable::NodeSequence
7 include Delayer::Deferred::Deferredable::Chainable
8
9 def initialize(&proc)
10 fail Error, "Delayer::Deferred::Chain can't create instance." if self.class == Delayer::Deferred::Chain::Base
11 @proc = proc
12 end
13
14 def activate(response)
15 change_sequence(:activate)
16 if evaluate?(response)
17 @proc.(response.value)
18 else
19 response
20 end
21 ensure
22 change_sequence(:complete)
23 end
24
25 def inspect
26 "#<#{self.class} seq:#{sequence.name} child:#{has_child?}>"
27 end
28
29 def node_name
30 @proc.source_location.join(':'.freeze)
31 end
32 end
33 end
34
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/chain/base"
2
3 module Delayer::Deferred::Chain
4 class Next < Base
5 def evaluate?(response)
6 response.ok?
7 end
8
9 private
10
11 def graph_shape
12 'box'.freeze
13 end
14 end
15 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/chain/base"
2
3 module Delayer::Deferred::Chain
4 class Trap < Base
5 def evaluate?(response)
6 response.ng?
7 end
8
9 private
10
11 def graph_shape
12 'diamond'.freeze
13 end
14 end
15 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer::Deferred
3 module Chain; end
4 end
5
6 require "delayer/deferred/chain/await"
7 require "delayer/deferred/chain/base"
8 require "delayer/deferred/chain/next"
9 require "delayer/deferred/chain/trap"
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/promise"
2 require "delayer/deferred/chain"
3 require "delayer/deferred/deferredable"
4 require "delayer/deferred/worker"
5 require "delayer/deferred/version"
6
7 module Delayer::Deferred
8 Deferred = Promise
9 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer::Deferred::Deferredable
3 module Awaitable
4
5 # _self_ が終了して結果が出るまで呼び出し側のDeferredを停止し、 _self_ の結果を返す。
6 # 呼び出し側はDeferredブロック内でなければならないが、 _Deferred#next_ を使わずに
7 # 直接戻り値を得ることが出来る。
8 # _self_ が失敗した場合は、呼び出し側のDeferredの直近の _trap_ ブロックが呼ばれる。
9 def +@
10 response = Fiber.yield(Delayer::Deferred::Request::Await.new(self))
11 if response.ok?
12 response.value
13 else
14 Delayer::Deferred.fail(response.value)
15 end
16 end
17
18 def enter_await
19 change_sequence(:await)
20 end
21
22 def exit_await
23 change_sequence(:resume)
24 end
25 end
26 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/deferredable/awaitable"
2 require "delayer/deferred/deferredable/graph"
3 require "delayer/deferred/deferredable/node_sequence"
4
5 module Delayer::Deferred::Deferredable
6 module Chainable
7 include Awaitable
8 include Graph
9 include NodeSequence
10
11 attr_reader :child
12
13 # このDeferredが成功した場合の処理を追加する。
14 # 新しいDeferredのインスタンスを返す。
15 # このメソッドはスレッドセーフです。
16 # TODO: procが空のとき例外を発生させる
17 def next(&proc)
18 add_child(Delayer::Deferred::Chain::Next.new(&proc))
19 end
20 alias deferred next
21
22 # このDeferredが失敗した場合の処理を追加する。
23 # 新しいDeferredのインスタンスを返す。
24 # このメソッドはスレッドセーフです。
25 # TODO: procが空のとき例外を発生させる
26 def trap(&proc)
27 add_child(Delayer::Deferred::Chain::Trap.new(&proc))
28 end
29 alias error trap
30
31 # この一連のDeferredをこれ以上実行しない。
32 # このメソッドはスレッドセーフです。
33 def cancel
34 change_sequence(:genocide) unless spoiled?
35 end
36
37 def has_child?
38 child ? true : false
39 end
40
41 # 子を追加する。
42 # _Delayer::Deferred::Chainable_ を直接指定できる。通常外部から呼ぶときは _next_ か _trap_ メソッドを使うこと。
43 # このメソッドはスレッドセーフです。
44 # ==== Args
45 # [chainable] 子となるDeferred
46 # ==== Return
47 # 必ず _chainable_ を返す
48 # ==== Raise
49 # [Delayer::Deferred::SequenceError]
50 # 既に子が存在している場合
51 def add_child(chainable)
52 change_sequence(:get_child) do
53 chainable.parent = self
54 @child = chainable
55 end
56 end
57
58 # 子が追加された時に一度だけコールバックするオブジェクトを登録する。
59 # observerと言っているが、実際には _Delayer::Deferred::Worker_ を渡して利用している。
60 # このメソッドはスレッドセーフです。
61 # ==== Args
62 # [observer] pushメソッドを備えているもの。引数に _@child_ の値が渡される
63 # ==== Return
64 # self
65 def add_child_observer(observer)
66 change_sequence(:gaze) do
67 @child_observer = observer
68 end
69 self
70 end
71
72 def awaited
73 @awaited ||= [].freeze
74 end
75
76 def has_awaited?
77 not awaited.empty?
78 end
79
80 def add_awaited(awaitable)
81 @awaited = [*awaited, awaitable].freeze
82 self
83 end
84
85 # activateメソッドを呼ぶDelayerジョブを登録する寸前に呼ばれる。
86 def reserve_activate
87 change_sequence(:reserve)
88 end
89
90 def enter_pass
91 change_sequence(:pass)
92 end
93
94 def exit_pass
95 change_sequence(:resume)
96 end
97
98 protected
99
100 # 親を再帰的に辿り、一番最初のノードを返す。
101 # 親が複数見つかった場合は、それらを返す。
102 def ancestor
103 if @parent
104 @parent.ancestor
105 else
106 self
107 end
108 end
109
110 # cancelとかデバッグ用のコールグラフを得るために親を登録しておく。
111 # add_childから呼ばれる。
112 def parent=(chainable)
113 @parent = chainable
114 end
115
116 private
117
118 def call_child_observer
119 if has_child? and defined?(@child_observer)
120 change_sequence(:called)
121 @child_observer.push(@child)
122 end
123 end
124
125 def on_sequence_changed(old_seq, flow, new_seq)
126 case new_seq
127 when NodeSequence::BURST_OUT
128 call_child_observer
129 when NodeSequence::GENOCIDE
130 @parent.cancel if defined?(@parent) and @parent
131 when NodeSequence::RESERVED_C, NodeSequence::RUN_C, NodeSequence::PASS_C, NodeSequence::AWAIT_C, NodeSequence::GRAFT_C
132 if !has_child?
133 notice "child: #{@child.inspect}"
134 raise Delayer::Deferred::SequenceError.new("Sequence changed `#{old_seq.name}' to `#{flow}', but it has no child")
135 end
136 end
137 end
138
139 # ノードの名前。サブクラスでオーバライドし、ノードが定義されたファイルの名前や行数などを入れておく。
140 def node_name
141 self.class.to_s
142 end
143
144 def graph_mynode
145 if defined?(@seq_logger)
146 label = "#{node_name}\n(#{@seq_logger.map(&:name).join('→')})"
147 else
148 label = "#{node_name}\n(#{sequence.name})"
149 end
150 "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]"
151 end
152
153 end
154 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer::Deferred::Deferredable
3 =begin rdoc
4 graphvizによってChainableなDeferredをDOT言語形式でダンプする機能を追加するmix-in。
5 いずれかのノードに対して _graph_ メソッドを呼ぶと、再帰的に親子を全て辿り、digraphの断片の文字列を得ることが出来る。
6
7 == 出力例
8
9 20892180 [shape=egg,label="#<Class:0x000000027da288>.Promise\n(reserved)"]
10 20892480 [shape=box,label="test/thread_test.rb:53\n(connected)"]
11 20891440 [shape=diamond,label="test/thread_test.rb:56\n(fresh)"]
12 20892480 -> 20891440
13 20892180 -> 20892480
14
15 =end
16 module Graph
17 # この一連のDeferredチェインの様子を、DOT言語フォーマットで出力する
18 # ==== Args
19 # [child_only:]
20 # _true_ なら、このノードとその子孫のみを描画する。
21 # _false_ なら、再帰的に親を遡り、そこから描画を開始する。
22 # [output:]
23 # このオブジェクトに、 _<<_ メソッドで内容が書かれる。
24 # 省略した場合は、戻り値が _String_ になる。
25 # ==== Return
26 # [String] DOT言語によるグラフ
27 # [output:] 引数 output: に指定されたオブジェクト
28 def graph(child_only: false, output: String.new)
29 if child_only
30 output << "digraph Deferred {\n".freeze
31 Enumerator.new{ |yielder|
32 graph_child(output: yielder)
33 }.lazy.each{|l|
34 output << "\t#{l}\n"
35 }
36 output << '}'.freeze
37 else
38 ancestor.graph(child_only: true, output: output)
39 end
40 end
41
42 # Graph.graph の結果を内容とする一時ファイルを作成して返す。
43 # ただし、ブロックを渡された場合は、一時ファイルを引数にそのブロックを一度だけ実行し、ブロックの戻り値をこのメソッドの戻り値とする。
44 # ==== Args
45 # [&block] 一時ファイルを利用する処理
46 # ==== Return
47 # [Tempfile] ブロックを指定しなかった場合。作成された一時ファイルオブジェクト
48 # [Object] ブロックが指定された場合。ブロックの実行結果。
49 def graph_save(permanent: false, &block)
50 if block
51 Tempfile.open{|tmp|
52 graph(output: tmp)
53 tmp.seek(0)
54 block.(tmp)
55 }
56 else
57 tmp = Tempfile.open
58 graph(output: tmp).tap{|t|t.seek(0)}
59 end
60 end
61
62 # 画像ファイルとしてグラフを書き出す。
63 # dotコマンドが使えないと失敗する。
64 # ==== Args
65 # [format:] 画像の拡張子
66 # ==== Return
67 # [String] 書き出したファイル名
68 def graph_draw(dir: '/tmp', format: 'svg'.freeze)
69 graph_save do |dotfile|
70 base = File.basename(dotfile.path)
71 dest = File.join(dir, "#{base}.#{format}")
72 system("dot -T#{format} #{dotfile.path} -o #{dest}")
73 dest
74 end
75 end
76
77 # このノードとその子全てのDeferredチェインの様子を、DOT言語フォーマットで出力する。
78 # Delayer::Deferred::Deferredable::Graph#graph の内部で利用されるため、将来このメソッドのインターフェイスは変更される可能性がある。
79 def graph_child(output:)
80 output << graph_mynode
81 if has_child?
82 @child.graph_child(output: output)
83 output << "#{__id__} -> #{@child.__id__}"
84 end
85 if has_awaited?
86 awaited.each do |awaitable|
87 if awaitable.is_a?(Delayer::Deferred::Deferredable::Chainable)
88 awaitable.ancestor.graph_child(output: output)
89 else
90 label = "#{awaitable.class}"
91 output << "#{awaitable.__id__} [shape=oval,label=#{label.inspect}]"
92 end
93 output << "#{awaitable.__id__} -> #{__id__} [label = \"await\", style = \"dotted\"]"
94 end
95 end
96 nil
97 end
98
99 private
100
101 # このノードを描画する時の形の名前を文字列で返す。
102 # 以下のページにあるような、graphvizで取り扱える形の中から選ぶこと。
103 # http://www.graphviz.org/doc/info/shapes.html
104 def graph_shape
105 'oval'.freeze
106 end
107
108 # このノードの形などをDOT言語の断片で返す。
109 # このメソッドをオーバライドすることで、描画されるノードの見た目を自由に変更することが出来る。
110 # ただし、簡単な変更だけなら別のメソッドをオーバライドするだけで可能なので、このmix-inの他のメソッドも参照すること。
111 def graph_mynode
112 label = "#{node_name}\n(#{sequence.name})"
113 "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]"
114 end
115
116 end
117 end
0 # -*- coding: utf-8 -*-
1 require 'delayer/deferred/error'
2
3 require 'thread'
4
5 module Delayer::Deferred::Deferredable
6 module NodeSequence
7 class Sequence
8 attr_reader :name
9
10 def initialize(name)
11 @name = name.to_sym
12 @map = {}
13 @exceptions = Hash.new(Delayer::Deferred::SequenceError)
14 end
15
16 def add(seq, flow = seq.name)
17 @map[flow] = seq
18 self
19 end
20
21 def exception(exc, flow)
22 @exceptions[flow] = exc
23 self
24 end
25
26 def pull(flow)
27 if @map.has_key?(flow.to_sym)
28 @map[flow.to_sym]
29 else
30 raise @exceptions[flow.to_sym], "Invalid sequence flow `#{name}' to `#{flow}'."
31 end
32 end
33
34 def inspect
35 "#<#{self.class}: #{name}>"
36 end
37 end
38
39 FRESH = Sequence.new(:fresh)
40 CONNECTED = Sequence.new(:connected) # 子がいる、未実行
41 RESERVED = Sequence.new(:reserved) # 実行キュー待ち
42 RESERVED_C= Sequence.new(:reserved) # 実行キュー待ち(子がいる)
43 RUN = Sequence.new(:run) # 実行中
44 RUN_C = Sequence.new(:run) # 実行中(子がいる)
45 PASS = Sequence.new(:pass) # パス中
46 PASS_C = Sequence.new(:pass) # パス中
47 AWAIT = Sequence.new(:await) # Await中
48 AWAIT_C = Sequence.new(:await) # Await中(子がいる)
49 GRAFT = Sequence.new(:graft) # 戻り値がAwaitableの時
50 GRAFT_C = Sequence.new(:graft) # 戻り値がAwaitableの時(子がいる)
51 CALL_CHILD= Sequence.new(:call_child) # 完了、子がいる
52 STOP = Sequence.new(:stop) # 完了、子なし
53 WAIT = Sequence.new(:wait) # 完了、オブザーバ登録済み
54 BURST_OUT = Sequence.new(:burst_out) # 完了、オブザーバ登録済み、子追加済み
55 ROTTEN = Sequence.new(:rotten).freeze # 終了
56 GENOCIDE = Sequence.new(:genocide).freeze# この地ではかつて大量虐殺があったという。
57
58 FRESH
59 .add(CONNECTED, :get_child)
60 .add(RESERVED, :reserve)
61 .add(GENOCIDE).freeze
62 CONNECTED
63 .add(RESERVED_C, :reserve)
64 .exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
65 .add(GENOCIDE).freeze
66 RESERVED
67 .add(RUN, :activate)
68 .add(RESERVED_C, :get_child)
69 .add(GENOCIDE).freeze
70 RESERVED_C
71 .add(RUN_C, :activate)
72 .exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
73 .add(GENOCIDE).freeze
74 RUN
75 .add(RUN_C, :get_child)
76 .add(PASS)
77 .add(AWAIT, :await)
78 .add(STOP, :complete)
79 .add(GENOCIDE).freeze
80 RUN_C
81 .add(PASS_C)
82 .add(AWAIT_C, :await)
83 .add(CALL_CHILD, :complete)
84 .exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
85 .add(GENOCIDE).freeze
86 PASS
87 .add(PASS_C, :get_child)
88 .add(RUN, :resume)
89 .add(GENOCIDE).freeze
90 PASS_C
91 .add(RUN_C, :resume)
92 .add(GENOCIDE).freeze
93 AWAIT
94 .add(RUN, :resume)
95 .add(AWAIT_C, :get_child)
96 .add(GENOCIDE).freeze
97 AWAIT_C
98 .add(RUN_C, :resume)
99 .exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
100 .add(GENOCIDE).freeze
101 CALL_CHILD
102 .add(GRAFT_C, :await)
103 .add(ROTTEN, :called)
104 .add(GENOCIDE).freeze
105 GRAFT
106 .add(STOP, :resume)
107 .add(GRAFT_C, :get_child)
108 .add(GENOCIDE).freeze
109 GRAFT_C
110 .add(CALL_CHILD, :resume)
111 .add(GENOCIDE).freeze
112 STOP
113 .add(GRAFT, :await)
114 .add(WAIT, :gaze)
115 .add(GENOCIDE).freeze
116 WAIT
117 .add(BURST_OUT, :get_child)
118 .add(GENOCIDE).freeze
119 BURST_OUT
120 .add(ROTTEN, :called)
121 .add(GENOCIDE).freeze
122
123 SEQUENCE_LOCK = Monitor.new
124
125 def sequence
126 @sequence ||= FRESH
127 end
128
129 # このメソッドはスレッドセーフです
130 def change_sequence(flow, &block)
131 SEQUENCE_LOCK.synchronize do
132 old_seq = sequence
133 new_seq = @sequence = sequence.pull(flow)
134 (@seq_logger ||= [old_seq]) << new_seq
135 if block
136 result = block.()
137 on_sequence_changed(old_seq, flow, new_seq)
138 result
139 else
140 on_sequence_changed(old_seq, flow, new_seq)
141 nil
142 end
143 end
144 end
145
146 def on_sequence_changed(old_seq, flow, new_seq)
147 end
148
149 def activated?
150 ![FRESH, CONNECTED, RUN, RUN_C].include?(sequence)
151 end
152
153 def spoiled?
154 sequence == ROTTEN || sequence == GENOCIDE
155 end
156 end
157 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/deferredable/chainable"
2 require "delayer/deferred/deferredable/node_sequence"
3 require "delayer/deferred/response"
4
5 module Delayer::Deferred::Deferredable
6 =begin rdoc
7 Promiseなど、親を持たず、自身がWorkerを作成できるもの。
8 =end
9 module Trigger
10 include NodeSequence
11 include Chainable
12
13 # Deferredを直ちに実行する。
14 # このメソッドはスレッドセーフです。
15 def call(value = nil)
16 execute(Delayer::Deferred::Response::Ok.new(value))
17 end
18
19 # Deferredを直ちに失敗させる。
20 # このメソッドはスレッドセーフです。
21 def fail(exception = nil)
22 execute(Delayer::Deferred::Response::Ng.new(exception))
23 end
24
25 private
26
27 def execute(value)
28 worker = Delayer::Deferred::Worker.new(delayer: self.class.delayer,
29 initial: value)
30 worker.push(self)
31 end
32 end
33 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/version"
2
3 module Delayer::Deferred
4 module Deferredable; end
5 end
6
7 require "delayer/deferred/deferredable/awaitable"
8 require "delayer/deferred/deferredable/chainable"
9 require "delayer/deferred/deferredable/graph"
10 require "delayer/deferred/deferredable/node_sequence"
11 require "delayer/deferred/deferredable/trigger"
0 # -*- coding: utf-8 -*-
1 require "delayer"
2 require "delayer/deferred/enumerator"
3
4 module Enumerable
5 # 遅延each。あとで実行されるし、あんまりループに時間がかかるようなら一旦ループを終了する
6 def deach(delayer=Delayer, &proc)
7 to_enum.deach(delayer, &proc)
8 end
9 end
0 # -*- coding: utf-8 -*-
1 require "delayer"
2 require "delayer/deferred/deferred"
3
4 class Enumerator
5 def deach(delayer=Delayer, &proc)
6 delayer.Deferred.new.next do
7 self.each do |node|
8 delayer.Deferred.pass
9 proc.(node)
10 end
11 end
12 end
13 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer::Deferred
3 Error = Class.new(StandardError)
4
5 class ForeignCommandAborted < Error
6 attr_reader :process
7 def initialize(message, process:)
8 super(message)
9 @process = process
10 end
11 end
12
13 SequenceError = Class.new(Error) do
14 attr_accessor :deferred
15 def initialize(message, deferred: nil)
16 super(message)
17 @deferred = deferred
18 end
19 end
20 MultipleAssignmentError = Class.new(SequenceError)
21 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/tools"
2 require "delayer/deferred/deferredable/trigger"
3
4 module Delayer::Deferred
5 class Promise
6 extend Delayer::Deferred::Tools
7 include Deferredable::Trigger
8
9 class << self
10 def new(stop=false, name: caller_locations(1,1).first.to_s, &block)
11 result = promise = super(name: name)
12 result = promise.next(&block) if block_given?
13 promise.call(true) unless stop
14 result
15 end
16
17 def Thread
18 @thread_class ||= gen_thread_class end
19
20 def Promise
21 self
22 end
23
24 def delayer
25 ::Delayer
26 end
27
28 def to_s
29 "#{self.delayer}.Promise"
30 end
31
32 private
33
34 def gen_thread_class
35 the_delayer = delayer
36 Class.new(Thread) do
37 define_singleton_method(:delayer) do
38 the_delayer
39 end
40 end
41 end
42 end
43
44 def initialize(name:)
45 super()
46 @name = name
47 end
48
49 def activate(response)
50 change_sequence(:activate)
51 change_sequence(:complete)
52 response
53 end
54
55 def inspect
56 "#<#{self.class} seq:#{sequence.name}>"
57 end
58
59 def ancestor
60 self
61 end
62
63 def parent=(chainable)
64 fail Error, "#{self.class} can't has parent."
65 end
66
67 private
68
69 def graph_shape
70 'egg'.freeze
71 end
72
73 def node_name
74 @name.to_s
75 end
76 end
77 end
0 # -*- coding: utf-8 -*-
1
2 # -*- coding: utf-8 -*-
3
4 module Delayer::Deferred::Request
5 class Base
6 attr_reader :value
7 def initialize(value)
8 @value = value
9 end
10 end
11
12 =begin rdoc
13 Fiberが次のWorkerを要求している時に返す値。
14 新たなインスタンスは作らず、 _NEXT_WORKER_ にあるインスタンスを使うこと。
15 =end
16 class NextWorker < Base
17 # _deferred_ に渡された次のChainableに、 _deferred_ の戻り値を渡す要求を出す。
18 # ==== Args
19 # [deferred] 実行が完了したDeferred 。次のDeferredとして _deferred.child_ を呼び出す
20 # [worker] このDeferredチェインを実行しているWorker
21 def accept_request(worker:, deferred:)
22 if deferred.has_child?
23 worker.push(deferred.child)
24 else
25 deferred.add_child_observer(worker)
26 end
27 end
28 end
29
30 =begin rdoc
31 Chainable#+@ が呼ばれた時に、一旦そこで処理を止めるためのリクエスト。
32 _value_ には、実行完了を待つDeferredが入っている。
33 ==== わかりやすい!
34 accept_requestメソッドの引数のdeferred {
35 +value
36 }
37 =end
38 class Await < Base
39 alias_method :foreign_deferred, :value
40 def accept_request(worker:, deferred:)
41 deferred.enter_await
42 foreign_deferred.add_child(Delayer::Deferred::Chain::Await.new(worker: worker, deferred: deferred))
43 end
44 end
45
46 =begin rdoc
47 一旦処理を中断して、Delayerキューに並び直すためのリクエスト。
48 Tools#pass から利用される。
49 新たなインスタンスは作らず、 _PASS_ にあるインスタンスを使うこと。
50 =end
51 class Pass < Base
52 def accept_request(worker:, deferred:)
53 deferred.enter_pass
54 worker.resume_pass(deferred)
55 end
56 end
57
58 NEXT_WORKER = NextWorker.new(nil).freeze
59 PASS = Pass.new(nil).freeze
60 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer::Deferred::Response
3 class Base
4 attr_reader :value
5 def initialize(value)
6 @value = value
7 end
8
9 def ng?
10 !ok?
11 end
12 end
13
14 class Ok < Base
15 def ok?
16 true
17 end
18 end
19
20 class Ng < Base
21 def ok?
22 false
23 end
24 end
25 end
0 # -*- coding: utf-8 -*-
1
2 Delayer::Deferred::ResultContainer = Struct.new(:success_flag, :value) do
3 def ok?
4 success_flag
5 end
6
7 def ng?
8 !success_flag
9 end
10 end
0 # -*- coding: utf-8 -*-
1 require "delayer"
2 require "delayer/deferred/deferredable/awaitable"
3
4 class Thread
5 include ::Delayer::Deferred::Deferredable::Awaitable
6
7 def self.delayer
8 Delayer
9 end
10
11 # このDeferredが成功した場合の処理を追加する。
12 # 新しいDeferredのインスタンスを返す。
13 # このメソッドはスレッドセーフです。
14 # TODO: procが空のとき例外を発生させる
15 def next(name: caller_locations(1,1).first.to_s, &proc)
16 add_child(Delayer::Deferred::Chain::Next.new(&proc), name: name)
17 end
18 alias deferred next
19
20 # このDeferredが失敗した場合の処理を追加する。
21 # 新しいDeferredのインスタンスを返す。
22 # このメソッドはスレッドセーフです。
23 # TODO: procが空のとき例外を発生させる
24 def trap(name: caller_locations(1,1).first.to_s, &proc)
25 add_child(Delayer::Deferred::Chain::Trap.new(&proc), name: name)
26 end
27 alias error trap
28
29 def add_child(chainable, name: caller_locations(1,1).first.to_s)
30 __gen_promise(name).add_child(chainable)
31 end
32
33 private
34
35 def __gen_promise(name)
36 promise = self.class.delayer.Promise.new(true, name: name)
37 Thread.new(self) do |tt|
38 __promise_callback(tt, promise)
39 end
40 promise
41 end
42
43 def __promise_callback(tt, promise)
44 begin
45 result = tt.value
46 self.class.delayer.new do
47 promise.call(result)
48 end
49 rescue Exception => err
50 self.class.delayer.new do
51 promise.fail(err)
52 end
53 end
54 end
55
56 end
0 # -*- coding: utf-8 -*-
1 require 'delayer/deferred/error'
2
3 module Delayer::Deferred
4 module Tools
5 def next(&proc)
6 new.next(&proc) end
7
8 def trap(&proc)
9 new.trap(&proc) end
10
11 # 実行中のDeferredを失敗させる。raiseと違って、Exception以外のオブジェクトをtrap()に渡すことができる。
12 # Deferredのnextとtrapの中でだけ呼び出すことができる。
13 # ==== Args
14 # [value] trap()に渡す値
15 # ==== Throw
16 # :__deferredable_fail をthrowする
17 def fail(value)
18 throw(:__deferredable_fail, value) end
19
20 # 実行中のDeferredを、Delayerのタイムリミットが来ている場合に限り一旦中断する。
21 # 長期に渡る可能性のある処理で、必要に応じて他のタスクを先に実行してもよい場合に呼び出す。
22 def pass
23 Fiber.yield(Request::PASS) if delayer.expire?
24 end
25
26 # 複数のdeferredを引数に取って、それら全ての実行が終了したら、
27 # その結果を引数の順番通りに格納したArrayを引数に呼ばれるDeferredを返す。
28 # 引数のDeferredが一つでも失敗するとこのメソッドの返すDeferredも失敗する。
29 # ==== Args
30 # [*args] 終了を待つDeferredオブジェクト
31 # ==== Return
32 # Deferred
33 def when(*args)
34 return self.next{[]} if args.empty?
35 args = args.flatten
36 args.each_with_index{|d, index|
37 unless d.is_a?(Deferredable::Chainable) || d.is_a?(Deferredable::Awaitable)
38 raise TypeError, "Argument #{index} of Deferred.when must be #{Deferredable::Chainable}, but given #{d.class}"
39 end
40 if d.respond_to?(:has_child?) && d.has_child?
41 raise "Already assigned child for argument #{index}"
42 end
43 }
44 defer, *follow = *args
45 defer.next{|res|
46 [res, *follow.map{|d| +d }]
47 }
48 end
49 # Kernel#systemを呼び出して、コマンドが成功たら成功するDeferredを返す。
50 # 失敗した場合、trap{}ブロックには $? の値(Process::Status)か、例外が発生した場合それが渡される
51 # ==== Args
52 # [*args] Kernel#spawn の引数
53 # ==== Return
54 # Deferred
55 def system(*args)
56 delayer.Deferred.Thread.new {
57 Process.waitpid2(Kernel.spawn(*args))
58 }.next{|_pid, status|
59 if status && status.success?
60 status
61 else
62 raise ForeignCommandAborted.new("command aborted: #{args.join(' ')}", process: status) end
63 }
64 end
65 end
66 end
0 module Delayer
1 module Deferred
2 VERSION = "2.0.0"
3 end
4 end
0 # -*- coding: utf-8 -*-
1 require "delayer/deferred/request"
2 require "delayer/deferred/response"
3
4 module Delayer::Deferred
5 =begin rdoc
6 Deferredを実行するためのWorker。Deferredチェインを実行するFiberを
7 管理する。
8
9 == pushに渡すオブジェクトについて
10 Worker#push に渡す引数は、activateメソッドを実装している必要がある。
11
12 === activate(response)
13 ==== Args
14 response :: Delayer::Deferred::Response::Base Deferredに渡す値
15 ==== Returns
16 [Delayer::Deferred::Response::Base]
17 これを返すと、値の自動変換が行われないため、意図的に失敗させたり、Deferredを次のブロックに伝搬させることができる。
18 [Delayer::Deferred::Chainable]
19 戻り値のDeferredが終わるまでWorkerの処理を停止する。
20 再開された時、結果は戻り値のDeferredの結果に置き換えられる。
21 [else]
22 _Delayer::Deferred::Response::Ok.new_ の引数に渡され、その結果が利用される
23 =end
24 class Worker
25 def initialize(delayer:, initial:)
26 @delayer, @initial = delayer, initial
27 end
28
29 def push(deferred)
30 deferred.reserve_activate
31 @delayer.new do
32 next if deferred.spoiled?
33 begin
34 fiber.resume(deferred).accept_request(worker: self,
35 deferred: deferred)
36 rescue Delayer::Deferred::SequenceError => err
37 err.deferred = deferred
38 raise
39 end
40 end
41 nil
42 end
43
44 # Awaitから復帰した時に呼ばれる。
45 # ==== Args
46 # [response] Awaitの結果(Delayer::Deferred::Response::Base)
47 # [deferred] 現在実行中のDeferred
48 def give_response(response, deferred)
49 @delayer.new do
50 next if deferred.spoiled?
51 deferred.exit_await
52 fiber.resume(response).accept_request(worker: self,
53 deferred: deferred)
54 end
55 nil
56 end
57
58 # Tools#pass から復帰した時に呼ばれる。
59 # ==== Args
60 # [deferred] 現在実行中のDeferred
61 def resume_pass(deferred)
62 deferred.exit_pass
63 @delayer.new do
64 next if deferred.spoiled?
65 fiber.resume(nil).accept_request(worker: self,
66 deferred: deferred)
67 end
68 end
69
70 private
71
72 def fiber
73 @fiber ||= Fiber.new{|response|
74 loop do
75 response = wait_and_activate(response)
76 case response.value
77 when Delayer::Deferred::SequenceError
78 raise response.value
79 end
80 end
81 }.tap{|f| f.resume(@initial); @initial = nil }
82 end
83
84 def wait_and_activate(argument)
85 response = catch(:success) do
86 failed = catch(:__deferredable_fail) do
87 begin
88 if argument.value.is_a? Deferredable::Awaitable
89 throw :success, +argument.value
90 else
91 defer = Fiber.yield(Request::NEXT_WORKER)
92 res = defer.activate(argument)
93 if res.is_a? Delayer::Deferred::Deferredable::Awaitable
94 defer.add_awaited(res)
95 end
96 end
97 throw :success, res
98 rescue Exception => err
99 throw :__deferredable_fail, err
100 end
101 end
102 Response::Ng.new(failed)
103 end
104 if response.is_a?(Response::Base)
105 response
106 else
107 Response::Ok.new(response)
108 end
109 end
110 end
111 end
0 # coding: utf-8
1 require "delayer"
2 require "delayer/deferred/deferred"
3 require "delayer/deferred/deferredable"
4 require "delayer/deferred/enumerable"
5 require "delayer/deferred/enumerator"
6 require "delayer/deferred/thread"
7 require "delayer/deferred/tools"
8 require "delayer/deferred/version"
9
10 module Delayer
11 module Deferred
12 class << self
13 #真ならデバッグ情報を集める
14 attr_accessor :debug
15
16 def method_missing(*rest, &block)
17 Delayer::Deferred::Deferred.__send__(*rest, &block)
18 end
19 end
20 end
21
22 module Extend
23 def Promise
24 @promise ||= begin
25 the_delayer = self
26 Class.new(::Delayer::Deferred::Promise) {
27 define_singleton_method(:delayer) {
28 the_delayer } } end
29 end
30 alias :Deferred :Promise
31 #deprecate :Deferred, "Promise", 2018, 03
32 end
33 end
34
35 Delayer::Deferred.debug = false
0 # -*- coding: utf-8 -*-
1
2 module Delayer
3 class Error < ::StandardError; end
4 class TooLate < Error; end
5 class AlreadyExecutedError < TooLate; end
6 class AlreadyCanceledError < TooLate; end
7 class AlreadyRunningError < TooLate; end
8 class InvalidPriorityError < Error; end
9 def self.StateError(state)
10 case state
11 when :run
12 AlreadyRunningError
13 when :done
14 AlreadyExecutedError
15 when :cancel
16 AlreadyCanceledError
17 end
18 end
19 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer
3 module Extend
4 attr_accessor :expire
5 attr_reader :exception
6
7 def self.extended(klass)
8 klass.class_eval do
9 @first_pointer = @last_pointer = nil
10 @busy = false
11 @expire = 0
12 @remain_hook = nil
13 @exception = nil
14 @remain_received = false
15 @lock = Mutex.new
16 end
17 end
18
19 # Run registered jobs.
20 # ==== Args
21 # [current_expire] expire for processing (secs, 0=unexpired)
22 # ==== Return
23 # self
24 def run(current_expire = @expire)
25 if 0 == current_expire
26 run_once while not empty?
27 else
28 @end_time = Time.new.to_f + @expire
29 run_once while not(empty?) and @end_time >= Time.new.to_f
30 @end_time = nil
31 end
32 if @remain_hook
33 @remain_received = !empty?
34 @remain_hook.call if @remain_received
35 end
36 rescue Exception => e
37 @exception = e
38 raise e
39 end
40
41 def expire?
42 if defined?(@end_time) and @end_time
43 @end_time < Time.new.to_f
44 else
45 false
46 end
47 end
48
49 # Run a job and forward pointer.
50 # ==== Return
51 # self
52 def run_once
53 if @first_pointer
54 @busy = true
55 procedure = forward
56 procedure = forward while @first_pointer and procedure.canceled?
57 procedure.run unless procedure.canceled?
58 end
59 ensure
60 @busy = false
61 end
62
63 # Return if some jobs processing now.
64 # ==== Args
65 # [args]
66 # ==== Return
67 # true if Delayer processing job
68 def busy?
69 @busy
70 end
71
72 # Return true if no jobs has.
73 # ==== Return
74 # true if no jobs has.
75 def empty?
76 !@first_pointer
77 end
78
79 # Return remain jobs quantity.
80 # ==== Return
81 # Count of remain jobs
82 def size(node = @first_pointer)
83 if node
84 1 + size(node.next)
85 else
86 0
87 end
88 end
89
90 # register new job.
91 # ==== Args
92 # [procedure] job(Delayer::Procedure)
93 # ==== Return
94 # self
95 def register(procedure)
96 lock.synchronize do
97 if @last_pointer
98 @last_pointer = @last_pointer.break procedure
99 else
100 @last_pointer = @first_pointer = procedure
101 end
102 if @remain_hook and not @remain_received
103 @remain_received = true
104 @remain_hook.call
105 end
106 end
107 self
108 end
109
110 def register_remain_hook
111 @remain_hook = Proc.new
112 end
113
114 private
115
116 def forward
117 lock.synchronize do
118 prev = @first_pointer
119 @first_pointer = @first_pointer.next
120 @last_pointer = nil unless @first_pointer
121 prev
122 end
123 end
124
125 def lock
126 @lock
127 end
128
129 end
130 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer
3 module Priority
4 attr_reader :priority
5
6 def self.included(klass)
7 klass.class_eval do
8 include ::Delayer
9 extend Extend
10 end
11 end
12
13 def initialize(priority = self.class.instance_eval{ @default_priority }, *args)
14 self.class.validate_priority priority
15 @priority = priority
16 super(*args)
17 end
18
19 module Extend
20 def self.extended(klass)
21 klass.class_eval do
22 @priority_pointer = {}
23 end
24 end
25
26 # register new job.
27 # ==== Args
28 # [procedure] job(Delayer::Procedure)
29 # ==== Return
30 # self
31 def register(procedure)
32 priority = procedure.delayer.priority
33 lock.synchronize do
34 last_pointer = get_prev_point(priority)
35 if last_pointer
36 @priority_pointer[priority] = last_pointer.break procedure
37 else
38 procedure.next = @first_pointer
39 @priority_pointer[priority] = @first_pointer = procedure
40 end
41 if @last_pointer
42 @last_pointer = @priority_pointer[priority]
43 end
44 if @remain_hook and not @remain_received
45 @remain_received = true
46 @remain_hook.call
47 end
48 end
49 self
50 end
51
52 def get_prev_point(priority)
53 if @priority_pointer[priority]
54 @priority_pointer[priority]
55 else
56 next_index = @priorities.index(priority) - 1
57 get_prev_point @priorities[next_index] if 0 <= next_index
58 end
59 end
60
61 def validate_priority(symbol)
62 unless @priorities.include? symbol
63 raise Delayer::InvalidPriorityError, "undefined priority '#{symbol}'"
64 end
65 end
66
67 private
68
69 def forward
70 lock.synchronize do
71 prev = @first_pointer
72 @first_pointer = @first_pointer.next
73 @last_pointer = nil unless @first_pointer
74 @priority_pointer.each do |priority, pointer|
75 @priority_pointer[priority] = @first_pointer if prev == pointer
76 end
77 prev
78 end
79 end
80
81 end
82 end
83 end
0 # -*- coding: utf-8 -*-
1
2 module Delayer
3 class Procedure
4 attr_reader :state, :delayer
5 attr_accessor :next
6 def initialize(delayer, &proc)
7 @delayer, @proc = delayer, proc
8 @state = :stop
9 @next = nil
10 @delayer.class.register(self)
11 end
12
13 # Run a process
14 # ==== Exception
15 # Delayer::TooLate :: if already called run()
16 # ==== Return
17 # node
18 def run
19 unless :stop == @state
20 raise Delayer::StateError(@state), "call twice Delayer::Procedure"
21 end
22 @state = :run
23 @proc.call
24 @state = :done
25 @proc = nil
26 end
27
28 # Cancel this job
29 # ==== Exception
30 # Delayer::TooLate :: if already called run()
31 # ==== Return
32 # self
33 def cancel
34 unless :stop == @state
35 raise Delayer::StateError(@state), "cannot cancel Delayer::Procedure"
36 end
37 @state = :cancel
38 self
39 end
40
41 # Return true if canceled this task
42 # ==== Return
43 # true if canceled this task
44 def canceled?
45 :cancel == @state
46 end
47
48 # insert node between self and self.next
49 # ==== Args
50 # [node] insertion
51 # ==== Return
52 # node
53 def break(node)
54 tail = @next
55 @next = node
56 node.next = tail
57 node
58 end
59 end
60 end
0 module Delayer
1 VERSION = "0.0.2"
2 end
0 # -*- coding: utf-8 -*-
1 require "delayer/version"
2 require "delayer/error"
3 require "delayer/extend"
4 require "delayer/procedure"
5 require "delayer/priority"
6 require "monitor"
7
8 module Delayer
9 class << self
10 attr_accessor :default
11
12 def included(klass)
13 klass.extend Extend
14 end
15
16 # Generate new Delayer class.
17 # ==== Args
18 # [options]
19 # Hash
20 # expire :: processing expire (secs, 0=unlimited)
21 # priority :: priorities
22 # default :: default priotity
23 # ==== Return
24 # A new class
25 def generate_class(options = {})
26 if options[:priority]
27 Class.new do
28 include Priority
29 @expire = options[:expire] || 0
30 @priorities = options[:priority]
31 @default_priority = options[:default]
32 end
33 else
34 Class.new do
35 include ::Delayer
36 @expire = options[:expire] || 0
37 end
38 end
39 end
40
41 def method_missing(*args, &proc)
42 (@default ||= generate_class).__send__(*args, &proc)
43 end
44 end
45
46 def initialize(*args)
47 super
48 @procedure = Procedure.new(self, &Proc.new)
49 end
50
51 # Cancel this job
52 # ==== Exception
53 # Delayer::AlreadyExecutedError :: if already called run()
54 # ==== Return
55 # self
56 def cancel
57 @procedure.cancel
58 self
59 end
60 end
0 # -*- coding: utf-8 -*-
1
2 module Diva::Combinable
3 class Combinator
4 def initialize(a, b)
5 @a, @b = a, b
6 end
7
8 # コンストラクタに渡された二つの値が _behavior_ をサポートしていれば真を返す
9 # ==== Args
10 # [behavior] 想定する振る舞い(Symbol)
11 # ==== Return
12 # true or false
13 def =~(behavior)
14 fail 'Not implement'
15 end
16
17 def ===(behavior)
18 self =~ behavior
19 end
20
21 # このCombinatorが持っている値のすべての組み合わせのうち、適切に _behavior_
22 # をサポートしている組み合わせについて繰り返すEnumeratorを返す
23 # ==== Args
24 # [behavior] 想定する振る舞い(Symbol)
25 # ==== Return
26 # [レシーバ, 引数] のペアを列挙するEnumerator
27 def enum(behavior)
28 Enumerator.new do |yielder|
29 yielder << [@a, @b] if a_b(behavior)
30 yielder << [@b, @a] if b_a(behavior)
31 end
32 end
33
34 def method_missing(behavior, *rest)
35 if rest.empty?
36 return self =~ behavior
37 end
38 super
39 end
40
41 private
42
43 def a_b(behavior)
44 @a.respond_to?(behavior) && @a.__send__(behavior, @b)
45 end
46
47 def b_a(behavior)
48 @b.respond_to?(behavior) && @b.__send__(behavior, @a)
49 end
50 end
51
52 class PluralCombinator < Combinator
53 def enum(behavior)
54 Enumerator.new do |yielder|
55 @b.each do |b|
56 (b | @a).enum(behavior).each(&yielder.method(:<<))
57 end
58 end
59 end
60 end
61
62 class SingleCombinator < Combinator
63 def =~(behavior)
64 a_b(behavior) || b_a(behavior)
65 end
66 end
67
68 class AnyCombinator < PluralCombinator
69 def =~(behavior)
70 @b.any?{|b| b | @a =~ behavior }
71 end
72 end
73
74 class AllCombinator < PluralCombinator
75 def =~(behavior)
76 @b.all?{|b| b | @a =~ behavior }
77 end
78 end
79
80 # _other_ との Diva::Conbinable::Combinator を生成する
81 # ==== Args
82 # [other] Diva::Modelか、Diva::Modelを列挙するEnumerable
83 # ==== Return
84 # Diva::Conbinable::Combinator
85 def |(other)
86 if other.is_a? Enumerable
87 AnyCombinator.new(self, other)
88 else
89 SingleCombinator.new(self, other)
90 end
91 end
92
93 # _other_ との Diva::Conbinable::Combinator を生成する
94 # ==== Args
95 # [other] Diva::Modelか、Diva::Modelを列挙するEnumerable
96 # ==== Return
97 # Diva::Conbinable::Combinator
98 def &(other)
99 if other.is_a? Enumerable
100 AllCombinator.new(self, other)
101 else
102 SingleCombinator.new(self, other)
103 end
104 end
105 end
0 # -*- coding: utf-8 -*-
1
2 =begin rdoc
3 データの保存/復元を実際に担当するデータソース。
4 データソースをモデルにModel::add_data_retrieverにて幾つでも参加させることが出来る。
5 =end
6 module Diva::DataSource
7 USE_ALL = -1 # findbyidの引数。全てのDataSourceから探索する
8 USE_LOCAL_ONLY = -2 # findbyidの引数。ローカルにあるデータのみを使う
9
10 attr_accessor :keys
11
12 # idをもつデータを返す。
13 # もし返せない場合は、nilを返す
14 def findbyid(id, policy)
15 nil
16 end
17
18 # 取得できたらそのDivaのインスタンスをキーにして実行されるDeferredを返す
19 def idof(id)
20 Thread.new{ findbyid(id) } end
21 alias [] idof
22
23 # データの保存
24 # データ一件保存する。保存に成功したか否かを返す。
25 def store_datum(datum)
26 false
27 end
28
29 def inspect
30 self.class.to_s
31 end
32 end
0 # -*- coding: utf-8 -*-
1 module Diva
2 class DivaError < StandardError; end
3
4 class InvalidTypeError < DivaError; end
5
6 class InvalidEntityError < DivaError; end
7
8 # 実装してもしなくてもいいメソッドが実装されておらず、結果を得られない
9 class NotImplementedError < DivaError; end
10
11 # IDやURIなどの一意にリソースを特定する情報を使ってデータソースに問い合わせたが、
12 # 対応する情報が見つからず、Modelを作成できない
13 class ModelNotFoundError < DivaError; end
14
15 # URIとして受け付けられない値を渡された
16 class InvalidURIError < InvalidTypeError; end
17
18 end
0 # -*- coding: utf-8 -*-
1
2 require 'diva/type'
3
4 =begin rdoc
5 Modelのキーの情報を格納する。
6 キーひとつにつき1つのインスタンスが作られる。
7 =end
8 module Diva
9 class Field
10 attr_reader :name, :type, :required
11
12 # [name] Symbol フィールドの名前
13 # [type] Symbol フィールドのタイプ。:int, :string, :bool, :time のほか、Diva::Modelのサブクラスを指定する
14 # [required] boolean _true_ なら、この項目を必須とする
15 def initialize(name, type, required: false)
16 @name = name.to_sym
17 @type = Diva::Type.optional(Diva::Type(type))
18 @required = !!required
19 end
20
21 def dump_for_json(value)
22 type.dump_for_json(value)
23 end
24
25 def required?
26 required
27 end
28
29 def to_sym
30 name
31 end
32
33 def to_s
34 name.to_s
35 end
36
37 def inspect
38 "#<#{self.class}: #{name}(#{type})#{required ? '*' : ''}>"
39 end
40 end
41 end
0 # -*- coding: utf-8 -*-
1
2 class Diva::FieldGenerator
3 def initialize(model_klass)
4 @model_klass = model_klass
5 end
6
7 def int(field_name, required: false)
8 @model_klass.add_field(field_name, type: :int, required: required)
9 end
10
11 def string(field_name, required: false)
12 @model_klass.add_field(field_name, type: :string, required: required)
13 end
14
15 def bool(field_name, required: false)
16 @model_klass.add_field(field_name, type: :bool, required: required)
17 end
18
19 def time(field_name, required: false)
20 @model_klass.add_field(field_name, type: :time, required: required)
21 end
22
23 def uri(field_name, required: false)
24 @model_klass.add_field(field_name, type: :uri, required: required)
25 end
26
27 def has(field_name, type, required: false)
28 @model_klass.add_field(field_name, type: type, required: required)
29 end
30 end
31
32
33
34
35
36
37
38
0 # -*- coding: utf-8 -*-
1 =begin rdoc
2 いろんなリソースの基底クラス
3 =end
4
5 require 'diva/combinator'
6 require 'diva/model_extend'
7 require 'diva/uri'
8 require 'diva/spec'
9
10 require 'securerandom'
11
12 class Diva::Model
13 include Comparable
14 include Diva::Combinable
15 extend Diva::ModelExtend
16
17 def initialize(args)
18 @value = args.dup
19 validate
20 self.class.store_datum(self)
21 end
22
23 # データをマージする。
24 # selfにあってotherにもあるカラムはotherの内容で上書きされる。
25 # 上書き後、データはDataSourceに保存される
26 def merge(other)
27 @value.update(other.to_hash)
28 validate
29 self.class.store_datum(self)
30 end
31
32 # このModelのパーマリンクを返す。
33 # パーマリンクはWebのURLで、Web上のリソースでない場合はnilを返す。
34 # ==== Return
35 # 次のいずれか
36 # [URI::HTTP|Diva::URI] パーマリンク
37 # [nil] パーマリンクが存在しない
38 def perma_link
39 nil
40 end
41
42 # このModelのURIを返す。
43 # ==== Return
44 # [URI::Generic|Diva::URI] パーマリンク
45 def uri
46 perma_link || Diva::URI.new("#{self.class.scheme}://#{self.class.host}#{path}")
47 end
48
49 # このModelが、登録されているアカウントのうちいずれかが作成したものであれば true を返す
50 # ==== Args
51 # [service] Service | Enumerable 「自分」のService
52 # ==== Return
53 # [true] 自分のによって作られたオブジェクトである
54 # [false] 自分のによって作られたオブジェクトではない
55 def me?(service=nil)
56 false
57 end
58
59 def hash
60 @_hash ||= self.uri.to_s.hash ^ self.class.hash
61 end
62
63 def <=>(other)
64 if other.is_a?(Diva::Model)
65 created - other.created
66 elsif other.respond_to?(:[]) and other[:created]
67 created - other[:created]
68 else
69 id - other
70 end
71 end
72
73 def ==(other)
74 if other.is_a? Diva::Model
75 self.class == other.class && uri == other.uri
76 end
77 end
78
79 def eql?(other)
80 self == other
81 end
82
83 def to_hash
84 Hash[self.class.fields.map{|f| [f.name, fetch(f.name)] }]
85 end
86
87 def to_json(*rest, **kwrest)
88 Hash[
89 self.class.fields.map{|f| [f.name, f.dump_for_json(fetch(f.name))] }
90 ].to_json(*rest, **kwrest)
91 end
92
93 # カラムの生の内容を返す
94 def fetch(key)
95 @value[key.to_sym]
96 end
97 alias [] fetch
98
99 # カラムに別の値を格納する。
100 # 格納後、データはDataSourceに保存される
101 def []=(key, value)
102 @value[key.to_sym] = value
103 self.class.store_datum(self)
104 value
105 end
106
107 # カラムと型が違うものがある場合、例外を発生させる。
108 def validate
109 raise RuntimeError, "argument is #{@value}, not Hash" if not @value.is_a?(Hash)
110 self.class.fields.each do |field|
111 begin
112 @value[field.name] = field.type.cast(@value[field.name])
113 rescue Diva::InvalidTypeError => err
114 raise Diva::InvalidTypeError, "#{err} in field `#{field}'"
115 end
116 end
117 end
118
119 # キーとして定義されていない値を全て除外した配列を生成して返す。
120 # また、Modelを子に含んでいる場合、それを外部キーに変換する。
121 def filtering
122 datum = self.to_hash
123 result = Hash.new
124 self.class.fields.each do |field|
125 begin
126 result[field.name] = field.type.cast(datum[field.name])
127 rescue Diva::InvalidTypeError => err
128 raise Diva::InvalidTypeError, "#{err} in field `#{field}'"
129 end
130 end
131 result
132 end
133
134 # このインスタンスのタイトル。
135 def title
136 fields = self.class.fields.lazy.map(&:name)
137 case
138 when fields.include?(:name)
139 name.gsub("\n", '')
140 when fields.include?(:description)
141 description.gsub("\n", '')
142 else
143 to_s.gsub("\n", '')
144 end
145 end
146
147 private
148 # URIがデフォルトで使うpath要素
149 def path
150 @path ||= "/#{SecureRandom.uuid}"
151 end
152
153 end
154
0 # -*- coding: utf-8 -*-
1
2 require 'diva/field'
3 require 'diva/type'
4
5 =begin rdoc
6 Diva::Model のクラスメソッド
7 =end
8 module Diva::ModelExtend
9 extend Gem::Deprecate
10
11 attr_reader :slug, :spec
12
13 # Modelのインスタンスのuriスキーム。オーバライドして適切な値にする
14 # ==== Return
15 # [String] URIスキーム
16 def scheme
17 @_scheme ||= self.to_s.split('::',2).first.gsub(/\W/,'').downcase.freeze
18 end
19
20 # Modelのインスタンスのホスト名。オーバライドして適切な値にする
21 # ==== Return
22 # [String] ホスト名
23 def host
24 @_host ||= self.to_s.split('::',2).last.split('::').reverse.join('.').gsub(/[^\w\.]/,'').downcase.freeze
25 end
26
27 # Modelにフィールドを追加する。
28 # ==== Args
29 # [field_name] Symbol フィールドの名前
30 # [type] Symbol フィールドのタイプ。:int, :string, :bool, :time のほか、Diva::Modelのサブクラスを指定する
31 # [required] boolean _true_ なら、この項目を必須とする
32 def add_field(field, type: nil, required: false)
33 if field.is_a?(Symbol)
34 field = Diva::Field.new(field, type, required: required)
35 end
36 (@fields ||= []) << field
37 define_method(field.name) do
38 @value[field.name]
39 end
40
41 define_method("#{field.name}?") do
42 !!@value[field.name]
43 end
44
45 define_method("#{field.name}=") do |value|
46 @value[field.name] = field.type.cast(value)
47 self.class.store_datum(self)
48 value
49 end
50 self
51 end
52
53 def fields
54 @fields ||= []
55 end
56 alias :keys :fields
57 deprecate :keys, "fields", 2018, 02
58
59 #
60 # プライベートクラスメソッド
61 #
62
63 def field
64 Diva::FieldGenerator.new(self)
65 end
66
67 # URIに対応するリソースの内容を持ったModelを作成する。
68 # URIに対応する情報はネットワーク上などから取得される場合もある。そういった場合はこのメソッドは
69 # Delayer::Deferred::Deferredable を返す可能性がある。
70 # とくにオーバライドしない場合、このメソッドは常に例外 Diva::NotImplementedError を投げる。
71 # ==== Args
72 # [uri] _handle_ メソッドで指定したいずれかの条件に一致するURI
73 # ==== Return
74 # [Delayer::Deferred::Deferredable]
75 # ネットワークアクセスを行って取得するなど取得に時間がかかる場合
76 # [self]
77 # すぐにModelを生成できる場合、そのModel
78 # ==== Raise
79 # [Diva::NotImplementedError]
80 # このModelでは、find_by_uriが実装されていない
81 # [Diva::ModelNotFoundError]
82 # _uri_ に対応するリソースが見つからなかった
83 def find_by_uri(uri)
84 raise Diva::NotImplementedError, "#{self}.find_by_uri does not implement."
85 end
86
87 # Modelが生成・更新された時に呼ばれるコールバックメソッドです
88 def store_datum(retriever); end
89
90 def container_class
91 Array
92 end
93 end
0 # -*- coding: utf-8 -*-
1
2 Diva::ModelSpec = Struct.new(
3 :slug,
4 :name,
5 :reply,
6 :myself,
7 :timeline,
8 )
0 # -*- coding: utf-8 -*-
1
2 =begin rdoc
3 Modelの各キーに格納できる値の制約。
4 この制約に満たない場合は、アトミックな制約であれば値の変換が行われ、そうでない場合は
5 Diva::InvalidTypeError 例外を投げる。
6
7 これは新しくインスタンスなどを作らず、INT、FLOATなどのプリセットを利用する。
8
9 == 設定できる制約
10 Modelフィールドの制約には以下のようなものがある。
11
12 === アトミックな制約
13 以下のような値は、DivaのModelフィールドにおいてはアトミックな制約と呼び、そのまま格納できる。
14 [INT] 数値(Integer)
15 [FLOAT] 少数(Float)
16 [BOOL] 真理値(true|false)
17 [STRING] 文字列(String)
18 [TIME] 時刻(Time)
19 [URI] URI(Diva::URI|URI::Generic|Addressable::URI)
20
21 === Model
22 Diva::Modelのサブクラスであれば、それを制約とすることができる。
23
24 === 配列
25 アトミックな制約またはModel制約を満たした値の配列を格納することができる。
26 配列の全ての要素が設定された制約を満たしていれば、配列制約が満たされたことになる。
27
28 =end
29 require "time"
30
31 module Diva::Type
32 extend self
33
34 def model_of(model)
35 ModelType.new(model)
36 end
37
38 def array_of(type)
39 ArrayType.new(type)
40 end
41
42 def optional(type)
43 OptionalType.new(type)
44 end
45
46 # 全てのType共通のスーパークラス
47 class MetaType
48 attr_reader :name
49
50 def initialize(name, *rest, &cast)
51 @name = name.to_sym
52 if cast
53 define_singleton_method :cast, &cast
54 end
55 end
56
57 def cast(value)
58 value
59 end
60
61 def to_s
62 name.to_s
63 end
64
65 def dump_for_json(value)
66 value
67 end
68
69 def inspect
70 "Diva::Type(#{to_s})"
71 end
72 end
73
74 class AtomicType < MetaType
75 end
76
77 INT = AtomicType.new(:int) do |v|
78 case v
79 when Integer
80 v
81 when Numeric, String, Time
82 v.to_i
83 when TrueClass
84 1
85 when FalseClass
86 0
87 else
88 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
89 end
90 end
91 FLOAT = AtomicType.new(:float) do |v|
92 case v
93 when Float
94 v
95 when Numeric, String, Time
96 v.to_f
97 else
98 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
99 end
100 end
101 BOOL = AtomicType.new(:bool) do |v|
102 case v
103 when TrueClass, FalseClass
104 v
105 when String
106 !v.empty?
107 when Integer
108 v != 0
109 else
110 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
111 end
112 end
113 STRING = AtomicType.new(:string) do |v|
114 case v
115 when Diva::Model, Enumerable
116 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
117 else
118 v.to_s
119 end
120 end
121 class TimeType < AtomicType
122 def dump_for_json(value)
123 cast(value).iso8601
124 end
125 end
126 TIME = TimeType.new(:time) do |v|
127 case v
128 when Time
129 v
130 when Integer, Float
131 Time.at(v)
132 when String
133 Time.iso8601(v)
134 else
135 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
136 end
137 end
138 URI = AtomicType.new(:uri) do |v|
139 case v
140 when Diva::URI, Addressable::URI, ::URI::Generic
141 v
142 when String
143 Diva::URI.new(v)
144 else
145 raise Diva::InvalidTypeError, "The value is not a `#{name}'."
146 end
147 end
148
149 class ModelType < MetaType
150 attr_reader :model
151 def initialize(model, *rest, &cast)
152 super(:model, *rest)
153 @model = model
154 end
155
156 def cast(value)
157 case value
158 when model
159 value
160 when Hash
161 model.new(value)
162 else
163 raise Diva::InvalidTypeError, "The value #{value.inspect} is not a `#{model}'."
164 end
165 end
166
167 def to_s
168 "#{model} #{name}"
169 end
170 end
171
172 class ArrayType < MetaType
173 def initialize(type)
174 type = Diva::Type(type)
175 super("#{type.name}_array")
176 @type = type
177 end
178
179 def cast(value)
180 raise Diva::InvalidTypeError, "The value is not a `#{name}'." unless value.is_a?(Enumerable)
181 value.to_a.map(&@type.method(:cast))
182 end
183
184 def dump_for_json(value)
185 value.to_a.map(&@type.method(:dump_for_json))
186 end
187
188 def to_s
189 "Array of #{@type.to_s}"
190 end
191 end
192
193 class OptionalType < MetaType
194 def initialize(type)
195 super("optional_#{type.name}")
196 @type = type
197 end
198
199 def cast(value)
200 if value.nil?
201 value
202 else
203 @type.cast(value)
204 end
205 end
206
207 def dump_for_json(value)
208 if value.nil?
209 value
210 else
211 @type.dump_for_json(value)
212 end
213 end
214
215 def to_s
216 "#{@type.to_s}|nil"
217 end
218 end
219
220 end
221
222 module Diva
223 def self.Type(type)
224 case type
225 when Diva::Type::MetaType
226 type
227 when :int
228 Diva::Type::INT
229 when :float
230 Diva::Type::FLOAT
231 when :bool
232 Diva::Type::BOOL
233 when :string
234 Diva::Type::STRING
235 when :time
236 Diva::Type::TIME
237 when :uri
238 Diva::Type::URI
239 when ->x{x.class == Class && x.ancestors.include?(Diva::Model) }
240 Diva::Type.model_of(type)
241 when Array
242 Diva::Type.array_of(type.first)
243 else
244 fail "Invalid type #{type.inspect} (#{type.class})."
245 end
246 end
247 end
0 # -*- coding: utf-8 -*-
1
2 =begin rdoc
3 =Model用のURIクラス
4
5 mikutterでは、 URI や Addressable::URI の代わりに、このクラスを使用します。
6 URI や Addressable::URI に比べて、次のような特徴があります。
7
8 * コンストラクタに文字列を渡している場合、 _to_s_ がその文字列を返す。
9 * 正規化しないかわりに高速に動作します。
10 * Diva::URI のインスタンスは URI と同じように使える
11 * unicode文字などが入っていて URI では表現できない場合、 Addressable::URI を使う
12 * Addressable::URIでないと表現できないURIであればそちらを使うという判断を自動で行う
13
14 == 使い方
15 Diva::URI() メソッドの引数にString, URI, Addressable::URI, Hash, Diva::URIのいずれかを与えます。
16
17 [String] uriの文字列(ex: "http://mikutter.hachune.net/")
18 [URI] URI のインスタンス
19 [Addressable::URI] Addressable::URI のインスタンス
20 [Hash] これを引数にURI::Generic.build に渡すのと同じ形式の Hash
21 [Diva::URI] 即座にこれ自身を返す
22
23 == 例
24
25 Diva::URI("http://mikutter.hachune.net/")
26 Diva::URI(URI::Generic.build(scheme: 'http', host: 'mikutter.hachune.net'))
27 =end
28
29 require 'uri'
30 require 'addressable/uri'
31
32 class Diva::URI
33 def initialize(uri)
34 case uri.freeze
35 when URI, Addressable::URI
36 @uri = uri
37 when String
38 @uri_string = uri
39 when Hash
40 @uri_hash = uri
41 end
42 end
43
44 def ==(other)
45 case other
46 when URI, Addressable::URI, String
47 other.to_s == self.to_s
48 when Diva::URI
49 if has_string? or other.has_string?
50 to_s == other.to_s
51 else
52 other.to_uri == to_uri
53 end
54 end
55 end
56
57 def hash
58 to_s.hash ^ self.class.hash
59 end
60
61 def has_string?
62 !!@uri_string
63 end
64
65 def has_uri?
66 !!@uri
67 end
68
69 def to_s
70 @uri_string ||= to_uri.to_s.freeze
71 end
72
73 def to_uri
74 @uri ||= generate_uri.freeze
75 end
76
77 def scheme
78 if has_string? and !has_uri?
79 match = @uri_string.match(%r<\A(\w+):>)
80 if match
81 match[1]
82 else
83 to_uri.scheme
84 end
85 else
86 to_uri.scheme
87 end
88 end
89
90 def freeze
91 unless frozen?
92 to_uri
93 to_s
94 end
95 super
96 end
97
98 def respond_to?(method)
99 super or to_uri.respond_to?(method)
100 end
101
102 def method_missing(method, *rest, &block)
103 to_uri.__send__(method, *rest, &block)
104 end
105
106 private
107
108 def generate_uri
109 if @uri
110 @uri
111 elsif @uri_string
112 @uri = generate_uri_by_string
113 elsif @uri_hash
114 @uri = generate_uri_by_hash
115 end
116 @uri
117 end
118
119 def generate_uri_by_string
120 URI.parse(@uri_string)
121 rescue URI::Error
122 Addressable::URI.parse(@uri_string)
123 end
124
125 def generate_uri_by_hash
126 URI::Generic.build(@uri_hash)
127 rescue URI::Error
128 Addressable::URI.new(@uri_hash)
129 end
130 end
0 module Diva
1 VERSION = "0.3.2"
2 end
0 # coding: utf-8
1 require 'diva/version'
2 require 'diva/datasource'
3 require 'diva/error'
4 require 'diva/field_generator'
5 require 'diva/field'
6 require 'diva/model'
7 require 'diva/spec'
8 require 'diva/type'
9 require 'diva/uri'
10 require 'diva/version'
11
12
13 module Diva
14 def self.URI(uri)
15 Diva::URI.new(uri)
16 end
17 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 gettext/cgi.rb - GetText for CGI
4
5 Copyright (C) 2005-2009 Masao Mutoh
6
7 You may redistribute it and/or modify it under the same
8 license terms as Ruby or LGPL.
9 =end
10
11 require 'cgi'
12 require 'gettext'
13
14 Locale.init(:driver => :cgi)
15
16 module GetText
17
18 # Sets a CGI object. This methods is appeared when requiring "gettext/cgi".
19 # * cgi_: CGI object
20 # * Returns: self
21 def set_cgi(cgi_)
22 Locale.set_cgi(cgi_)
23 end
24
25 # Same as GetText.set_cgi. This methods is appeared when requiring "gettext/cgi".
26 # * cgi_: CGI object
27 # * Returns: cgi_
28 def cgi=(cgi_)
29 set_cgi(cgi_)
30 cgi_
31 end
32
33 # Gets the CGI object. If it is nil, returns new CGI object. This methods is appeared when requiring "gettext/cgi".
34 # * Returns: the CGI object
35 def cgi
36 Locale.cgi
37 end
38 end
0 # -*- coding: utf-8 -*-
1
2 module GetText
3 # For normalize/finding the related classes/modules.
4 # This is used for realizing the scope of TextDomain.
5 # (see: http://www.yotabanana.com/hiki/ruby-gettext-scope.html)
6 module ClassInfo
7 extend self
8 # normalize the class name
9 # klass should kind of the class, not object.
10 def normalize_class(klass)
11 ret = (klass.kind_of? Module) ? klass : klass.class
12 if ret.name =~ /^\#<|^$/ or ret == GetText or ret.name.nil?
13 ret = Object
14 end
15 ret
16 end
17
18 def root_ancestors # :nodoc:
19 Object.ancestors
20 end
21
22 # Internal method for related_classes.
23 def related_classes_internal(klass, all_classes = [], analyzed_classes = [] )
24 ret = []
25 klass = normalize_class(klass)
26
27 return [Object] if root_ancestors.include? klass
28
29 ary = klass.name.split(/::/)
30 while(v = ary.shift)
31 ret.unshift(((ret.size == 0) ? Object.const_get(v) : ret[0].const_get(v)))
32 end
33 ret -= analyzed_classes
34 if ret.size > 1
35 ret += related_classes_internal(ret[1], all_classes, analyzed_classes)
36 ret.uniq!
37 end
38 analyzed_classes << klass unless analyzed_classes.include? klass
39
40 klass.ancestors.each do |a|
41 next if a == klass
42 ret += related_classes_internal(a, all_classes, analyzed_classes)
43 ret.uniq!
44 end
45
46 if all_classes.size > 0
47 (ret & all_classes).uniq
48 else
49 ret.uniq
50 end
51 end
52
53 # Returns the classes which related to klass
54 # (klass's ancestors, included modules and nested modules)
55 def related_classes(klass, all_classes = [])
56 ret = related_classes_internal(klass, all_classes)
57 unless ret.include? Object
58 ret += [Object]
59 end
60 ret
61 end
62 end
63 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 locale_path.rb - GetText::LocalePath
4
5 Copyright (C) 2001-2010 Masao Mutoh
6
7 You may redistribute it and/or modify it under the same
8 license terms as Ruby or LGPL.
9
10 =end
11
12 require 'rbconfig'
13
14 module GetText
15 # Treats locale-path for mo-files.
16 class LocalePath
17 # The default locale paths.
18 CONFIG_PREFIX = RbConfig::CONFIG['prefix'].gsub(/\/local/, "")
19 DEFAULT_RULES = [
20 "./locale/%{lang}/LC_MESSAGES/%{name}.mo",
21 "./locale/%{lang}/%{name}.mo",
22 "#{RbConfig::CONFIG['datadir']}/locale/%{lang}/LC_MESSAGES/%{name}.mo",
23 "#{RbConfig::CONFIG['datadir'].gsub(/\/local/, "")}/locale/%{lang}/LC_MESSAGES/%{name}.mo",
24 "#{CONFIG_PREFIX}/share/locale/%{lang}/LC_MESSAGES/%{name}.mo",
25 "#{CONFIG_PREFIX}/local/share/locale/%{lang}/LC_MESSAGES/%{name}.mo"
26 ].uniq
27
28 class << self
29 # Add default locale path. Usually you should use GetText.add_default_locale_path instead.
30 # * path: a new locale path. (e.g.) "/usr/share/locale/%{lang}/LC_MESSAGES/%{name}.mo"
31 # ('locale' => "ja_JP", 'name' => "textdomain")
32 # * Returns: the new DEFAULT_LOCALE_PATHS
33 def add_default_rule(path)
34 DEFAULT_RULES.unshift(path)
35 end
36
37 # Returns path rules as an Array.
38 # (e.g.) ["/usr/share/locale/%{lang}/LC_MESSAGES/%{name}.mo", ...]
39 def default_path_rules
40 default_path_rules = []
41
42 if ENV["GETTEXT_PATH"]
43 ENV["GETTEXT_PATH"].split(/,/).each {|i|
44 default_path_rules += ["#{i}/%{lang}/LC_MESSAGES/%{name}.mo", "#{i}/%{lang}/%{name}.mo"]
45 }
46 end
47 default_path_rules += DEFAULT_RULES
48
49 load_path = $LOAD_PATH.dup
50 load_path.map!{|v| v.match(/(.*?)(\/lib)*?$/); $1}
51 load_path.each {|path|
52 default_path_rules += [
53 "#{path}/data/locale/%{lang}/LC_MESSAGES/%{name}.mo",
54 "#{path}/data/locale/%{lang}/%{name}.mo",
55 "#{path}/locale/%{lang}/LC_MESSAGES/%{name}.mo",
56 "#{path}/locale/%{lang}/%{name}.mo",
57 ]
58 }
59 # paths existed only.
60 default_path_rules = default_path_rules.select{|path|
61 Dir.glob(path % {:lang => "*", :name => "*"}).size > 0}.uniq
62 default_path_rules
63 end
64 end
65
66 attr_reader :locale_paths, :supported_locales
67
68 # Creates a new GetText::TextDomain.
69 # * name: the textdomain name.
70 # * topdir: the locale path ("%{topdir}/%{lang}/LC_MESSAGES/%{name}.mo") or nil.
71 def initialize(name, topdir = nil)
72 @name = name
73
74 if topdir
75 path_rules = ["#{topdir}/%{lang}/LC_MESSAGES/%{name}.mo", "#{topdir}/%{lang}/%{name}.mo"]
76 else
77 path_rules = self.class.default_path_rules
78 end
79
80 @locale_paths = {}
81 path_rules.each do |rule|
82 this_path_rules = rule % {:lang => "([^\/]+)", :name => name}
83 Dir.glob(rule % {:lang => "*", :name => name}).each do |path|
84 if /#{this_path_rules}/ =~ path
85 @locale_paths[$1] = path.untaint unless @locale_paths[$1]
86 end
87 end
88 end
89 @supported_locales = @locale_paths.keys.sort
90 end
91
92 # Gets the current path.
93 # * lang: a Locale::Tag.
94 def current_path(lang)
95 lang_candidates = lang.to_posix.candidates
96
97 lang_candidates.each do |tag|
98 path = @locale_paths[tag.to_s]
99 warn "GetText::TextDomain#load_mo: mo-file is #{path}" if $DEBUG
100 return path if path
101 end
102
103 if $DEBUG
104 warn "MO file is not found in"
105 @locale_paths.each do |path|
106 warn " #{path[1]}"
107 end
108 end
109 nil
110 end
111 end
112 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 mo.rb - A simple class for operating GNU MO file.
4
5 Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
6 Copyright (C) 2003-2009 Masao Mutoh
7 Copyright (C) 2002 Masahiro Sakai, Masao Mutoh
8 Copyright (C) 2001 Masahiro Sakai
9
10 Masahiro Sakai <s01397ms at sfc.keio.ac.jp>
11 Masao Mutoh <mutomasa at gmail.com>
12
13 You can redistribute this file and/or modify it under the same term
14 of Ruby. License of Ruby is included with Ruby distribution in
15 the file "README".
16
17 =end
18
19 require 'stringio'
20
21 module GetText
22 class MO < Hash
23 class InvalidFormat < RuntimeError; end;
24
25 attr_reader :filename
26
27 Header = Struct.new(:magic,
28 :revision,
29 :nstrings,
30 :orig_table_offset,
31 :translated_table_offset,
32 :hash_table_size,
33 :hash_table_offset)
34
35 # The following are only used in .mo files
36 # with minor revision >= 1.
37 class HeaderRev1 < Header
38 attr_accessor :n_sysdep_segments,
39 :sysdep_segments_offset,
40 :n_sysdep_strings,
41 :orig_sysdep_tab_offset,
42 :trans_sysdep_tab_offset
43 end
44
45 MAGIC_BIG_ENDIAN = "\x95\x04\x12\xde".force_encoding("ASCII-8BIT")
46 MAGIC_LITTLE_ENDIAN = "\xde\x12\x04\x95".force_encoding("ASCII-8BIT")
47
48 def self.open(arg = nil, output_charset = nil)
49 result = self.new(output_charset)
50 result.load(arg)
51 end
52
53 def initialize(output_charset = nil)
54 @filename = nil
55 @last_modified = nil
56 @little_endian = true
57 @output_charset = output_charset
58 @plural_proc = nil
59 super()
60 end
61
62 def store(msgid, msgstr, options)
63 string = generate_original_string(msgid, options)
64 self[string] = msgstr
65 end
66
67 def update!
68 if FileTest.exist?(@filename)
69 st = File.stat(@filename)
70 load(@filename) unless (@last_modified == [st.ctime, st.mtime])
71 else
72 warn "#{@filename} was lost." if $DEBUG
73 clear
74 end
75 self
76 end
77
78 def load(arg)
79 if arg.kind_of? String
80 begin
81 st = File.stat(arg)
82 @last_modified = [st.ctime, st.mtime]
83 rescue Exception
84 end
85 load_from_file(arg)
86 else
87 load_from_stream(arg)
88 end
89 @filename = arg
90 self
91 end
92
93 def load_from_stream(io)
94 magic = io.read(4)
95 case magic
96 when MAGIC_BIG_ENDIAN
97 @little_endian = false
98 when MAGIC_LITTLE_ENDIAN
99 @little_endian = true
100 else
101 raise InvalidFormat.new(sprintf("Unknown signature %s", magic.dump))
102 end
103
104 endian_type6 = @little_endian ? 'V6' : 'N6'
105 endian_type_astr = @little_endian ? 'V*' : 'N*'
106
107 header = HeaderRev1.new(magic, *(io.read(4 * 6).unpack(endian_type6)))
108
109 if header.revision == 1
110 # FIXME: It doesn't support sysdep correctly.
111 header.n_sysdep_segments = io.read(4).unpack(endian_type6)
112 header.sysdep_segments_offset = io.read(4).unpack(endian_type6)
113 header.n_sysdep_strings = io.read(4).unpack(endian_type6)
114 header.orig_sysdep_tab_offset = io.read(4).unpack(endian_type6)
115 header.trans_sysdep_tab_offset = io.read(4).unpack(endian_type6)
116 elsif header.revision > 1
117 raise InvalidFormat.new(sprintf("file format revision %d isn't supported", header.revision))
118 end
119 io.pos = header.orig_table_offset
120 orig_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
121
122 io.pos = header.translated_table_offset
123 trans_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
124
125 msgids = Array.new(header.nstrings)
126 for i in 0...header.nstrings
127 io.pos = orig_table_data[i * 2 + 1]
128 msgids[i] = io.read(orig_table_data[i * 2 + 0])
129 end
130
131 clear
132 for i in 0...header.nstrings
133 io.pos = trans_table_data[i * 2 + 1]
134 msgstr = io.read(trans_table_data[i * 2 + 0])
135
136 msgid = msgids[i]
137 if msgid.nil? || msgid.empty?
138 if msgstr
139 @charset = nil
140 @nplurals = nil
141 @plural = nil
142 msgstr.each_line{|line|
143 if /^Content-Type:/i =~ line and /charset=((?:\w|-)+)/i =~ line
144 @charset = $1
145 elsif /^Plural-Forms:\s*nplurals\s*\=\s*(\d*);\s*plural\s*\=\s*([^;]*)\n?/ =~ line
146 @nplurals = $1
147 @plural = $2
148 end
149 break if @charset and @nplurals
150 }
151 @nplurals = "1" unless @nplurals
152 @plural = "0" unless @plural
153 end
154 else
155 unless msgstr.nil?
156 msgstr = convert_encoding(msgstr, msgid)
157 end
158 end
159 self[convert_encoding(msgid, msgid)] = msgstr.freeze
160 end
161 self
162 end
163
164 def prime?(number)
165 ('1' * number) !~ /^1?$|^(11+?)\1+$/
166 end
167
168 begin
169 require 'prime'
170 def next_prime(seed)
171 Prime.instance.find{|x| x > seed }
172 end
173 rescue LoadError
174 def next_prime(seed)
175 require 'mathn'
176 prime = Prime.new
177 while current = prime.succ
178 return current if current > seed
179 end
180 end
181 end
182
183 HASHWORDBITS = 32
184 # From gettext-0.12.1/gettext-runtime/intl/hash-string.h
185 # Defines the so called `hashpjw' function by P.J. Weinberger
186 # [see Aho/Sethi/Ullman, COMPILERS: Principles, Techniques and Tools,
187 # 1986, 1987 Bell Telephone Laboratories, Inc.]
188 def hash_string(str)
189 hval = 0
190 str.each_byte do |b|
191 break if b == '\0'
192 hval <<= 4
193 hval += b.to_i
194 g = hval & (0xf << (HASHWORDBITS - 4))
195 if (g != 0)
196 hval ^= g >> (HASHWORDBITS - 8)
197 hval ^= g
198 end
199 end
200 hval
201 end
202
203 #Save data as little endian format.
204 def save_to_stream(io)
205 # remove untranslated message
206 translated_messages = reject do |msgid, msgstr|
207 msgstr.nil?
208 end
209
210 size = translated_messages.size
211 header_size = 4 * 7
212 table_size = 4 * 2 * size
213
214 hash_table_size = next_prime((size * 4) / 3)
215 hash_table_size = 3 if hash_table_size <= 2
216 header = Header.new(
217 MAGIC_LITTLE_ENDIAN, # magic
218 0, # revision
219 size, # nstrings
220 header_size, # orig_table_offset
221 header_size + table_size, # translated_table_offset
222 hash_table_size, # hash_table_size
223 header_size + table_size * 2 # hash_table_offset
224 )
225 io.write(header.to_a.pack('a4V*'))
226
227 ary = translated_messages.to_a
228 ary.sort!{|a, b| a[0] <=> b[0]} # sort by original string
229
230 pos = header.hash_table_size * 4 + header.hash_table_offset
231
232 orig_table_data = Array.new()
233 ary.each{|item, _|
234 orig_table_data.push(item.bytesize)
235 orig_table_data.push(pos)
236 pos += item.bytesize + 1 # +1 is <NUL>
237 }
238 io.write(orig_table_data.pack('V*'))
239
240 trans_table_data = Array.new()
241 ary.each{|_, item|
242 trans_table_data.push(item.bytesize)
243 trans_table_data.push(pos)
244 pos += item.bytesize + 1 # +1 is <NUL>
245 }
246 io.write(trans_table_data.pack('V*'))
247
248 hash_tab = Array.new(hash_table_size)
249 j = 0
250 ary[0...size].each {|key, _|
251 hash_val = hash_string(key)
252 idx = hash_val % hash_table_size
253 if hash_tab[idx] != nil
254 incr = 1 + (hash_val % (hash_table_size - 2))
255 begin
256 if (idx >= hash_table_size - incr)
257 idx -= hash_table_size - incr
258 else
259 idx += incr
260 end
261 end until (hash_tab[idx] == nil)
262 end
263 hash_tab[idx] = j + 1
264 j += 1
265 }
266 hash_tab.collect!{|i| i ? i : 0}
267
268 io.write(hash_tab.pack('V*'))
269
270 ary.each{|item, _| io.write(item); io.write("\0") }
271 ary.each{|_, item| io.write(item); io.write("\0") }
272
273 self
274 end
275
276 def load_from_file(filename)
277 @filename = filename
278 begin
279 File.open(filename, 'rb'){|f| load_from_stream(f)}
280 rescue => e
281 e.set_backtrace("File: #{@filename}")
282 raise e
283 end
284 end
285
286 def save_to_file(filename)
287 File.open(filename, 'wb'){|f| save_to_stream(f)}
288 end
289
290 def set_comment(msgid_or_sym, comment)
291 #Do nothing
292 end
293
294 def plural_as_proc
295 unless @plural_proc
296 @plural_proc = Proc.new{|n| eval(@plural)}
297 begin
298 @plural_proc.call(1)
299 rescue
300 @plural_proc = Proc.new{|n| 0}
301 end
302 end
303 @plural_proc
304 end
305
306 attr_accessor :little_endian, :path, :last_modified
307 attr_reader :charset, :nplurals, :plural
308
309 private
310 def convert_encoding(string, original_string)
311 return string if @output_charset.nil? or @charset.nil?
312
313 if Encoding.find(@output_charset) == Encoding.find(@charset)
314 string.force_encoding(@output_charset)
315 return string
316 end
317
318 begin
319 string.encode(@output_charset, @charset)
320 rescue EncodingError
321 if $DEBUG
322 warn "@charset = ", @charset
323 warn "@output_charset = ", @output_charset
324 warn "msgid = ", original_string
325 warn "msgstr = ", string
326 end
327 string
328 end
329 end
330
331 def generate_original_string(msgid, options)
332 string = ""
333
334 msgctxt = options.delete(:msgctxt)
335 msgid_plural = options.delete(:msgid_plural)
336
337 string << msgctxt << "\004" unless msgctxt.nil?
338 string << msgid
339 string << "\000" << msgid_plural unless msgid_plural.nil?
340 string
341 end
342 end
343 end
344
345 # Test
346
347 if $0 == __FILE__
348 if (ARGV.include? "-h") or (ARGV.include? "--help")
349 STDERR.puts("mo.rb [filename.mo ...]")
350 exit
351 end
352
353 ARGV.each{ |item|
354 mo = GetText::MO.open(item)
355 puts "------------------------------------------------------------------"
356 puts "charset = \"#{mo.charset}\""
357 puts "nplurals = \"#{mo.nplurals}\""
358 puts "plural = \"#{mo.plural}\""
359 puts "------------------------------------------------------------------"
360 mo.each do |key, value|
361 puts "original message = #{key.inspect}"
362 puts "translated message = #{value.inspect}"
363 puts "--------------------------------------------------------------------"
364 end
365 }
366 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
4 #
5 # License: Ruby's or LGPL
6 #
7 # This library is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Lesser General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 require "gettext/po_entry"
21
22 module GetText
23
24 # PO stores PO entries like Hash. Each key of {POEntry} is msgctxt
25 # and msgid.
26 # PO[msgctxt, msgid] returns the {POEntry} containing msgctxt and
27 # msgid.
28 # If you specify msgid only, msgctxt is treated as nonexistent.
29 #
30 # @since 2.3.4
31 class PO
32 include Enumerable
33
34 class NonExistentEntryError < StandardError
35 end
36
37 # @!attribute [rw] order
38 # The order is used to sort PO entries(objects of {POEntry}) in
39 # {#to_s}.
40 # @param [:reference, :msgid] order (:reference) The sort key.
41 #
42 # Use `:reference` for sorting by location that message is placed.
43 #
44 # Use `:msgid` for sorting by msgid alphabetical order.
45 #
46 # `:references` is deprecated since 3.0.4. It will be removed
47 # at 4.0.0. Use `:reference` instead.
48 #
49 # @return [Symbol] the name as order by sort.
50 attr_accessor :order
51
52 def initialize(order=nil)
53 @order = order || :reference
54 @entries = {}
55 end
56
57 # Returns {POEntry} containing msgctxt and msgid.
58 # If you specify one argument, it is treated as msgid.
59 # @overload [](msgid)
60 # @!macro [new] po.[].argument
61 # @param [String] msgid msgid contained returning {POEntry}.
62 # @return [POEntry]
63 # @!macro po.[].argument
64 # @overload [](msgctxt, msgid)
65 # @param [String] msgctxt msgctxt contained returning {POEntry}.
66 # @!macro po.[].argument
67 def [](msgctxt, msgid=nil)
68 if msgid.nil?
69 msgid = msgctxt
70 msgctxt = nil
71 end
72
73 @entries[[msgctxt, msgid]]
74 end
75
76 # Stores {POEntry} or msgstr binding msgctxt and msgid. If you
77 # specify msgstr, this method creates {POEntry} containing it.
78 # If you specify the two argument, the first argument is treated
79 # as msgid.
80 #
81 # @overload []=(msgid, po_entry)
82 # @!macro [new] po.store.entry.arguments
83 # @param [String] msgid msgid binded {POEntry}.
84 # @param [POEntry] po_entry stored {POEntry}.
85 # @!macro po.store.entry.arguments
86 # @overload []=(msgctxt, msgid, po_entry)
87 # @param [String] msgctxt msgctxt binded {POEntry}.
88 # @!macro po.store.entry.arguments
89 # @overload []=(msgid, msgstr)
90 # @!macro [new] po.store.msgstr.arguments
91 # @param [String] msgid msgid binded {POEntry}.
92 # @param [String] msgstr msgstr contained {POEntry} stored PO.
93 # This {POEntry} is generated in this method.
94 # @!macro po.store.msgstr.arguments
95 # @overload []=(msgctxt, msgid, msgstr)
96 # @param [String] msgctxt msgctxt binded {POEntry}.
97 # @!macro po.store.msgstr.arguments
98 def []=(*arguments)
99 case arguments.size
100 when 2
101 msgctxt = nil
102 msgid = arguments[0]
103 value = arguments[1]
104 when 3
105 msgctxt = arguments[0]
106 msgid = arguments[1]
107 value = arguments[2]
108 else
109 raise(ArgumentError,
110 "[]=: wrong number of arguments(#{arguments.size} for 2..3)")
111 end
112
113 id = [msgctxt, msgid]
114 if value.instance_of?(POEntry)
115 @entries[id] = value
116 return(value)
117 end
118
119 msgstr = value
120 if @entries.has_key?(id)
121 entry = @entries[id]
122 else
123 if msgctxt.nil?
124 entry = POEntry.new(:normal)
125 else
126 entry = POEntry.new(:msgctxt)
127 end
128 @entries[id] = entry
129 end
130 entry.msgctxt = msgctxt
131 entry.msgid = msgid
132 entry.msgstr = msgstr
133 entry
134 end
135
136 # Returns if PO stores {POEntry} containing msgctxt and msgid.
137 # If you specify one argument, it is treated as msgid and msgctxt
138 # is nil.
139 #
140 # @overload has_key?(msgid)
141 # @!macro [new] po.has_key?.arguments
142 # @param [String] msgid msgid contained {POEntry} checked if it be
143 # stored PO.
144 # @!macro po.has_key?.arguments
145 # @overload has_key?(msgctxt, msgid)
146 # @param [String] msgctxt msgctxt contained {POEntry} checked if
147 # it be stored PO.
148 # @!macro po.has_key?.arguments
149 def has_key?(*arguments)
150 case arguments.size
151 when 1
152 msgctxt = nil
153 msgid = arguments[0]
154 when 2
155 msgctxt = arguments[0]
156 msgid = arguments[1]
157 else
158 message = "has_key?: wrong number of arguments " +
159 "(#{arguments.size} for 1..2)"
160 raise(ArgumentError, message)
161 end
162 id = [msgctxt, msgid]
163 @entries.has_key?(id)
164 end
165
166 # Calls block once for each {POEntry} as a block parameter.
167 # @overload each(&block)
168 # @yield [entry]
169 # @yieldparam [POEntry] entry {POEntry} in PO.
170 # @overload each
171 # @return [Enumerator] Returns Enumerator for {POEntry}.
172 def each(&block)
173 @entries.each_value(&block)
174 end
175
176 # @return [Bool] `true` if there is no entry, `false` otherwise.
177 def empty?
178 @entries.empty?
179 end
180
181 # For {PoParer}.
182 def set_comment(msgid, comment, msgctxt=nil)
183 id = [msgctxt, msgid]
184 self[*id] = nil unless @entries.has_key?(id)
185 self[*id].comment = comment
186 end
187
188 # Formats each {POEntry} to the format of PO files and returns joined
189 # them.
190 # @see http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files
191 # The description for Format of PO in GNU gettext manual
192 # @param (see POEntry#to_s)
193 # @return [String] Formatted and joined PO entries. It is used for
194 # creating .po files.
195 def to_s(options={})
196 po_string = ""
197
198 header_entry = @entries[[nil, ""]]
199 unless header_entry.nil?
200 po_string << header_entry.to_s(options.merge(:max_line_width => nil))
201 end
202
203 content_entries = @entries.reject do |(_, msgid), _|
204 msgid == :last or msgid.empty?
205 end
206
207 sort(content_entries).each do |msgid, entry|
208 po_string << "\n" unless po_string.empty?
209 po_string << entry.to_s(options)
210 end
211
212 if @entries.has_key?([nil, :last])
213 po_string << "\n" unless po_string.empty?
214 po_string << @entries[[nil, :last]].to_s(options)
215 end
216
217 po_string
218 end
219
220 private
221 def sort(entries)
222 case @order
223 when :reference, :references # :references is deprecated.
224 sorted_entries = sort_by_reference(entries)
225 when :msgid
226 sorted_entries = sort_by_msgid(entries)
227 else
228 sorted_entries = entries.to_a
229 end
230 end
231
232 def sort_by_reference(entries)
233 entries.each do |_, entry|
234 entry.references = entry.references.sort do |reference, other|
235 compare_reference(reference, other)
236 end
237 end
238
239 entries.sort do |msgid_entry, other_msgid_entry|
240 # msgid_entry = [[msgctxt, msgid], POEntry]
241 entry_first_reference = msgid_entry[1].references.first
242 other_first_reference = other_msgid_entry[1].references.first
243 compare_reference(entry_first_reference, other_first_reference)
244 end
245 end
246
247 def compare_reference(reference, other)
248 entry_source, entry_line_number = split_reference(reference)
249 other_source, other_line_number = split_reference(other)
250
251 if entry_source != other_source
252 entry_source <=> other_source
253 else
254 entry_line_number <=> other_line_number
255 end
256 end
257
258 def split_reference(reference)
259 return ["", -1] if reference.nil?
260 if /\A(.+):(\d+?)\z/ =~ reference
261 [$1, $2.to_i]
262 else
263 [reference, -1]
264 end
265 end
266
267 def sort_by_msgid(entries)
268 entries.sort_by do |msgid_entry|
269 # msgid_entry = [[msgctxt, msgid], POEntry]
270 msgid_entry[0]
271 end
272 end
273 end
274 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2010 masone (Christian Felder) <ema@rh-productions.ch>
4 # Copyright (C) 2009 Masao Mutoh
5 #
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require "gettext/po_format"
22
23 module GetText
24 class ParseError < StandardError
25 end
26
27 # Contains data related to the expression or sentence that
28 # is to be translated.
29 class POEntry
30 class InvalidTypeError < StandardError
31 end
32
33 class NoMsgidError < StandardError
34 end
35
36 class NoMsgctxtError < StandardError
37 end
38
39 class NoMsgidPluralError < StandardError
40 end
41
42 PARAMS = {
43 :normal => [:msgid, :separator, :msgstr],
44 :plural => [:msgid, :msgid_plural, :separator, :msgstr],
45 :msgctxt => [:msgctxt, :msgid, :msgstr],
46 :msgctxt_plural => [:msgctxt, :msgid, :msgid_plural, :msgstr]
47 }
48
49 # Required
50 attr_reader :type # :normal, :plural, :msgctxt, :msgctxt_plural
51 attr_accessor :msgid
52 attr_accessor :msgstr
53 # Options
54 attr_accessor :msgid_plural
55 attr_accessor :separator
56 attr_accessor :msgctxt
57 attr_accessor :references # ["file1:line1", "file2:line2", ...]
58 attr_accessor :translator_comment
59 attr_accessor :extracted_comment
60 # @return [Array<String>] The flags for this PO entry.
61 # @since 3.0.4
62 attr_accessor :flags
63 attr_accessor :previous
64 attr_accessor :comment
65
66 # Create the object. +type+ should be :normal, :plural, :msgctxt or :msgctxt_plural.
67 def initialize(type)
68 self.type = type
69 @translator_comment = nil
70 @extracted_comment = nil
71 @references = []
72 @flags = []
73 @previous = nil
74 @msgctxt = nil
75 @msgid = nil
76 @msgid_plural = nil
77 @msgstr = nil
78 end
79
80 # Support for extracted comments. Explanation s.
81 # http://www.gnu.org/software/gettext/manual/gettext.html#Names
82 # @return [void]
83 def add_comment(new_comment)
84 if (new_comment and ! new_comment.empty?)
85 @extracted_comment ||= ""
86 @extracted_comment << "\n" unless @extracted_comment.empty?
87 @extracted_comment << new_comment
88 end
89 end
90
91 # @return [String, nil] The flag of the PO entry.
92 # @deprecated Since 3.0.4. Use {#flags} instead.
93 def flag
94 @flags.first
95 end
96
97 # Set the new flag for the PO entry.
98 #
99 # @param [String, nil] flag The new flag.
100 # @deprecated Since 3.0.4. Use {#flags=} instead.
101 def flag=(flag)
102 if flag.nil?
103 @flags = []
104 else
105 @flags = [flag]
106 end
107 end
108
109 # Checks if the self has same attributes as other.
110 def ==(other)
111 not other.nil? and
112 type == other.type and
113 msgid == other.msgid and
114 msgstr == other.msgstr and
115 msgid_plural == other.msgid_plural and
116 separator == other.separator and
117 msgctxt == other.msgctxt and
118 translator_comment == other.translator_comment and
119 extracted_comment == other.extracted_comment and
120 references == other.references and
121 flags == other.flags and
122 previous == other.previous and
123 comment == other.comment
124 end
125
126 def type=(type)
127 unless PARAMS.has_key?(type)
128 raise(InvalidTypeError, "\"%s\" is invalid type." % type)
129 end
130 @type = type
131 @param_type = PARAMS[@type]
132 end
133
134 # Checks if the other translation target is mergeable with
135 # the current one. Relevant are msgid and translation context (msgctxt).
136 def mergeable?(other)
137 other && other.msgid == self.msgid && other.msgctxt == self.msgctxt
138 end
139
140 # Merges two translation targets with the same msgid and returns the merged
141 # result. If one is declared as plural and the other not, then the one
142 # with the plural wins.
143 def merge(other)
144 return self unless other
145 unless mergeable?(other)
146 message = "Translation targets do not match: \n" +
147 " self: #{self.inspect}\n other: '#{other.inspect}'"
148 raise ParseError, message
149 end
150 if other.msgid_plural && !msgid_plural
151 res = other
152 unless res.references.include?(references[0])
153 res.references += references
154 res.add_comment(extracted_comment)
155 end
156 else
157 res = self
158 unless res.references.include?(other.references[0])
159 res.references += other.references
160 res.add_comment(other.extracted_comment)
161 end
162 end
163 res
164 end
165
166 # Format the po entry in PO format.
167 #
168 # @param [Hash] options
169 # @option options (see Formatter#initialize)
170 def to_s(options={})
171 raise(NoMsgidError, "msgid is nil.") unless @msgid
172
173 formatter = Formatter.new(self, options)
174 formatter.format
175 end
176
177 # Returns true if the type is kind of msgctxt.
178 def msgctxt?
179 [:msgctxt, :msgctxt_plural].include?(@type)
180 end
181
182 # Returns true if the type is kind of plural.
183 def plural?
184 [:plural, :msgctxt_plural].include?(@type)
185 end
186
187 # @return true if the entry is header entry, false otherwise.
188 # Header entry is normal type and has empty msgid.
189 def header?
190 @type == :normal and @msgid == ""
191 end
192
193 # @return true if the entry is obsolete entry, false otherwise.
194 # Obsolete entry is normal type and has :last msgid.
195 def obsolete?
196 @type == :normal and @msgid == :last
197 end
198
199 # @return true if the entry is fuzzy entry, false otherwise.
200 # Fuzzy entry has "fuzzy" flag.
201 def fuzzy?
202 @flags.include?("fuzzy")
203 end
204
205 # @return true if the entry is translated entry, false otherwise.
206 def translated?
207 return false if fuzzy?
208 return false if @msgstr.nil? or @msgstr.empty?
209 true
210 end
211
212 def [](number)
213 param = @param_type[number]
214 raise ParseError, 'no more string parameters expected' unless param
215 send param
216 end
217
218 private
219
220 # sets or extends the value of a translation target params like msgid,
221 # msgctxt etc.
222 # param is symbol with the name of param
223 # value - new value
224 def set_value(param, value)
225 send "#{param}=", (send(param) || '') + value
226 end
227
228 class Formatter
229 class << self
230 def escape(string)
231 return "" if string.nil?
232
233 string.gsub(/([\\"\t\n])/) do
234 special_character = $1
235 case special_character
236 when "\t"
237 "\\t"
238 when "\n"
239 "\\n"
240 else
241 "\\#{special_character}"
242 end
243 end
244 end
245 end
246
247 include POFormat
248
249 DEFAULT_MAX_LINE_WIDTH = 78
250
251 # @param [POEntry] entry The entry to be formatted.
252 # @param [Hash] options
253 # @option options [Bool] :include_translator_comment (true)
254 # Includes translator comments in formatted string if true.
255 # @option options [Bool] :include_extracted_comment (true)
256 # Includes extracted comments in formatted string if true.
257 # @option options [Bool] :include_reference_comment (true)
258 # Includes reference comments in formatted string if true.
259 # @option options [Bool] :include_flag_comment (true)
260 # Includes flag comments in formatted string if true.
261 # @option options [Bool] :include_previous_comment (true)
262 # Includes previous comments in formatted string if true.
263 # @option options [Bool] :include_all_comments (true)
264 # Includes all comments in formatted string if true.
265 # Other specific `:include_XXX` options get preference over
266 # this option.
267 # You can remove all comments by specifying this option as
268 # false and omitting other `:include_XXX` options.
269 # @option options [Integer] :max_line_width (78)
270 # Wraps long lines that is longer than the `:max_line_width`.
271 # Don't break long lines if `:max_line_width` is less than 0
272 # such as `-1`.
273 # @option options [Encoding] :encoding (nil)
274 # Encodes to the specific encoding.
275 def initialize(entry, options={})
276 @entry = entry
277 @options = normalize_options(options)
278 end
279
280 def format
281 if @entry.obsolete?
282 return format_obsolete_comment(@entry.comment)
283 end
284
285 str = format_comments
286
287 # msgctxt, msgid, msgstr
288 if @entry.msgctxt?
289 if @entry.msgctxt.nil?
290 no_msgctxt_message = "This POEntry is a kind of msgctxt " +
291 "but the msgctxt property is nil. " +
292 "msgid: #{@entry.msgid}"
293 raise(NoMsgctxtError, no_msgctxt_message)
294 end
295 str << "msgctxt " << format_message(@entry.msgctxt)
296 end
297
298 str << "msgid " << format_message(@entry.msgid)
299 if @entry.plural?
300 if @entry.msgid_plural.nil?
301 no_plural_message = "This POEntry is a kind of plural " +
302 "but the msgid_plural property is nil. " +
303 "msgid: #{@entry.msgid}"
304 raise(NoMsgidPluralError, no_plural_message)
305 end
306
307 str << "msgid_plural " << format_message(@entry.msgid_plural)
308
309 if @entry.msgstr.nil?
310 str << "msgstr[0] \"\"\n"
311 str << "msgstr[1] \"\"\n"
312 else
313 msgstrs = @entry.msgstr.split("\000", -1)
314 msgstrs.each_with_index do |msgstr, index|
315 str << "msgstr[#{index}] " << format_message(msgstr)
316 end
317 end
318 else
319 str << "msgstr "
320 str << format_message(@entry.msgstr)
321 end
322
323 encode(str)
324 end
325
326 private
327 def normalize_options(options)
328 options = options.dup
329 include_comment_keys = [
330 :include_translator_comment,
331 :include_extracted_comment,
332 :include_reference_comment,
333 :include_flag_comment,
334 :include_previous_comment,
335 ]
336 if options[:include_all_comments].nil?
337 options[:include_all_comments] = true
338 end
339 default_include_comment_value = options[:include_all_comments]
340 include_comment_keys.each do |key|
341 options[key] = default_include_comment_value if options[key].nil?
342 end
343 options[:max_line_width] ||= DEFAULT_MAX_LINE_WIDTH
344 options
345 end
346
347 def include_translator_comment?
348 @options[:include_translator_comment]
349 end
350
351 def include_extracted_comment?
352 @options[:include_extracted_comment]
353 end
354
355 def include_reference_comment?
356 @options[:include_reference_comment]
357 end
358
359 def include_flag_comment?
360 @options[:include_flag_comment]
361 end
362
363 def include_previous_comment?
364 @options[:include_previous_comment]
365 end
366
367 def format_comments
368 formatted_comment = ""
369 if include_translator_comment?
370 formatted_comment << format_translator_comment
371 end
372 if include_extracted_comment?
373 formatted_comment << format_extracted_comment
374 end
375 if include_reference_comment?
376 formatted_comment << format_reference_comment
377 end
378 if include_flag_comment?
379 formatted_comment << format_flag_comment
380 end
381 if include_previous_comment?
382 formatted_comment << format_previous_comment
383 end
384 formatted_comment
385 end
386
387 def format_translator_comment
388 format_comment("#", @entry.translator_comment)
389 end
390
391 def format_extracted_comment
392 format_comment(EXTRACTED_COMMENT_MARK, @entry.extracted_comment)
393 end
394
395 def format_reference_comment
396 max_line_width = @options[:max_line_width]
397 formatted_reference = ""
398 if not @entry.references.nil? and not @entry.references.empty?
399 formatted_reference << REFERENCE_COMMENT_MARK
400 line_width = 2
401 @entry.references.each do |reference|
402 if max_line_width > 0 and
403 line_width + reference.size > max_line_width
404 formatted_reference << "\n"
405 formatted_reference << "#{REFERENCE_COMMENT_MARK} #{reference}"
406 line_width = 3 + reference.size
407 else
408 formatted_reference << " #{reference}"
409 line_width += 1 + reference.size
410 end
411 end
412
413 formatted_reference << "\n"
414 end
415 formatted_reference
416 end
417
418 def format_flag_comment
419 formatted_flags = ""
420 @entry.flags.each do |flag|
421 formatted_flags << format_comment(FLAG_MARK, flag)
422 end
423 formatted_flags
424 end
425
426 def format_previous_comment
427 format_comment(PREVIOUS_COMMENT_MARK, @entry.previous)
428 end
429
430 def format_comment(mark, comment)
431 return "" if comment.nil?
432
433 formatted_comment = ""
434 comment.each_line do |comment_line|
435 if comment_line == "\n"
436 formatted_comment << "#{mark}\n"
437 else
438 formatted_comment << "#{mark} #{comment_line.strip}\n"
439 end
440 end
441 formatted_comment
442 end
443
444 def format_obsolete_comment(comment)
445 mark = "#~"
446 return "" if comment.nil?
447
448 formatted_comment = ""
449 comment.each_line do |comment_line|
450 if /\A#[^~]/ =~ comment_line or comment_line.start_with?(mark)
451 formatted_comment << "#{comment_line.chomp}\n"
452 elsif comment_line == "\n"
453 formatted_comment << "\n"
454 else
455 formatted_comment << "#{mark} #{comment_line.strip}\n"
456 end
457 end
458 formatted_comment
459 end
460
461 def format_message(message)
462 empty_formatted_message = "\"\"\n"
463 return empty_formatted_message if message.nil?
464
465 chunks = wrap_message(message)
466 return empty_formatted_message if chunks.empty?
467
468 formatted_message = ""
469 if chunks.size > 1 or chunks.first.end_with?("\n")
470 formatted_message << empty_formatted_message
471 end
472 chunks.each do |chunk|
473 formatted_message << "\"#{escape(chunk)}\"\n"
474 end
475 formatted_message
476 end
477
478 def escape(string)
479 self.class.escape(string)
480 end
481
482 def wrap_message(message)
483 return [message] if message.empty?
484
485 max_line_width = @options[:max_line_width]
486
487 chunks = []
488 message.each_line do |line|
489 if max_line_width <= 0
490 chunks << line
491 else
492 # TODO: use character width instead of the number of characters
493 line.scan(/.{1,#{max_line_width}}/m) do |chunk|
494 chunks << chunk
495 end
496 end
497 end
498 chunks
499 end
500
501 def encode(string)
502 encoding = @options[:encoding]
503 return string if encoding.nil?
504 string.encode(encoding)
505 end
506 end
507 end
508 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
3 #
4 # License: Ruby's or LGPL
5 #
6 # This library is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 module GetText
20 module POFormat
21 TRANSLATOR_COMMENT_MARK = "# "
22 EXTRACTED_COMMENT_MARK = "#."
23 FLAG_MARK = "#,"
24 PREVIOUS_COMMENT_MARK = "#|"
25 REFERENCE_COMMENT_MARK = "#:"
26 end
27 end
0 # -*- coding: utf-8 -*-
1 #
2 # po_parser.rb - Generate a .mo
3 #
4 # Copyright (C) 2003-2009 Masao Mutoh <mutomasa at gmail.com>
5 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
6 #
7 # You may redistribute it and/or modify it under the same
8 # license terms as Ruby or LGPL.
9
10 #
11 # DO NOT MODIFY!!!!
12 # This file is automatically generated by Racc 1.4.11
13 # from Racc grammer file "".
14 #
15
16 require 'racc/parser.rb'
17
18 require "gettext/po"
19
20 module GetText
21 class POParser < Racc::Parser
22
23 module_eval(<<'...end po_parser.ry/module_eval...', 'po_parser.ry', 123)
24 if GetText.respond_to?(:bindtextdomain)
25 include GetText
26 GetText.bindtextdomain("gettext")
27 else
28 def _(message_id)
29 message_id
30 end
31 private :_
32 end
33
34 attr_writer :ignore_fuzzy, :report_warning
35 def initialize
36 @ignore_fuzzy = true
37 @report_warning = true
38 end
39
40 def ignore_fuzzy?
41 @ignore_fuzzy
42 end
43
44 def report_warning?
45 @report_warning
46 end
47
48 def unescape(orig)
49 ret = orig.gsub(/\\n/, "\n")
50 ret.gsub!(/\\t/, "\t")
51 ret.gsub!(/\\r/, "\r")
52 ret.gsub!(/\\"/, "\"")
53 ret
54 end
55 private :unescape
56
57 def unescape_string(string)
58 string.gsub(/\\\\/, "\\")
59 end
60 private :unescape_string
61
62 def parse(str, data)
63 @translator_comments = []
64 @extracted_comments = []
65 @references = []
66 @flags = []
67 @previous = []
68 @comments = []
69 @data = data
70 @fuzzy = false
71 @msgctxt = nil
72 @msgid_plural = nil
73
74 str.strip!
75 @q = []
76 until str.empty? do
77 case str
78 when /\A\s+/
79 str = $'
80 when /\Amsgctxt/
81 @q.push [:MSGCTXT, $&]
82 str = $'
83 when /\Amsgid_plural/
84 @q.push [:MSGID_PLURAL, $&]
85 str = $'
86 when /\Amsgid/
87 @q.push [:MSGID, $&]
88 str = $'
89 when /\Amsgstr/
90 @q.push [:MSGSTR, $&]
91 str = $'
92 when /\A\[(\d+)\]/
93 @q.push [:PLURAL_NUM, $1]
94 str = $'
95 when /\A\#~(.*)/
96 if report_warning?
97 $stderr.print _("Warning: obsolete msgid exists.\n")
98 $stderr.print " #{$&}\n"
99 end
100 @q.push [:COMMENT, $&]
101 str = $'
102 when /\A\#(.*)/
103 @q.push [:COMMENT, $&]
104 str = $'
105 when /\A\"(.*)\"/
106 @q.push [:STRING, unescape_string($1)]
107 str = $'
108 else
109 #c = str[0,1]
110 #@q.push [:STRING, c]
111 str = str[1..-1]
112 end
113 end
114 @q.push [false, "$end"]
115 if $DEBUG
116 @q.each do |a,b|
117 puts "[#{a}, #{b}]"
118 end
119 end
120 @yydebug = true if $DEBUG
121 do_parse
122
123 if @comments.size > 0
124 @data.set_comment(:last, @comments.join("\n"))
125 end
126 @data
127 end
128
129 def next_token
130 @q.shift
131 end
132
133 def on_message(msgid, msgstr)
134 msgstr = nil if msgstr.empty?
135
136 if @data.instance_of?(PO)
137 type = detect_entry_type
138 entry = POEntry.new(type)
139 entry.translator_comment = format_comment(@translator_comments)
140 entry.extracted_comment = format_comment(@extracted_comments)
141 entry.flags = @flags
142 entry.previous = format_comment(@previous)
143 entry.references = @references
144 entry.msgctxt = @msgctxt
145 entry.msgid = msgid
146 entry.msgid_plural = @msgid_plural
147 entry.msgstr = msgstr
148
149 @data[@msgctxt, msgid] = entry
150 else
151 options = {}
152 options[:msgctxt] = @msgctxt
153 options[:msgid_plural] = @msgid_plural
154 @data.store(msgid, msgstr, options)
155 @data.set_comment(msgid, format_comment(@comments))
156 end
157
158 @translator_comments = []
159 @extracted_comments = []
160 @references = []
161 @flags = []
162 @previous = []
163 @references = []
164 @comments.clear
165 @msgctxt = nil
166 @msgid_plural = nil
167 end
168
169 def format_comment(comments)
170 return "" if comments.empty?
171
172 comment = comments.join("\n")
173 comment << "\n" if comments.last.empty?
174 comment
175 end
176
177 def on_comment(comment)
178 @fuzzy = true if (/fuzzy/ =~ comment)
179 if @data.instance_of?(PO)
180 if comment == "#"
181 @translator_comments << ""
182 elsif /\A(#.)\s*(.*)\z/ =~ comment
183 mark = $1
184 content = $2
185 case mark
186 when POFormat::TRANSLATOR_COMMENT_MARK
187 @translator_comments << content
188 when POFormat::EXTRACTED_COMMENT_MARK
189 @extracted_comments << content
190 when POFormat::REFERENCE_COMMENT_MARK
191 @references.concat(parse_references_line(content))
192 when POFormat::FLAG_MARK
193 @flags.concat(parse_flags_line(content))
194 when POFormat::PREVIOUS_COMMENT_MARK
195 @previous << content
196 else
197 @comments << comment
198 end
199 end
200 else
201 @comments << comment
202 end
203 end
204
205 def parse_file(po_file, data)
206 args = [ po_file ]
207 # In Ruby 1.9, we must detect proper encoding of a PO file.
208 if String.instance_methods.include?(:encode)
209 encoding = detect_file_encoding(po_file)
210 args << "r:#{encoding}"
211 end
212 @po_file = po_file
213 parse(File.open(*args) {|io| io.read }, data)
214 end
215
216 private
217 def detect_file_encoding(po_file)
218 open(po_file, :encoding => "ASCII-8BIT") do |input|
219 in_header = false
220 input.each_line do |line|
221 case line.chomp
222 when /\Amsgid\s+"(.*)"\z/
223 id = $1
224 break unless id.empty?
225 in_header = true
226 when /\A"Content-Type:.*\scharset=(.*)\\n"\z/
227 charset = $1
228 next unless in_header
229 break if template_charset?(charset)
230 return Encoding.find(charset)
231 end
232 end
233 end
234 Encoding.default_external
235 end
236
237 def template_charset?(charset)
238 charset == "CHARSET"
239 end
240
241 def detect_entry_type
242 if @msgctxt.nil?
243 if @msgid_plural.nil?
244 :normal
245 else
246 :plural
247 end
248 else
249 if @msgid_plural.nil?
250 :msgctxt
251 else
252 :msgctxt_plural
253 end
254 end
255 end
256
257 def parse_references_line(line)
258 line.split(/\s+/)
259 end
260
261 def parse_flags_line(line)
262 line.split(/\s+/)
263 end
264 ...end po_parser.ry/module_eval...
265 ##### State transition tables begin ###
266
267 racc_action_table = [
268 2, 13, 10, 9, 6, 17, 16, 15, 22, 15,
269 15, 13, 13, 13, 15, 11, 22, 24, 13, 15 ]
270
271 racc_action_check = [
272 1, 17, 1, 1, 1, 14, 14, 14, 19, 19,
273 12, 6, 16, 9, 18, 2, 20, 22, 24, 25 ]
274
275 racc_action_pointer = [
276 nil, 0, 15, nil, nil, nil, 4, nil, nil, 6,
277 nil, nil, 3, nil, 0, nil, 5, -6, 7, 2,
278 10, nil, 9, nil, 11, 12 ]
279
280 racc_action_default = [
281 -1, -16, -16, -2, -3, -4, -16, -6, -7, -16,
282 -13, 26, -5, -15, -16, -14, -16, -16, -8, -16,
283 -9, -11, -16, -10, -16, -12 ]
284
285 racc_goto_table = [
286 12, 21, 23, 14, 4, 5, 3, 7, 8, 20,
287 18, 19, 1, nil, nil, nil, nil, nil, 25 ]
288
289 racc_goto_check = [
290 5, 9, 9, 5, 3, 4, 2, 6, 7, 8,
291 5, 5, 1, nil, nil, nil, nil, nil, 5 ]
292
293 racc_goto_pointer = [
294 nil, 12, 5, 3, 4, -6, 6, 7, -10, -18 ]
295
296 racc_goto_default = [
297 nil, nil, nil, nil, nil, nil, nil, nil, nil, nil ]
298
299 racc_reduce_table = [
300 0, 0, :racc_error,
301 0, 10, :_reduce_none,
302 2, 10, :_reduce_none,
303 2, 10, :_reduce_none,
304 2, 10, :_reduce_none,
305 2, 12, :_reduce_5,
306 1, 13, :_reduce_none,
307 1, 13, :_reduce_none,
308 4, 15, :_reduce_8,
309 5, 16, :_reduce_9,
310 2, 17, :_reduce_10,
311 1, 17, :_reduce_none,
312 3, 18, :_reduce_12,
313 1, 11, :_reduce_13,
314 2, 14, :_reduce_14,
315 1, 14, :_reduce_15 ]
316
317 racc_reduce_n = 16
318
319 racc_shift_n = 26
320
321 racc_token_table = {
322 false => 0,
323 :error => 1,
324 :COMMENT => 2,
325 :MSGID => 3,
326 :MSGCTXT => 4,
327 :MSGID_PLURAL => 5,
328 :MSGSTR => 6,
329 :STRING => 7,
330 :PLURAL_NUM => 8 }
331
332 racc_nt_base = 9
333
334 racc_use_result_var = true
335
336 Racc_arg = [
337 racc_action_table,
338 racc_action_check,
339 racc_action_default,
340 racc_action_pointer,
341 racc_goto_table,
342 racc_goto_check,
343 racc_goto_default,
344 racc_goto_pointer,
345 racc_nt_base,
346 racc_reduce_table,
347 racc_token_table,
348 racc_shift_n,
349 racc_reduce_n,
350 racc_use_result_var ]
351
352 Racc_token_to_s_table = [
353 "$end",
354 "error",
355 "COMMENT",
356 "MSGID",
357 "MSGCTXT",
358 "MSGID_PLURAL",
359 "MSGSTR",
360 "STRING",
361 "PLURAL_NUM",
362 "$start",
363 "msgfmt",
364 "comment",
365 "msgctxt",
366 "message",
367 "string_list",
368 "single_message",
369 "plural_message",
370 "msgstr_plural",
371 "msgstr_plural_line" ]
372
373 Racc_debug_parser = true
374
375 ##### State transition tables end #####
376
377 # reduce 0 omitted
378
379 # reduce 1 omitted
380
381 # reduce 2 omitted
382
383 # reduce 3 omitted
384
385 # reduce 4 omitted
386
387 module_eval(<<'.,.,', 'po_parser.ry', 26)
388 def _reduce_5(val, _values, result)
389 @msgctxt = unescape(val[1])
390
391 result
392 end
393 .,.,
394
395 # reduce 6 omitted
396
397 # reduce 7 omitted
398
399 module_eval(<<'.,.,', 'po_parser.ry', 38)
400 def _reduce_8(val, _values, result)
401 msgid_raw = val[1]
402 msgid = unescape(msgid_raw)
403 msgstr = unescape(val[3])
404 use_message_p = true
405 if @fuzzy and not msgid.empty?
406 use_message_p = (not ignore_fuzzy?)
407 if report_warning?
408 if ignore_fuzzy?
409 $stderr.print _("Warning: fuzzy message was ignored.\n")
410 else
411 $stderr.print _("Warning: fuzzy message was used.\n")
412 end
413 $stderr.print " #{@po_file}: msgid '#{msgid_raw}'\n"
414 end
415 end
416 @fuzzy = false
417 on_message(msgid, msgstr) if use_message_p
418 result = ""
419
420 result
421 end
422 .,.,
423
424 module_eval(<<'.,.,', 'po_parser.ry', 61)
425 def _reduce_9(val, _values, result)
426 if @fuzzy and ignore_fuzzy?
427 if val[1] != ""
428 if report_warning?
429 $stderr.print _("Warning: fuzzy message was ignored.\n")
430 $stderr.print "msgid = '#{val[1]}\n"
431 end
432 else
433 on_message("", unescape(val[3]))
434 end
435 @fuzzy = false
436 else
437 @msgid_plural = unescape(val[3])
438 on_message(unescape(val[1]), unescape(val[4]))
439 end
440 result = ""
441
442 result
443 end
444 .,.,
445
446 module_eval(<<'.,.,', 'po_parser.ry', 82)
447 def _reduce_10(val, _values, result)
448 if val[0].size > 0
449 result = val[0] + "\000" + val[1]
450 else
451 result = ""
452 end
453
454 result
455 end
456 .,.,
457
458 # reduce 11 omitted
459
460 module_eval(<<'.,.,', 'po_parser.ry', 94)
461 def _reduce_12(val, _values, result)
462 result = val[2]
463
464 result
465 end
466 .,.,
467
468 module_eval(<<'.,.,', 'po_parser.ry', 101)
469 def _reduce_13(val, _values, result)
470 on_comment(val[0])
471
472 result
473 end
474 .,.,
475
476 module_eval(<<'.,.,', 'po_parser.ry', 109)
477 def _reduce_14(val, _values, result)
478 result = val.delete_if{|item| item == ""}.join
479
480 result
481 end
482 .,.,
483
484 module_eval(<<'.,.,', 'po_parser.ry', 113)
485 def _reduce_15(val, _values, result)
486 result = val[0]
487
488 result
489 end
490 .,.,
491
492 def _reduce_none(val, _values, result)
493 val[0]
494 end
495
496 end # class POParser
497 end # module GetText
498
499
0 # -*- coding: utf-8 -*-
1
2 =begin
3 text_domain.rb - GetText::TextDomain
4
5 Copyright (C) 2001-2009 Masao Mutoh
6 Copyright (C) 2001-2003 Masahiro Sakai
7
8 Masahiro Sakai <s01397ms@sfc.keio.ac.jp>
9 Masao Mutoh <mutomasa at gmail.com>
10
11 You may redistribute it and/or modify it under the same
12 license terms as Ruby or LGPL.
13 =end
14
15 require 'gettext/mo'
16 require 'gettext/locale_path'
17
18 module GetText
19 # GetText::TextDomain class manages mo-files of a text domain.
20 #
21 # Usually, you don't need to use this class directly.
22 #
23 # Notice: This class is unstable. APIs will be changed.
24 class TextDomain
25
26 attr_reader :output_charset
27 attr_reader :mofiles
28 attr_reader :name
29
30 @@cached = ! $DEBUG
31 # Cache the mo-file or not.
32 # Default is true. If $DEBUG is set then false.
33 def self.cached?
34 @@cached
35 end
36
37 # Set to cache the mo-file or not.
38 # * val: true if cached, otherwise false.
39 def self.cached=(val)
40 @@cached = val
41 end
42
43 # Creates a new GetText::TextDomain.
44 # * name: the text domain name.
45 # * topdir: the locale path ("%{topdir}/%{lang}/LC_MESSAGES/%{name}.mo") or nil.
46 # * output_charset: output charset.
47 # * Returns: a newly created GetText::TextDomain object.
48 def initialize(name, topdir = nil, output_charset = nil)
49 @name, @output_charset = name, output_charset
50
51 @locale_path = LocalePath.new(@name, topdir)
52 @mofiles = {}
53 end
54
55 # Translates the translated string.
56 # * lang: Locale::Tag::Simple's subclass.
57 # * msgid: the original message.
58 # * Returns: the translated string or nil.
59 def translate_singular_message(lang, msgid)
60 return "" if msgid.nil?
61
62 lang_key = lang.to_s
63
64 mo = nil
65 if self.class.cached?
66 mo = @mofiles[lang_key]
67 end
68 unless mo
69 mo = load_mo(lang)
70 end
71
72 if (! mo) or (mo ==:empty)
73 return nil
74 end
75
76 return mo[msgid] if mo.has_key?(msgid)
77
78 ret = nil
79 if msgid.include?("\000")
80 # Check "aaa\000bbb" and show warning but return the singular part.
81 msgid_single = msgid.split("\000")[0]
82 msgid_single_prefix_re = /^#{Regexp.quote(msgid_single)}\000/
83 mo.each do |key, val|
84 if msgid_single_prefix_re =~ key
85 # Usually, this is not caused to make po-files from rgettext.
86 separated_msgid = msgid.gsub(/\000/, '", "')
87 duplicated_msgid = key.gsub(/\000/, '", "')
88 warn("Warning: " +
89 "n_(\"#{separated_msgid}\") and " +
90 "n_(\"#{duplicated_msgid}\") " +
91 "are duplicated.")
92 ret = val
93 break
94 end
95 end
96 else
97 msgid_prefix_re = /^#{Regexp.quote(msgid)}\000/
98 mo.each do |key, val|
99 if msgid_prefix_re =~ key
100 ret = val.split("\000")[0]
101 break
102 end
103 end
104 end
105 ret
106 end
107
108 DEFAULT_PLURAL_CALC = Proc.new {|n| n != 1}
109 DEFAULT_SINGLE_CALC = Proc.new {|n| 0}
110
111 # Translates the translated string.
112 # * lang: Locale::Tag::Simple's subclass.
113 # * msgid: the original message.
114 # * msgid_plural: the original message(plural).
115 # * Returns: the translated string as an Array ([[msgstr1, msgstr2, ...], cond]) or nil.
116 def translate_plural_message(lang, msgid, msgid_plural) #:nodoc:
117 key = msgid + "\000" + msgid_plural
118 msg = translate_singular_message(lang, key)
119 ret = nil
120 if ! msg
121 ret = nil
122 elsif msg.include?("\000")
123 # [[msgstr[0], msgstr[1], msgstr[2],...], cond]
124 mo = @mofiles[lang.to_posix.to_s]
125 cond = (mo and mo != :empty) ? mo.plural_as_proc : DEFAULT_PLURAL_CALC
126 ret = [msg.split("\000"), cond]
127 else
128 ret = [[msg], DEFAULT_SINGLE_CALC]
129 end
130 ret
131 end
132
133 # Clear cached mofiles.
134 def clear
135 @mofiles = {}
136 end
137
138 # Set output_charset.
139 # * charset: output charset.
140 def output_charset=(charset)
141 @output_charset = charset
142 clear
143 end
144
145 private
146 # Load a mo-file from the file.
147 # lang is the subclass of Locale::Tag::Simple.
148 def load_mo(lang)
149 lang = lang.to_posix unless lang.kind_of? Locale::Tag::Posix
150 lang_key = lang.to_s
151
152 mo = @mofiles[lang_key]
153 if mo
154 if mo == :empty
155 return :empty
156 elsif ! self.class.cached?
157 mo.update!
158 end
159 return mo
160 end
161
162 path = @locale_path.current_path(lang)
163
164 if path
165 charset = @output_charset || lang.charset || Locale.charset || "UTF-8"
166 charset = normalize_charset(charset)
167 @mofiles[lang_key] = MO.open(path, charset)
168 else
169 @mofiles[lang_key] = :empty
170 end
171 end
172
173 def normalize_charset(charset)
174 case charset
175 when /\Autf8\z/i
176 "UTF-8"
177 else
178 charset
179 end
180 end
181 end
182 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 gettext/text_domain_group - GetText::TextDomainGroup class
4
5 Copyright (C) 2009 Masao Mutoh
6
7 You may redistribute it and/or modify it under the same
8 license terms as Ruby or LGPL.
9
10 =end
11
12 module GetText
13
14 class TextDomainGroup
15 attr_reader :text_domains
16
17 def initialize
18 @text_domains = []
19 end
20
21 def add(text_domain)
22 @text_domains.unshift(text_domain) unless @text_domains.include? text_domain
23 end
24 end
25 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 gettext/text_domain_manager - GetText::TextDomainManager class
4
5 Copyright (C) 2009 Masao Mutoh
6
7 You may redistribute it and/or modify it under the same
8 license terms as Ruby or LGPL.
9
10 =end
11
12 require 'gettext/class_info'
13 require 'gettext/text_domain'
14 require 'gettext/text_domain_group'
15
16 module GetText
17
18 module TextDomainManager
19
20 @@text_domain_pool = {}
21 @@text_domain_group_pool = {}
22
23 @@output_charset = nil
24 @@gettext_classes = []
25
26 @@singular_message_cache = {}
27 @@plural_message_cache = {}
28 @@cached = ! $DEBUG
29
30 extend self
31
32 # Find text domain by name
33 def text_domain_pool(domainname)
34 @@text_domain_pool[domainname]
35 end
36
37 # Set the value whether cache messages or not.
38 # true to cache messages, otherwise false.
39 #
40 # Default is true. If $DEBUG is false, messages are not checked even if
41 # this value is true.
42 def cached=(val)
43 @@cached = val
44 TextDomain.cached = val
45 end
46
47 # Return the cached value.
48 def cached?
49 TextDomain.cached?
50 end
51
52 # Gets the output charset.
53 def output_charset
54 @@output_charset
55 end
56
57 # Sets the output charset.The program can have a output charset.
58 def output_charset=(charset)
59 @@output_charset = charset
60 @@text_domain_pool.each do |key, text_domain|
61 text_domain.output_charset = charset
62 end
63 end
64
65 # bind text domain to the class.
66 def bind_to(klass, domainname, options = {})
67 warn "Bind the domain '#{domainname}' to '#{klass}'. " if $DEBUG
68
69 charset = options[:output_charset] || self.output_charset
70 text_domain = create_or_find_text_domain(domainname,options[:path],charset)
71 target_klass = ClassInfo.normalize_class(klass)
72 create_or_find_text_domain_group(target_klass).add(text_domain)
73 @@gettext_classes << target_klass unless @@gettext_classes.include? target_klass
74
75 text_domain
76 end
77
78 def each_text_domains(klass) #:nodoc:
79 lang = Locale.candidates[0]
80 ClassInfo.related_classes(klass, @@gettext_classes).each do |target|
81 if group = @@text_domain_group_pool[target]
82 group.text_domains.each do |text_domain|
83 yield text_domain, lang
84 end
85 end
86 end
87 end
88
89 # Translates msgid, but if there are no localized text,
90 # it returns a last part of msgid separeted "div" or whole of the msgid with no "div".
91 #
92 # * msgid: the message id.
93 # * div: separator or nil.
94 # * Returns: the localized text by msgid. If there are no localized text,
95 # it returns a last part of msgid separeted "div".
96 def translate_singular_message(klass, msgid, div = nil)
97 klass = ClassInfo.normalize_class(klass)
98 key = [Locale.current, klass, msgid, div]
99 msg = @@singular_message_cache[key]
100 return msg if msg and @@cached
101 # Find messages from related classes.
102 each_text_domains(klass) do |text_domain, lang|
103 msg = text_domain.translate_singular_message(lang, msgid)
104 break if msg
105 end
106
107 # If not found, return msgid.
108 msg ||= msgid
109 if div and msg == msgid
110 if index = msg.rindex(div)
111 msg = msg[(index + 1)..-1]
112 end
113 end
114 @@singular_message_cache[key] = msg
115 end
116
117 # This function is similar to the get_singular_message function
118 # as it finds the message catalogs in the same way.
119 # But it takes two extra arguments for plural form.
120 # The msgid parameter must contain the singular form of the string to be converted.
121 # It is also used as the key for the search in the catalog.
122 # The msgid_plural parameter is the plural form.
123 # The parameter n is used to determine the plural form.
124 # If no message catalog is found msgid1 is returned if n == 1, otherwise msgid2.
125 # And if msgid includes "div", it returns a last part of msgid separeted "div".
126 #
127 # * msgid: the singular form with "div". (e.g. "Special|An apple", "An apple")
128 # * msgid_plural: the plural form. (e.g. "%{num} Apples")
129 # * n: a number used to determine the plural form.
130 # * div: the separator. Default is "|".
131 # * Returns: the localized text which key is msgid_plural if n is plural(follow plural-rule) or msgid.
132 # "plural-rule" is defined in po-file.
133 #
134 # or
135 #
136 # * [msgid, msgid_plural] : msgid and msgid_plural an Array
137 # * n: a number used to determine the plural form.
138 # * div: the separator. Default is "|".
139 def translate_plural_message(klass, arg1, arg2, arg3 = "|", arg4 = "|")
140 klass = ClassInfo.normalize_class(klass)
141 # parse arguments
142 if arg1.kind_of?(Array)
143 msgid = arg1[0]
144 msgid_plural = arg1[1]
145 n = arg2
146 if arg3 and arg3.kind_of? Numeric
147 raise ArgumentError, _("ngettext: 3rd parmeter is wrong: value = %{number}") % {:number => arg3}
148 end
149 div = arg3
150 else
151 msgid = arg1
152 msgid_plural = arg2
153 raise ArgumentError, _("ngettext: 3rd parameter should be a number, not nil.") unless arg3
154 n = arg3
155 div = arg4
156 end
157
158 key = [Locale.current, klass, msgid, msgid_plural, div]
159 msgs = @@plural_message_cache[key]
160 unless (msgs and @@cached)
161 # Find messages from related classes.
162 msgs = nil
163 each_text_domains(klass) do |text_domain, lang|
164 msgs = text_domain.translate_plural_message(lang, msgid, msgid_plural)
165 break if msgs
166 end
167
168 msgs = [[msgid, msgid_plural], TextDomain::DEFAULT_PLURAL_CALC] unless msgs
169
170 msgstrs = msgs[0]
171 if div and msgstrs[0] == msgid and index = msgstrs[0].rindex(div)
172 msgstrs[0] = msgstrs[0][(index + 1)..-1]
173 end
174 @@plural_message_cache[key] = msgs
175 end
176
177 # Return the singular or plural message.
178 msgstrs = msgs[0]
179 plural = msgs[1].call(n)
180 return msgstrs[plural] if plural.kind_of?(Numeric)
181 return plural ? msgstrs[1] : msgstrs[0]
182 end
183
184 # for testing.
185 def dump_all_text_domains
186 [
187 @@text_domain_pool.dup,
188 @@text_domain_group_pool.dup,
189 @@gettext_classes.dup,
190 ]
191 end
192
193 # for testing.
194 def restore_all_text_domains(dumped_all_text_domains)
195 @@text_domain_pool, @@text_domain_group_pool, @@gettext_classes =
196 dumped_all_text_domains
197 clear_caches
198 end
199
200 # for testing.
201 def clear_all_text_domains
202 @@text_domain_pool = {}
203 @@text_domain_group_pool = {}
204 @@gettext_classes = []
205 clear_caches
206 end
207
208 # for testing.
209 def clear_caches
210 @@singular_message_cache = {}
211 @@plural_message_cache = {}
212 end
213
214 def create_or_find_text_domain_group(klass) #:nodoc:
215 group = @@text_domain_group_pool[klass]
216 return group if group
217
218 @@text_domain_group_pool[klass] = TextDomainGroup.new
219 end
220
221 def create_or_find_text_domain(name, path, charset)#:nodoc:
222 text_domain = @@text_domain_pool[name]
223 return text_domain if text_domain
224
225 @@text_domain_pool[name] = TextDomain.new(name, path, charset)
226 end
227 end
228 end
0 # Copyright (C) 2014 Kouhei Sutou <kou@clear-code.com>
1 #
2 # License: Ruby's or LGPL
3 #
4 # This library is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Lesser General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 require "optparse"
18 require "gettext"
19 require "gettext/po_parser"
20 require "gettext/po"
21
22 module GetText
23 module Tools
24 class MsgCat
25 class << self
26 # (see #run)
27 #
28 # This method is provided just for convenience. It equals to
29 # `new.run(*command_line)`.
30 def run(*command_line)
31 new.run(*command_line)
32 end
33 end
34
35 # Concatenates po-files.
36 #
37 # @param [Array<String>] command_line
38 # The command line arguments for rmsgcat.
39 # @return [void]
40 def run(*command_line)
41 config = Config.new
42 config.parse(command_line)
43
44 parser = POParser.new
45 parser.report_warning = config.report_warning?
46 parser.ignore_fuzzy = !config.include_fuzzy?
47 output_po = PO.new
48 output_po.order = config.order
49 merger = Merger.new(output_po, config)
50 config.pos.each do |po_file_name|
51 po = PO.new
52 parser.parse_file(po_file_name, po)
53 merger.merge(po)
54 end
55
56 output_po_string = output_po.to_s(config.po_format_options)
57 if config.output.is_a?(String)
58 File.open(File.expand_path(config.output), "w") do |file|
59 file.print(output_po_string)
60 end
61 else
62 puts(output_po_string)
63 end
64 end
65
66 # @private
67 class Merger
68 def initialize(output_po, config)
69 @output_po = output_po
70 @config = config
71 end
72
73 def merge(po)
74 po.each do |entry|
75 id = [entry.msgctxt, entry.msgid]
76 if @output_po.has_key?(*id)
77 merged_entry = merge_entry(@output_po[*id], entry)
78 else
79 merged_entry = entry
80 end
81 @output_po[*id] = merged_entry if merged_entry
82 end
83 end
84
85 private
86 def merge_entry(base_entry, new_entry)
87 if base_entry.header?
88 return merge_header(base_entry, new_entry)
89 end
90
91 if base_entry.fuzzy?
92 return merge_fuzzy_entry(base_entry, new_entry)
93 end
94
95 if base_entry.translated?
96 base_entry
97 else
98 new_entry
99 end
100 end
101
102 def merge_header(base_entry, new_entry)
103 base_entry
104 end
105
106 def merge_fuzzy_entry(base_entry, new_entry)
107 if new_entry.fuzzy?
108 base_entry
109 else
110 new_entry
111 end
112 end
113 end
114
115 # @private
116 class Config
117 include GetText
118
119 bindtextdomain("gettext")
120
121 # @return [Array<String>] The input PO file names.
122 attr_accessor :pos
123
124 # @return [String] The output file name.
125 attr_accessor :output
126
127 # @return [:reference, :msgid] The sort key.
128 attr_accessor :order
129
130 # @return [Hash] The PO format options.
131 # @see PO#to_s
132 # @see POEntry#to_s
133 attr_accessor :po_format_options
134
135 # (see include_fuzzy?)
136 attr_writer :include_fuzzy
137
138 # (see report_warning?)
139 attr_writer :report_warning
140
141 def initialize
142 @pos = []
143 @output = nil
144 @order = nil
145 @po_format_options = {
146 :max_line_width => POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH,
147 }
148 @include_fuzzy = true
149 @report_warning = true
150 end
151
152 # @return [Boolean] Whether includes fuzzy entries or not.
153 def include_fuzzy?
154 @include_fuzzy
155 end
156
157 # @return [Boolean] Whether reports warning messages or not.
158 def report_warning?
159 @report_warning
160 end
161
162 def parse(command_line)
163 parser = create_option_parser
164 @pos = parser.parse(command_line)
165 end
166
167 private
168 def create_option_parser
169 parser = OptionParser.new
170 parser.version = GetText::VERSION
171 parser.banner = _("Usage: %s [OPTIONS] PO_FILE1 PO_FILE2 ...") % $0
172 parser.separator("")
173 parser.separator(_("Concatenates and merges PO files."))
174 parser.separator("")
175 parser.separator(_("Specific options:"))
176
177 parser.on("-o", "--output=FILE",
178 _("Write output to specified file"),
179 _("(default: the standard output)")) do |output|
180 @output = output
181 end
182
183 parser.on("--sort-by-msgid",
184 _("Sort output by msgid")) do
185 @order = :msgid
186 end
187
188 parser.on("--sort-by-location",
189 _("Sort output by location")) do
190 @order = :reference
191 end
192
193 parser.on("--sort-by-file",
194 _("Sort output by location"),
195 _("It is same as --sort-by-location"),
196 _("Just for GNU gettext's msgcat compatibility")) do
197 @order = :reference
198 end
199
200 parser.on("--[no-]sort-output",
201 _("Sort output by msgid"),
202 _("It is same as --sort-by-msgid"),
203 _("Just for GNU gettext's msgcat compatibility")) do |sort|
204 @order = sort ? :msgid : nil
205 end
206
207 parser.on("--no-location",
208 _("Remove location information")) do |boolean|
209 @po_format_options[:include_reference_comment] = boolean
210 end
211
212 parser.on("--no-all-comments",
213 _("Remove all comments")) do |boolean|
214 @po_format_options[:include_all_comments] = boolean
215 end
216
217 parser.on("--width=WIDTH", Integer,
218 _("Set output page width"),
219 "(#{@po_format_options[:max_line_width]})") do |width|
220 @po_format_options[:max_line_width] = width
221 end
222
223 parser.on("--[no-]wrap",
224 _("Break long message lines, longer than the output page width, into several lines"),
225 "(#{@po_format_options[:max_line_width] >= 0})") do |wrap|
226 if wrap
227 max_line_width = POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH
228 else
229 max_line_width = -1
230 end
231 @po_format_options[:max_line_width] = max_line_width
232 end
233
234 parser.on("--no-fuzzy",
235 _("Ignore fuzzy entries")) do |include_fuzzy|
236 @include_fuzzy = include_fuzzy
237 end
238
239 parser.on("--no-report-warning",
240 _("Don't report warning messages")) do |report_warning|
241 @report_warning = report_warning
242 end
243
244 parser
245 end
246 end
247 end
248 end
249 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
4 # Copyright (C) 2003-2009 Masao Mutoh
5 #
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require "optparse"
22 require "fileutils"
23 require "gettext"
24 require "gettext/po_parser"
25
26 module GetText
27 module Tools
28 class MsgFmt #:nodoc:
29 # Create a mo-file from a target file(po-file).
30 # You must specify a path of a target file in arguments.
31 # If a path of a mo-file is not specified in arguments, a mo-file is
32 # created as "messages.mo" in the current directory.
33 # @param [Array<String>] arguments arguments for rmsgfmt.
34 # @return [void]
35 class << self
36 def run(*arguments)
37 new.run(*arguments)
38 end
39 end
40
41 include GetText
42
43 bindtextdomain("gettext")
44
45 def initialize
46 @input_file = nil
47 @output_file = nil
48 end
49
50 def run(*options) # :nodoc:
51 initialize_arguments(*options)
52
53 parser = POParser.new
54 data = MO.new
55
56 parser.parse_file(@input_file, data)
57 data.save_to_file(@output_file)
58 end
59
60 def initialize_arguments(*options) # :nodoc:
61 input_file, output_file = parse_commandline_options(*options)
62
63 if input_file.nil?
64 raise(ArgumentError, _("no input files specified."))
65 end
66
67 if output_file.nil?
68 output_file = "messages.mo"
69 end
70
71 @input_file = input_file
72 @output_file = output_file
73 end
74
75 def parse_commandline_options(*options)
76 output_file = nil
77
78 parser = OptionParser.new
79 parser.banner = _("Usage: %s input.po [-o output.mo]" % $0)
80 parser.separator("")
81 description = _("Generate binary message catalog from textual " +
82 "translation description.")
83 parser.separator(description)
84 parser.separator("")
85 parser.separator(_("Specific options:"))
86
87 parser.on("-o", "--output=FILE",
88 _("write output to specified file")) do |out|
89 output_file = out
90 end
91
92 parser.on_tail("--version", _("display version information and exit")) do
93 puts(VERSION)
94 exit(true)
95 end
96 parser.parse!(options)
97
98 input_file = options[0]
99 [input_file, output_file]
100 end
101 end
102 end
103 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
3 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
4 #
5 # License: Ruby's or LGPL
6 #
7 # This library is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Lesser General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 require "etc"
21 require "gettext"
22 require "gettext/po_parser"
23 require "gettext/tools/msgmerge"
24 require "locale/info"
25 require "optparse"
26
27 module GetText
28 module Tools
29 class MsgInit
30 class Error < StandardError
31 end
32
33 class ArgumentError < Error
34 end
35
36 class ValidationError < Error
37 end
38
39 class << self
40 # Create a new .po file from initializing .pot file with user's
41 # environment and input.
42 # @param [Array<String>] arguments arguments for rmsginit.
43 # @return [void]
44 def run(*arguments)
45 new.run(*arguments)
46 end
47 end
48
49 include GetText
50
51 bindtextdomain("gettext")
52
53 def initialize
54 @input_file = nil
55 @output_file = nil
56 @locale = nil
57 @language = nil
58 @entry = nil
59 @comment = nil
60 @translator = nil
61 @set_translator = true
62 @translator_name = nil
63 @translator_eamil = nil
64 end
65
66 # Create .po file from .pot file, user's inputs and metadata.
67 # @param [Array] arguments the list of arguments for rmsginit
68 def run(*arguments)
69 parse_arguments(*arguments)
70 validate
71
72 parser = POParser.new
73 parser.ignore_fuzzy = false
74 pot = parser.parse_file(@input_file, GetText::PO.new)
75 po = replace_pot_header(pot)
76
77 File.open(@output_file, "w") do |f|
78 f.puts(po.to_s)
79 end
80 end
81
82 private
83 VERSION = GetText::VERSION
84
85 def parse_arguments(*arguments)
86 parser = OptionParser.new
87 description = _("Create a new .po file from initializing .pot " +
88 "file with user's environment and input.")
89 parser.separator(description)
90 parser.separator("")
91 parser.separator(_("Specific options:"))
92
93 input_description = _("Use INPUT as a .pot file. If INPUT is not " +
94 "specified, INPUT is a .pot file existing " +
95 "the current directory.")
96 parser.on("-i", "--input=FILE", input_description) do |input|
97 @input_file = input
98 end
99
100 output_description = _("Use OUTPUT as a created .po file. If OUTPUT " +
101 "is not specified, OUTPUT depend on LOCALE " +
102 "or the current locale on your environment.")
103 parser.on("-o", "--output=OUTPUT", output_description) do |output|
104 @output_file = output
105 end
106
107 locale_description = _("Use LOCALE as target locale. If LOCALE is " +
108 "not specified, LOCALE is the current " +
109 "locale on your environment.")
110 parser.on("-l", "--locale=LOCALE", locale_description) do |loc|
111 @locale = loc
112 end
113
114 parser.on("--no-translator",
115 _("Don't set translator information")) do
116 @set_translator = false
117 end
118
119 parser.on("--translator-name=NAME",
120 _("Use NAME as translator name")) do |name|
121 @translator_name = name
122 end
123
124 parser.on("--translator-email=EMAIL",
125 _("Use EMAIL as translator email address")) do |email|
126 @translator_email = email
127 end
128
129 parser.on("-h", "--help", _("Display this help and exit")) do
130 puts(parser.help)
131 exit(true)
132 end
133
134 version_description = _("Display version and exit")
135 parser.on_tail("-v", "--version", version_description) do
136 puts(VERSION)
137 exit(true)
138 end
139
140 begin
141 parser.parse!(arguments)
142 rescue OptionParser::ParseError
143 raise(ArgumentError, $!.message)
144 end
145 end
146
147 def validate
148 if @input_file.nil?
149 @input_file = Dir.glob("./*.pot").first
150 if @input_file.nil?
151 raise(ValidationError,
152 _(".pot file does not exist in the current directory."))
153 end
154 else
155 unless File.exist?(@input_file)
156 raise(ValidationError,
157 _("file '%s' does not exist." % @input_file))
158 end
159 end
160
161 if @locale.nil?
162 language_tag = Locale.current
163 else
164 language_tag = Locale::Tag.parse(@locale)
165 end
166
167 unless valid_locale?(language_tag)
168 raise(ValidationError,
169 _("Locale '#{language_tag}' is invalid. " +
170 "Please check if your specified locale is usable."))
171 end
172 @locale = language_tag.to_simple.to_s
173 @language = language_tag.language
174
175 @output_file ||= "#{@locale}.po"
176 if File.exist?(@output_file)
177 raise(ValidationError,
178 _("file '%s' has already existed." % @output_file))
179 end
180 end
181
182 def valid_locale?(language_tag)
183 return false if language_tag.instance_of?(Locale::Tag::Irregular)
184
185 Locale::Info.language_code?(language_tag.language)
186 end
187
188 def replace_pot_header(pot)
189 @entry = pot[""].msgstr
190 @comment = pot[""].translator_comment
191 @translator = translator_info
192
193 replace_entry
194 replace_comment
195
196 pot[""] = @entry
197 pot[""].translator_comment = @comment
198 pot[""].flags = pot[""].flags.reject do |flag|
199 flag == "fuzzy"
200 end
201 pot
202 end
203
204 def translator_info
205 return nil unless @set_translator
206 name = translator_name
207 email = translator_email
208 if name and email
209 "#{name} <#{email}>"
210 else
211 nil
212 end
213 end
214
215 def translator_name
216 @translator_name ||= read_translator_name
217 end
218
219 def read_translator_name
220 prompt(_("Please enter your full name"), guess_translator_name)
221 end
222
223 def guess_translator_name
224 name = guess_translator_name_from_password_entry
225 name ||= ENV["USERNAME"]
226 name
227 end
228
229 def guess_translator_name_from_password_entry
230 password_entry = find_password_entry
231 return nil if password_entry.nil?
232
233 name = password_entry.gecos.split(/,/).first.strip
234 name = nil if name.empty?
235 name
236 end
237
238 def find_password_entry
239 Etc.getpwuid
240 rescue ArgumentError
241 nil
242 end
243
244 def translator_email
245 @translator_email ||= read_translator_email
246 end
247
248 def read_translator_email
249 prompt(_("Please enter your email address"), guess_translator_email)
250 end
251
252 def guess_translator_email
253 ENV["EMAIL"]
254 end
255
256 def prompt(message, default)
257 print(message)
258 print(" [#{default}]") if default
259 print(": ")
260
261 user_input = $stdin.gets.chomp
262 if user_input.empty?
263 default
264 else
265 user_input
266 end
267 end
268
269 def replace_entry
270 replace_last_translator
271 replace_pot_revision_date
272 replace_language
273 replace_plural_forms
274 end
275
276 def replace_comment
277 replace_description
278 replace_first_author
279 replace_copyright_year
280 @comment = @comment.gsub(/^fuzzy$/, "")
281 end
282
283 EMAIL = "EMAIL@ADDRESS"
284 FIRST_AUTHOR_KEY = /^FIRST AUTHOR <#{EMAIL}>, (\d+\.)$/
285
286 def replace_last_translator
287 unless @translator.nil?
288 @entry = @entry.gsub(LAST_TRANSLATOR_KEY, "\\1 #{@translator}")
289 end
290 end
291
292 POT_REVISION_DATE_KEY = /^(PO-Revision-Date:).+/
293
294 def replace_pot_revision_date
295 @entry = @entry.gsub(POT_REVISION_DATE_KEY, "\\1 #{revision_date}")
296 end
297
298 LANGUAGE_KEY = /^(Language:).+/
299 LANGUAGE_TEAM_KEY = /^(Language-Team:).+/
300
301 def replace_language
302 language_name = Locale::Info.get_language(@language).name
303 @entry = @entry.gsub(LANGUAGE_KEY, "\\1 #{@locale}")
304 @entry = @entry.gsub(LANGUAGE_TEAM_KEY, "\\1 #{language_name}")
305 end
306
307 PLURAL_FORMS =
308 /^(Plural-Forms:) nplurals=INTEGER; plural=EXPRESSION;$/
309
310 def replace_plural_forms
311 plural_entry = plural_forms(@language)
312 if PLURAL_FORMS =~ @entry
313 @entry = @entry.gsub(PLURAL_FORMS, "\\1 #{plural_entry}\n")
314 else
315 @entry << "Plural-Forms: #{plural_entry}\n"
316 end
317 end
318
319 def plural_forms(language)
320 case language
321 when "ja", "vi", "ko", /\Azh.*\z/
322 nplural = "1"
323 plural_expression = "0"
324 when "en", "de", "nl", "sv", "da", "no", "fo", "es", "pt",
325 "it", "bg", "el", "fi", "et", "he", "eo", "hu", "tr",
326 "ca", "nb"
327 nplural = "2"
328 plural_expression = "n != 1"
329 when "pt_BR", "fr"
330 nplural = "2"
331 plural_expression = "n>1"
332 when "lv"
333 nplural = "3"
334 plural_expression = "n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2"
335 when "ga"
336 nplural = "3"
337 plural_expression = "n==1 ? 0 : n==2 ? 1 : 2"
338 when "ro"
339 nplural = "3"
340 plural_expression = "n==1 ? 0 : " +
341 "(n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2"
342 when "lt", "bs"
343 nplural = "3"
344 plural_expression = "n%10==1 && n%100!=11 ? 0 : " +
345 "n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2"
346 when "ru", "uk", "sr", "hr"
347 nplural = "3"
348 plural_expression = "n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +
349 "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"
350 when "cs", "sk"
351 nplural = "3"
352 plural_expression = "(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2"
353 when "pl"
354 nplural = "3"
355 plural_expression = "n==1 ? 0 : n%10>=2 && " +
356 "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2"
357 when "sl"
358 nplural = "4"
359 plural_expression = "n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 " +
360 "|| n%100==4 ? 2 : 3"
361 else
362 nplural = nil
363 plural_expression = nil
364 end
365
366 "nplurals=#{nplural}; plural=#{plural_expression};"
367 end
368
369 DESCRIPTION_TITLE = /^SOME DESCRIPTIVE TITLE\.$/
370
371 def replace_description
372 language_name = Locale::Info.get_language(@language).name
373 package_name = ""
374 @entry.gsub(/Project-Id-Version: (.+?) .+/) do
375 package_name = $1
376 end
377 description = "#{language_name} translations " +
378 "for #{package_name} package."
379 @comment = @comment.gsub(DESCRIPTION_TITLE, "\\1 #{description}")
380 end
381
382 YEAR_KEY = /^(FIRST AUTHOR <#{EMAIL}>,) YEAR\.$/
383 LAST_TRANSLATOR_KEY = /^(Last-Translator:) FULL NAME <#{EMAIL}>$/
384
385 def replace_first_author
386 @comment = @comment.gsub(YEAR_KEY, "\\1 #{year}.")
387 unless @translator.nil?
388 @comment = @comment.gsub(FIRST_AUTHOR_KEY, "#{@translator}, \\1")
389 end
390 end
391
392 COPYRIGHT_KEY = /^(Copyright \(C\)) YEAR (THE PACKAGE'S COPYRIGHT HOLDER)$/
393 def replace_copyright_year
394 @comment = @comment.gsub(COPYRIGHT_KEY, "\\1 #{year} \\2")
395 end
396
397 def now
398 @now ||= Time.now
399 end
400
401 def revision_date
402 now.strftime("%Y-%m-%d %H:%M%z")
403 end
404
405 def year
406 now.year
407 end
408 end
409 end
410 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2013 Haruka Yoshihara <yoshihara@clear-code.com>
3 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
4 # Copyright (C) 2005-2009 Masao Mutoh
5 # Copyright (C) 2005,2006 speakillof
6 #
7 # License: Ruby's or LGPL
8 #
9 # This library is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Lesser General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This library is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Lesser General Public License for more details.
18 #
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 require "optparse"
23 require "text"
24 require "gettext"
25 require "gettext/po_parser"
26 require "gettext/po"
27
28 module GetText
29 module Tools
30 class MsgMerge
31 class << self
32 # (see #run)
33 #
34 # This method is provided just for convenience. It equals to
35 # `new.run(*command_line)`.
36 def run(*command_line)
37 new.run(*command_line)
38 end
39 end
40
41 # Merge a po-file inluding translated messages and a new pot-file.
42 #
43 # @param [Array<String>] command_line
44 # command line arguments for rmsgmerge.
45 # @return [void]
46 def run(*command_line)
47 config = Config.new
48 config.parse(command_line)
49
50 parser = POParser.new
51 parser.ignore_fuzzy = false
52 definition_po = PO.new
53 reference_pot = PO.new
54 parser.parse_file(config.definition_po, definition_po)
55 parser.parse_file(config.reference_pot, reference_pot)
56
57 merger = Merger.new(reference_pot, definition_po, config)
58 result = merger.merge
59 result.order = config.order
60 p result if $DEBUG
61 print result.generate_po if $DEBUG
62
63 if config.output.is_a?(String)
64 File.open(File.expand_path(config.output), "w+") do |file|
65 file.write(result.to_s(config.po_format_options))
66 end
67 else
68 puts(result.to_s(config.po_format_options))
69 end
70 end
71
72 # @private
73 class Merger
74 # Merge the reference with the definition: take the #. and
75 # #: comments from the reference, take the # comments from
76 # the definition, take the msgstr from the definition. Add
77 # this merged entry to the output message list.
78
79 POT_DATE_EXTRACT_RE = /POT-Creation-Date:\s*(.*)?\s*$/
80 POT_DATE_RE = /POT-Creation-Date:.*?$/
81
82 def initialize(reference, definition, config)
83 @reference = reference
84 @definition = definition
85 @translated_entries = @definition.reject do |entry|
86 entry.msgstr.nil?
87 end
88 @config = config
89 end
90
91 def merge
92 result = GetText::PO.new
93
94 @reference.each do |entry|
95 id = [entry.msgctxt, entry.msgid]
96 result[*id] = merge_definition(entry)
97 end
98
99 add_obsolete_entry(result) if @config.output_obsolete_entries?
100 result
101 end
102
103 private
104 def merge_definition(entry)
105 msgid = entry.msgid
106 msgctxt = entry.msgctxt
107 id = [msgctxt, msgid]
108
109 if @definition.has_key?(*id)
110 return merge_entry(entry, @definition[*id])
111 end
112
113 return entry unless @config.enable_fuzzy_matching?
114
115 if msgctxt.nil?
116 same_msgid_entry = find_by_msgid(@translated_entries, msgid)
117 if same_msgid_entry and same_msgid_entry.msgctxt
118 return merge_fuzzy_entry(entry, same_msgid_entry)
119 end
120 end
121
122 fuzzy_entry = find_fuzzy_entry(@translated_entries, msgid, msgctxt)
123 if fuzzy_entry
124 return merge_fuzzy_entry(entry, fuzzy_entry)
125 end
126
127 entry
128 end
129
130 def merge_entry(reference_entry, definition_entry)
131 if definition_entry.header?
132 return merge_header(reference_entry, definition_entry)
133 end
134
135 return definition_entry if definition_entry.fuzzy?
136
137 entry = reference_entry
138 entry.translator_comment = definition_entry.translator_comment
139 entry.previous = nil
140
141 unless definition_entry.msgid_plural == reference_entry.msgid_plural
142 entry.flags << "fuzzy"
143 end
144
145 entry.msgstr = definition_entry.msgstr
146 entry
147 end
148
149 def merge_header(new_header, old_header)
150 header = old_header
151 if POT_DATE_EXTRACT_RE =~ new_header.msgstr
152 create_date = $1
153 pot_creation_date = "POT-Creation-Date: #{create_date}"
154 header.msgstr = header.msgstr.gsub(POT_DATE_RE, pot_creation_date)
155 end
156 header.flags = []
157 header
158 end
159
160 def find_by_msgid(entries, msgid)
161 same_msgid_entries = entries.find_all do |entry|
162 entry.msgid == msgid
163 end
164 same_msgid_entries = same_msgid_entries.sort_by do |entry|
165 entry.msgctxt
166 end
167 same_msgid_entries.first
168 end
169
170 def merge_fuzzy_entry(entry, fuzzy_entry)
171 merged_entry = merge_entry(entry, fuzzy_entry)
172 merged_entry.flags << "fuzzy"
173 merged_entry
174 end
175
176 MAX_FUZZY_DISTANCE = 0.5 # XXX: make sure that its value is proper.
177
178 def find_fuzzy_entry(definition, msgid, msgctxt)
179 return nil if msgid == :last
180 min_distance_entry = nil
181 min_distance = MAX_FUZZY_DISTANCE
182
183 same_msgctxt_entries = definition.find_all do |entry|
184 entry.msgctxt == msgctxt and not entry.msgid == :last
185 end
186 same_msgctxt_entries.each do |entry|
187 distance = normalize_distance(entry.msgid, msgid)
188 next if distance.nil?
189 if min_distance > distance
190 min_distance = distance
191 min_distance_entry = entry
192 end
193 end
194
195 min_distance_entry
196 end
197
198 MAX_N_CHARACTERS_DIFFERENCE = 10
199 def normalize_distance(source, destination)
200 n_characters_difference = (source.size - destination.size).abs
201 return nil if n_characters_difference > MAX_N_CHARACTERS_DIFFERENCE
202
203 max_size = [source.size, destination.size].max
204 return 0.0 if max_size.zero?
205
206 Text::Levenshtein.distance(source, destination) / max_size.to_f
207 end
208
209 def add_obsolete_entry(result)
210 obsolete_entry = generate_obsolete_entry(result)
211 return if obsolete_entry.nil?
212
213 result[:last] = obsolete_entry
214 end
215
216 def generate_obsolete_entry(result)
217 obsolete_entries = extract_obsolete_entries(result)
218 obsolete_comments = obsolete_entries.collect do |entry|
219 entry.to_s
220 end
221
222 return nil if obsolete_comments.empty?
223
224 obsolete_entry = POEntry.new(:normal)
225 obsolete_entry.msgid = :last
226 obsolete_entry.comment = obsolete_comments.join("\n")
227 obsolete_entry
228 end
229
230 def extract_obsolete_entries(result)
231 @definition.find_all do |entry|
232 if entry.obsolete?
233 true
234 elsif entry.msgstr.nil?
235 false
236 else
237 id = [entry.msgctxt, entry.msgid]
238 not result.has_key?(*id)
239 end
240 end
241 end
242 end
243
244 # @private
245 class Config
246 include GetText
247
248 bindtextdomain("gettext")
249
250 attr_accessor :definition_po, :reference_pot
251 attr_accessor :output, :update
252 attr_accessor :order
253 attr_accessor :po_format_options
254
255 # update mode options
256 attr_accessor :backup, :suffix
257
258 # (#see #enable_fuzzy_matching?)
259 attr_writer :enable_fuzzy_matching
260
261 # (#see #output_obsolete_entries?)
262 attr_writer :output_obsolete_entries
263
264 # The result is written back to def.po.
265 # --backup=CONTROL make a backup of def.po
266 # --suffix=SUFFIX override the usual backup suffix
267 # The version control method may be selected
268 # via the --backup option or through
269 # the VERSION_CONTROL environment variable. Here are the values:
270 # none, off never make backups (even if --backup is given)
271 # numbered, t make numbered backups
272 # existing, nil numbered if numbered backups exist, simple otherwise
273 # simple, never always make simple backups
274 # The backup suffix is `~', unless set with --suffix or
275 # the SIMPLE_BACKUP_SUFFIX environment variable.
276
277 def initialize
278 @definition_po = nil
279 @reference_po = nil
280 @update = false
281 @output = nil
282 @order = :reference
283 @po_format_options = {
284 :max_line_width => POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH,
285 }
286 @enable_fuzzy_matching = true
287 @update = nil
288 @output_obsolete_entries = true
289 @backup = ENV["VERSION_CONTROL"]
290 @suffix = ENV["SIMPLE_BACKUP_SUFFIX"] || "~"
291 @input_dirs = ["."]
292 end
293
294 def parse(command_line)
295 parser = create_option_parser
296 rest = parser.parse(command_line)
297
298 if rest.size != 2
299 puts(parser.help)
300 exit(false)
301 end
302
303 @definition_po, @reference_pot = rest
304 @output = @definition_po if @update
305 end
306
307 # @return [Bool] true if fuzzy matching is enabled, false otherwise.
308 def enable_fuzzy_matching?
309 @enable_fuzzy_matching
310 end
311
312 # @return [Bool] true if outputting obsolete entries is
313 # enabled, false otherwise.
314 def output_obsolete_entries?
315 @output_obsolete_entries
316 end
317
318 private
319 def create_option_parser
320 parser = OptionParser.new
321 parser.banner =
322 _("Usage: %s [OPTIONS] definition.po reference.pot") % $0
323 #parser.summary_width = 80
324 parser.separator("")
325 description = _("Merges two Uniforum style .po files together. " +
326 "The definition.po file is an existing PO file " +
327 "with translations. The reference.pot file is " +
328 "the last created PO file with up-to-date source " +
329 "references. The reference.pot is generally " +
330 "created by rxgettext.")
331 parser.separator(description)
332 parser.separator("")
333 parser.separator(_("Specific options:"))
334
335 parser.on("-U", "--[no-]update",
336 _("Update definition.po")) do |update|
337 @update = update
338 end
339
340 parser.on("-o", "--output=FILE",
341 _("Write output to specified file")) do |output|
342 @output = output
343 end
344
345 parser.on("--[no-]sort-output",
346 _("Sort output by msgid"),
347 _("It is same as --sort-by-msgid"),
348 _("Just for GNU gettext's msgcat compatibility")) do |sort|
349 @order = sort ? :msgid : nil
350 end
351
352 parser.on("--sort-by-file",
353 _("Sort output by location"),
354 _("It is same as --sort-by-location"),
355 _("Just for GNU gettext's msgcat compatibility")) do
356 @order = :reference
357 end
358
359 parser.on("--sort-by-location",
360 _("Sort output by location")) do
361 @order = :reference
362 end
363
364 parser.on("--sort-by-msgid",
365 _("Sort output by msgid")) do
366 @order = :msgid
367 end
368
369 parser.on("--[no-]location",
370 _("Preserve '#: FILENAME:LINE' lines")) do |location|
371 @po_format_options[:include_reference_comment] = location
372 end
373
374 parser.on("--width=WIDTH", Integer,
375 _("Set output page width"),
376 "(#{@po_format_options[:max_line_width]})") do |width|
377 @po_format_options[:max_line_width] = width
378 end
379
380 parser.on("--[no-]wrap",
381 _("Break long message lines, longer than the output page width, into several lines"),
382 "(#{@po_format_options[:max_line_width] >= 0})") do |wrap|
383 if wrap
384 max_line_width = POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH
385 else
386 max_line_width = -1
387 end
388 @po_format_options[:max_line_width] = max_line_width
389 end
390
391 parser.on("--[no-]fuzzy-matching",
392 _("Disable fuzzy matching"),
393 _("(enable)")) do |boolean|
394 @enable_fuzzy_matching = boolean
395 end
396
397 parser.on("--no-obsolete-entries",
398 _("Don't output obsolete entries")) do |boolean|
399 @output_obsolete_entries = boolean
400 end
401
402 parser.on("-h", "--help", _("Display this help and exit")) do
403 puts(parser.help)
404 exit(true)
405 end
406
407 parser.on_tail("--version",
408 _("Display version information and exit")) do
409 puts(GetText::VERSION)
410 exit(true)
411 end
412
413 parser
414 end
415 end
416 end
417 end
418 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 parser/erb.rb - parser for ERB
4
5 Copyright (C) 2005-2009 Masao Mutoh
6
7 You may redistribute it and/or modify it under the same
8 license terms as Ruby or LGPL.
9 =end
10
11 require 'erb'
12 require 'gettext/tools/parser/ruby'
13
14 module GetText
15 class ErbParser
16 @config = {
17 :extnames => ['.rhtml', '.erb']
18 }
19
20 class << self
21 # Sets some preferences to parse ERB files.
22 # * config: a Hash of the config. It can takes some values below:
23 # * :extnames: An Array of target files extension. Default is [".rhtml"].
24 def init(config)
25 config.each{|k, v|
26 @config[k] = v
27 }
28 end
29
30 def target?(file) # :nodoc:
31 @config[:extnames].each do |v|
32 return true if File.extname(file) == v
33 end
34 false
35 end
36
37 # Parses eRuby script located at `path`.
38 #
39 # This is a short cut method. It equals to `new(path,
40 # options).parse`.
41 #
42 # @return [Array<POEntry>] Extracted messages
43 # @see #initialize and #parse
44 def parse(path, options={})
45 parser = new(path, options)
46 parser.parse
47 end
48 end
49
50 MAGIC_COMMENT = /\A#coding:.*\n/
51
52 # @param path [String] eRuby script path to be parsed
53 # @param options [Hash]
54 def initialize(path, options={})
55 @path = path
56 @options = options
57 end
58
59 # Extracts messages from @path.
60 #
61 # @return [Array<POEntry>] Extracted messages
62 def parse
63 content = IO.read(@path)
64 src = ERB.new(content).src
65
66 # Force the src encoding back to the encoding in magic comment
67 # or original content.
68 encoding = detect_encoding(src) || content.encoding
69 src.force_encoding(encoding)
70
71 # Remove magic comment prepended by erb in Ruby 1.9.
72 src = src.gsub(MAGIC_COMMENT, "")
73
74 RubyParser.new(@path, @options).parse_source(src)
75 end
76
77 def detect_encoding(erb_source)
78 if /\A#coding:(.*)\n/ =~ erb_source
79 $1
80 else
81 nil
82 end
83 end
84 end
85 end
86
87 if __FILE__ == $0
88 # ex) ruby glade.rhtml foo.rhtml bar.rhtml
89 ARGV.each do |file|
90 p GetText::ErbParser.parse(file)
91 end
92 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 parser/glade.rb - parser for Glade-2
4
5 Copyright (C) 2013 Kouhei Sutou <kou@clear-code.com>
6 Copyright (C) 2004,2005 Masao Mutoh
7
8 You may redistribute it and/or modify it under the same
9 license terms as Ruby or LGPL.
10 =end
11
12 require 'cgi'
13 require 'gettext'
14
15 module GetText
16 class GladeParser
17 extend GetText
18
19 bindtextdomain("gettext")
20
21 class << self
22 XML_RE = /<\?xml/
23 GLADE_RE = /glade-2.0.dtd/
24
25 def target?(file) # :nodoc:
26 data = IO.readlines(file)
27 if XML_RE =~ data[0] and GLADE_RE =~ data[1]
28 true
29 else
30 if File.extname(file) == '.glade'
31 raise _("`%{file}' is not glade-2.0 format.") % {:file => file}
32 end
33 false
34 end
35 end
36
37 def parse(path, options={})
38 parser = new(path, options)
39 parser.parse
40 end
41 end
42
43 TARGET1 = /<property.*translatable="yes">(.*)/
44 TARGET2 = /(.*)<\/property>/
45
46 def initialize(path, options={})
47 @path = path
48 @options = options
49 end
50
51 def parse # :nodoc:
52 File.open(@path) do |file|
53 parse_source(file)
54 end
55 end
56
57 private
58 def parse_source(input) # :nodoc:
59 targets = []
60 target = false
61 start_line_no = nil
62 val = nil
63
64 input.each_line.with_index do |line, i|
65 if TARGET1 =~ line
66 start_line_no = i + 1
67 val = $1 + "\n"
68 target = true
69 if TARGET2 =~ $1
70 val = $1
71 add_target(val, start_line_no, targets)
72 val = nil
73 target = false
74 end
75 elsif target
76 if TARGET2 =~ line
77 val << $1
78 add_target(val, start_line_no, targets)
79 val = nil
80 target = false
81 else
82 val << line
83 end
84 end
85 end
86 targets
87 end
88
89 def add_target(val, line_no, targets) # :nodoc:
90 return unless val.size > 0
91 assoc_data = targets.assoc(val)
92 val = CGI.unescapeHTML(val)
93 if assoc_data
94 targets[targets.index(assoc_data)] = assoc_data << "#{@path}:#{line_no}"
95 else
96 targets << [val.gsub(/\n/, '\n'), "#{@path}:#{line_no}"]
97 end
98 targets
99 end
100 end
101 end
102
103 if __FILE__ == $0
104 # ex) ruby glade.rb foo.glade bar.glade
105 ARGV.each do |file|
106 p GetText::GladeParser.parse(file)
107 end
108 end
0 # -*- coding: utf-8 -*-
1 =begin
2 parser/ruby.rb - parser for ruby script
3
4 Copyright (C) 2013 Kouhei Sutou <kou@clear-code.com>
5 Copyright (C) 2003-2009 Masao Mutoh
6 Copyright (C) 2005 speakillof
7 Copyright (C) 2001,2002 Yasushi Shoji, Masao Mutoh
8
9 You may redistribute it and/or modify it under the same
10 license terms as Ruby or LGPL.
11
12 =end
13
14 require "irb/ruby-lex"
15 require "stringio"
16 require "gettext/po_entry"
17
18 module GetText
19 class RubyLexX < RubyLex # :nodoc: all
20 # Parser#parse resemlbes RubyLex#lex
21 def parse
22 until ( (tk = token).kind_of?(RubyToken::TkEND_OF_SCRIPT) && !@continue or tk.nil? )
23 s = get_readed
24 if RubyToken::TkSTRING === tk or RubyToken::TkDSTRING === tk
25 def tk.value
26 @value
27 end
28
29 def tk.value=(s)
30 @value = s
31 end
32
33 if @here_header
34 s = s.sub(/\A.*?\n/, "").sub(/^.*\n\Z/, "")
35 else
36 begin
37 s = eval(s)
38 rescue Exception
39 # Do nothing.
40 end
41 end
42
43 tk.value = s
44 end
45
46 if $DEBUG
47 if tk.is_a? TkSTRING or tk.is_a? TkDSTRING
48 $stderr.puts("#{tk}: #{tk.value}")
49 elsif tk.is_a? TkIDENTIFIER
50 $stderr.puts("#{tk}: #{tk.name}")
51 else
52 $stderr.puts(tk)
53 end
54 end
55
56 yield tk
57 end
58 return nil
59 end
60
61 # Original parser does not keep the content of the comments,
62 # so monkey patching this with new token type and extended
63 # identify_comment implementation
64 RubyToken.def_token :TkCOMMENT_WITH_CONTENT, TkVal
65
66 def identify_comment
67 @ltype = "#"
68 get_readed # skip the hash sign itself
69
70 while ch = getc
71 if ch == "\n"
72 @ltype = nil
73 ungetc
74 break
75 end
76 end
77 return Token(TkCOMMENT_WITH_CONTENT, get_readed)
78 end
79
80 end
81
82 # Extends POEntry for RubyParser.
83 # Implements a sort of state machine to assist the parser.
84 module POEntryForRubyParser
85 # Supports parsing by setting attributes by and by.
86 def set_current_attribute(str)
87 param = @param_type[@param_number]
88 raise ParseError, "no more string parameters expected" unless param
89 set_value(param, str)
90 end
91
92 def init_param
93 @param_number = 0
94 self
95 end
96
97 def advance_to_next_attribute
98 @param_number += 1
99 end
100 end
101 class POEntry
102 include POEntryForRubyParser
103 alias :initialize_old :initialize
104 def initialize(type)
105 initialize_old(type)
106 init_param
107 end
108 end
109
110 class RubyParser
111 ID = ["gettext", "_", "N_", "sgettext", "s_"]
112 PLURAL_ID = ["ngettext", "n_", "Nn_", "ns_", "nsgettext"]
113 MSGCTXT_ID = ["pgettext", "p_"]
114 MSGCTXT_PLURAL_ID = ["npgettext", "np_"]
115
116 class << self
117 def target?(file) # :nodoc:
118 true # always true, as the default parser.
119 end
120
121 # Parses Ruby script located at `path`.
122 #
123 # This is a short cut method. It equals to `new(path,
124 # options).parse`.
125 #
126 # @param (see #initialize)
127 # @option (see #initialize)
128 # @return (see #parse)
129 # @see #initialize
130 # @see #parse
131 def parse(path, options={})
132 parser = new(path, options)
133 parser.parse
134 end
135 end
136
137 #
138 # @example `:comment_tag` option: String tag
139 # path = "hello.rb"
140 # # content:
141 # # # TRANSLATORS: This is a comment to translators.
142 # # _("Hello")
143 # #
144 # # # This is a comment for programmers.
145 # # # TRANSLATORS: This is a comment to translators.
146 # # # This is also a comment to translators.
147 # # _("World")
148 # #
149 # # # This is a comment for programmers.
150 # # # This is also a comment for programmers
151 # # # because all lines don't start with "TRANSRATORS:".
152 # # _("Bye")
153 # options = {:comment_tag => "TRANSLATORS:"}
154 # parser = GetText::RubyParser.new(path, options)
155 # parser.parse
156 # # => [
157 # # POEntry<
158 # # :msgid => "Hello",
159 # # :extracted_comment =>
160 # # "TRANSLATORS: This is a comment to translators.",
161 # # >,
162 # # POEntry<
163 # # :msgid => "World",
164 # # :extracted_comment =>
165 # # "TRANSLATORS: This is a comment to translators.\n" +
166 # # "This is also a comment to translators.",
167 # # >,
168 # # POEntry<
169 # # :msgid => "Bye",
170 # # :extracted_comment => nil,
171 # # >,
172 # # ]
173 #
174 # @example `:comment_tag` option: nil tag
175 # path = "hello.rb"
176 # # content:
177 # # # This is a comment to translators.
178 # # # This is also a comment for translators.
179 # # _("Hello")
180 # options = {:comment_tag => nil}
181 # parser = GetText::RubyParser.new(path, options)
182 # parser.parse
183 # # => [
184 # # POEntry<
185 # # :msgid => "Hello",
186 # # :extracted_comment =>
187 # # "This is a comment to translators.\n" +
188 # # " This is also a comment for translators.",
189 # # >,
190 # # ]
191 #
192 # @param path [String] Ruby script path to be parsed
193 # @param options [Hash] Options
194 # @option options [String, nil] :comment_tag The tag to
195 # detect comments to be extracted. The extracted comments are
196 # used to deliver messages to translators from programmers.
197 #
198 # If the tag is String and a line in a comment start with the
199 # tag, the line and the following lines are extracted.
200 #
201 # If the tag is nil, all comments are extracted.
202 def initialize(path, options={})
203 @path = path
204 @options = options
205 end
206
207 # Extracts messages from @path.
208 #
209 # @return [Array<POEntry>] Extracted messages
210 def parse
211 source = IO.read(@path)
212
213 encoding = detect_encoding(source) || source.encoding
214 source.force_encoding(encoding)
215
216 parse_source(source)
217 end
218
219 def detect_encoding(source)
220 binary_source = source.dup.force_encoding("ASCII-8BIT")
221 if /\A.*coding\s*[=:]\s*([[:alnum:]\-_]+)/ =~ binary_source
222 $1.gsub(/-(?:unix|mac|dos)\z/, "")
223 else
224 nil
225 end
226 end
227
228 def parse_source(source)
229 po = []
230 file = StringIO.new(source)
231 rl = RubyLexX.new
232 rl.set_input(file)
233 rl.skip_space = true
234 #rl.readed_auto_clean_up = true
235
236 po_entry = nil
237 line_no = nil
238 last_comment = ""
239 reset_comment = false
240 ignore_next_comma = false
241 rl.parse do |tk|
242 begin
243 ignore_current_comma = ignore_next_comma
244 ignore_next_comma = false
245 case tk
246 when RubyToken::TkIDENTIFIER, RubyToken::TkCONSTANT
247 if store_po_entry(po, po_entry, line_no, last_comment)
248 last_comment = ""
249 end
250 if ID.include?(tk.name)
251 po_entry = POEntry.new(:normal)
252 elsif PLURAL_ID.include?(tk.name)
253 po_entry = POEntry.new(:plural)
254 elsif MSGCTXT_ID.include?(tk.name)
255 po_entry = POEntry.new(:msgctxt)
256 elsif MSGCTXT_PLURAL_ID.include?(tk.name)
257 po_entry = POEntry.new(:msgctxt_plural)
258 else
259 po_entry = nil
260 end
261 line_no = tk.line_no.to_s
262 when RubyToken::TkSTRING, RubyToken::TkDSTRING
263 po_entry.set_current_attribute tk.value if po_entry
264 when RubyToken::TkPLUS, RubyToken::TkNL
265 #do nothing
266 when RubyToken::TkINTEGER
267 ignore_next_comma = true
268 when RubyToken::TkCOMMA
269 unless ignore_current_comma
270 po_entry.advance_to_next_attribute if po_entry
271 end
272 else
273 if store_po_entry(po, po_entry, line_no, last_comment)
274 po_entry = nil
275 last_comment = ""
276 end
277 end
278 rescue
279 $stderr.print "\n\nError"
280 $stderr.print " parsing #{@path}:#{tk.line_no}\n\t #{source.lines.to_a[tk.line_no - 1]}" if tk
281 $stderr.print "\n #{$!.inspect} in\n"
282 $stderr.print $!.backtrace.join("\n")
283 $stderr.print "\n"
284 exit 1
285 end
286
287 case tk
288 when RubyToken::TkCOMMENT_WITH_CONTENT
289 last_comment = "" if reset_comment
290 if last_comment.empty?
291 comment1 = tk.value.lstrip
292 if comment_to_be_extracted?(comment1)
293 last_comment << comment1
294 end
295 else
296 last_comment += "\n"
297 last_comment += tk.value
298 end
299 reset_comment = false
300 when RubyToken::TkNL
301 else
302 reset_comment = true
303 end
304 end
305 po
306 end
307
308 private
309 def store_po_entry(po, po_entry, line_no, last_comment) #:nodoc:
310 if po_entry && po_entry.msgid
311 po_entry.references << @path + ":" + line_no
312 po_entry.add_comment(last_comment) unless last_comment.empty?
313 po << po_entry
314 true
315 else
316 false
317 end
318 end
319
320 def comment_to_be_extracted?(comment)
321 return false unless @options.has_key?(:comment_tag)
322
323 tag = @options[:comment_tag]
324 return true if tag.nil?
325
326 /\A#{Regexp.escape(tag)}/ === comment
327 end
328 end
329 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
3 #
4 # License: Ruby's or LGPL
5 #
6 # This library is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 require "rake"
20 require "gettext/tools"
21
22 module GetText
23 module Tools
24 class Task
25 class Error < StandardError
26 end
27
28 class ValidationError < Error
29 attr_reader :reasons
30 def initialize(reasons)
31 @reasons = reasons
32 lines = []
33 lines << "invalid configurations:"
34 @reasons.each do |variable, reason|
35 lines << "#{variable}: #{reason}"
36 end
37 super(lines.join("\n"))
38 end
39 end
40
41 include GetText
42 include Rake::DSL
43
44 class << self
45 # Define gettext related Rake tasks. Normally, use this method
46 # to define tasks because this method is a convenient API.
47 #
48 # See accessor APIs how to configure this task.
49 #
50 # See {#define} for what task is defined.
51 #
52 # @example Recommended usage
53 # require "gettext/tools/task"
54 # # Recommended usage
55 # GetText::Tools::Task.define do |task|
56 # task.spec = spec
57 # # ...
58 # end
59 # # Low level API
60 # task = GetText::Tools::Task.new
61 # task.spec = spec
62 # # ...
63 # task.define
64 #
65 # @yield [task] Gives the newely created task to the block.
66 # @yieldparam [GetText::Tools::Task] task The task that should be
67 # configured.
68 # @see {#define}
69 # @return [void]
70 def define
71 task = new
72 yield(task)
73 task.define
74 end
75 end
76
77 # @return [Gem::Specification, nil] Package information associated
78 # with the task.
79 attr_reader :spec
80
81 # @return [String, nil] Package name for messages.
82 attr_accessor :package_name
83
84 # @return [String, nil] Package version for messages.
85 attr_accessor :package_version
86
87 # It is a required parameter.
88 #
89 # @return [Array<String>] Supported locales. It is filled from
90 # {#po_base_directory} by default.
91 attr_accessor :locales
92 attr_accessor :po_base_directory
93 # @return [String] Base directory that has generated MOs. MOs
94 # are generated into
95 # `#{mo_base_directory}/#{locale}/LC_MESSAGES/#{domain}.mo`.
96 attr_accessor :mo_base_directory
97 # It is a required parameter.
98 #
99 # @return [Array<String>] Files that have messages.
100 attr_accessor :files
101 # It is a required parameter.
102 #
103 # @return [String] Text domain
104 attr_accessor :domain
105
106 # It is useful when you have multiple domains. You can define tasks
107 # for each domains by using different namespace prefix.
108 #
109 # It is `nil` by default. It means that tasks are defined at top
110 # level.
111 #
112 # TODO: example
113 #
114 # @return [String] Namespace prefix for tasks defined by this class.
115 attr_accessor :namespace_prefix
116
117 # @return [Array<String>] Command line options for extracting messages
118 # from sources.
119 # @see GetText::Tools::XGetText
120 # @see `rxgettext --help`
121 attr_accessor :xgettext_options
122
123 # @return [Array<String>] Command line options for merging PO with the
124 # latest POT.
125 # @see GetText::Tools::MsgMerge
126 # @see `rmsgmerge --help`
127 attr_accessor :msgmerge_options
128
129 # @return [Bool]
130 # @see #enable_description? For details.
131 attr_writer :enable_description
132
133 # @return [Bool]
134 # @see #enable_po? For details.
135 attr_writer :enable_po
136
137 # @param [Gem::Specification, nil] spec Package information associated
138 # with the task. Some information are extracted from the spec.
139 # @see #spec= What information are extracted from the spec.
140 def initialize(spec=nil)
141 initialize_variables
142 self.spec = spec
143 if spec
144 yield(self) if block_given?
145 warn("Use #{self.class.name}.define instead of #{self.class.name}.new(spec).")
146 define
147 end
148 end
149
150 # Sets package infromation by Gem::Specification. Here is a list
151 # for information extracted from the spec:
152 #
153 # * {#package_name}
154 # * {#package_version}
155 # * {#domain}
156 # * {#files}
157 #
158 # @param [Gem::Specification] spec package information for the
159 # i18n application.
160 def spec=(spec)
161 @spec = spec
162 return if @spec.nil?
163
164 @package_name = spec.name
165 @package_version = spec.version.to_s
166 @domain ||= spec.name
167 @files += target_files
168 end
169
170 # Define tasks from configured parameters.
171 #
172 # TODO: List defined Rake tasks.
173 def define
174 ensure_variables
175 validate
176
177 define_file_tasks
178 if namespace_prefix
179 namespace_recursive namespace_prefix do
180 define_named_tasks
181 end
182 else
183 define_named_tasks
184 end
185 end
186
187 # If it is true, each task has description. Otherwise, all tasks
188 # doesn't have description.
189 #
190 # @return [Bool]
191 # @since 3.0.1
192 def enable_description?
193 @enable_description
194 end
195
196 # If it is true, PO related tasks are defined. Otherwise, they
197 # are not defined.
198 #
199 # This parameter is useful to manage PO written by hand.
200 #
201 # @return [Bool]
202 # @since 3.0.1
203 def enable_po?
204 @enable_po
205 end
206
207 private
208 def initialize_variables
209 @spec = nil
210 @package_name = nil
211 @package_version = nil
212 @locales = []
213 @po_base_directory = "po"
214 @mo_base_directory = "locale"
215 @files = []
216 @domain = nil
217 @namespace_prefix = nil
218 @xgettext_options = []
219 @msgmerge_options = []
220 @enable_description = true
221 @enable_po = true
222 end
223
224 def ensure_variables
225 @locales = detect_locales if @locales.empty?
226 end
227
228 def validate
229 reasons = {}
230 if @locales.empty?
231 reasons["locales"] = "must set one or more locales"
232 end
233 if @enable_po and @files.empty?
234 reasons["files"] = "must set one or more files"
235 end
236 if @domain.nil?
237 reasons["domain"] = "must set domain"
238 end
239 raise ValidationError.new(reasons) unless reasons.empty?
240 end
241
242 def desc(*args)
243 return unless @enable_description
244 super
245 end
246
247 def define_file_tasks
248 define_pot_file_task
249
250 locales.each do |locale|
251 define_po_file_task(locale)
252 define_mo_file_task(locale)
253 end
254 end
255
256 def define_pot_file_task
257 return unless @enable_po
258
259 pot_dependencies = files.dup
260 unless File.exist?(po_base_directory)
261 directory po_base_directory
262 pot_dependencies << po_base_directory
263 end
264 file pot_file => pot_dependencies do
265 command_line = [
266 "--output", pot_file,
267 ]
268 if package_name
269 command_line.concat(["--package-name", package_name])
270 end
271 if package_version
272 command_line.concat(["--package-version", package_version])
273 end
274 command_line.concat(@xgettext_options)
275 command_line.concat(files)
276 GetText::Tools::XGetText.run(*command_line)
277 end
278 end
279
280 def define_po_file_task(locale)
281 return unless @enable_po
282
283 _po_file = po_file(locale)
284 po_dependencies = [pot_file]
285 _po_directory = po_directory(locale)
286 unless File.exist?(_po_directory)
287 directory _po_directory
288 po_dependencies << _po_directory
289 end
290 file _po_file => po_dependencies do
291 if File.exist?(_po_file)
292 command_line = [
293 "--update",
294 ]
295 command_line.concat(@msgmerge_options)
296 command_line.concat([_po_file, pot_file])
297 GetText::Tools::MsgMerge.run(*command_line)
298 else
299 GetText::Tools::MsgInit.run("--input", pot_file,
300 "--output", _po_file,
301 "--locale", locale.to_s)
302 end
303 end
304 end
305
306 def define_mo_file_task(locale)
307 _po_file = po_file(locale)
308 mo_dependencies = [_po_file]
309 _mo_directory = mo_directory(locale)
310 unless File.exist?(_mo_directory)
311 directory _mo_directory
312 mo_dependencies << _mo_directory
313 end
314 _mo_file = mo_file(locale)
315 file _mo_file => mo_dependencies do
316 GetText::Tools::MsgFmt.run(_po_file, "--output", _mo_file)
317 end
318 end
319
320 def define_named_tasks
321 namespace :gettext do
322 if @enable_po
323 define_pot_tasks
324 define_po_tasks
325 end
326
327 define_mo_tasks
328 end
329
330 desc "Update *.mo"
331 task :gettext => (current_scope + ["gettext", "mo", "update"]).join(":")
332 end
333
334 def define_pot_tasks
335 namespace :pot do
336 desc "Create #{pot_file}"
337 task :create => pot_file
338 end
339 end
340
341 def define_po_tasks
342 namespace :po do
343 desc "Add a new locale"
344 task :add, [:locale] do |_task, args|
345 locale = args.locale || ENV["LOCALE"]
346 if locale.nil?
347 raise "specify locale name by " +
348 "'rake #{_task.name}[${LOCALE}]' or " +
349 "rake #{_task.name} LOCALE=${LOCALE}'"
350 end
351 define_po_file_task(locale)
352 Rake::Task[po_file(locale)].invoke
353 end
354
355 update_tasks = []
356 @locales.each do |locale|
357 namespace locale do
358 desc "Update #{po_file(locale)}"
359 task :update => po_file(locale)
360 update_tasks << (current_scope + ["update"]).join(":")
361 end
362 end
363
364 desc "Update *.po"
365 task :update => update_tasks
366 end
367 end
368
369 def define_mo_tasks
370 namespace :mo do
371 update_tasks = []
372 @locales.each do |locale|
373 namespace locale do
374 desc "Update #{mo_file(locale)}"
375 task :update => mo_file(locale)
376 update_tasks << (current_scope + ["update"]).join(":")
377 end
378 end
379
380 desc "Update *.mo"
381 task :update => update_tasks
382 end
383 end
384
385 def pot_file
386 File.join(po_base_directory, "#{domain}.pot")
387 end
388
389 def po_directory(locale)
390 File.join(po_base_directory, locale.to_s)
391 end
392
393 def po_file(locale)
394 File.join(po_directory(locale), "#{domain}.po")
395 end
396
397 def mo_directory(locale)
398 File.join(mo_base_directory, locale.to_s, "LC_MESSAGES")
399 end
400
401 def mo_file(locale)
402 File.join(mo_directory(locale), "#{domain}.mo")
403 end
404
405 def target_files
406 files = @spec.files.find_all do |file|
407 /\A\.(?:rb|erb|glade)\z/i =~ File.extname(file)
408 end
409 files += @spec.executables.collect do |executable|
410 "bin/#{executable}"
411 end
412 files
413 end
414
415 def detect_locales
416 locales = []
417 return locales unless File.exist?(po_base_directory)
418
419 Dir.open(po_base_directory) do |dir|
420 dir.each do |entry|
421 next unless /\A[a-z]{2}(?:_[A-Z]{2})?\z/ =~ entry
422 next unless File.directory?(File.join(dir.path, entry))
423 locales << entry
424 end
425 end
426 locales
427 end
428
429 def current_scope
430 scope = Rake.application.current_scope
431 if scope.is_a?(Array)
432 scope
433 else
434 if scope.empty?
435 []
436 else
437 [scope.path]
438 end
439 end
440 end
441
442 def namespace_recursive(namespace_spec, &block)
443 first, rest = namespace_spec.split(/:/, 2)
444 namespace first do
445 if rest.nil?
446 block.call
447 else
448 namespace_recursive(rest, &block)
449 end
450 end
451 end
452 end
453 end
454 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
3 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
4 # Copyright (C) 2003-2010 Masao Mutoh
5 # Copyright (C) 2001,2002 Yasushi Shoji, Masao Mutoh
6 #
7 # License: Ruby's or LGPL
8 #
9 # This library is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Lesser General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This library is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Lesser General Public License for more details.
18 #
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 require "pathname"
23 require "optparse"
24 require "locale"
25 require "gettext"
26 require "gettext/po"
27
28 module GetText
29 module Tools
30 class XGetText
31 class << self
32 def run(*arguments)
33 new.run(*arguments)
34 end
35
36 # Adds a parser to the default parser list.
37 #
38 # @param (see #add_parser)
39 # @return [void]
40 #
41 # @see #add_parser
42 def add_parser(parser)
43 @@default_parsers.unshift(parser)
44 end
45 end
46
47 include GetText
48
49 bindtextdomain("gettext")
50
51 # @api private
52 @@default_parsers = []
53 builtin_parser_info_list = [
54 ["glade", "GladeParser"],
55 ["erb", "ErbParser"],
56 # ["ripper", "RipperParser"],
57 ["ruby", "RubyParser"] # Default parser.
58 ]
59 builtin_parser_info_list.each do |f, klass|
60 begin
61 require "gettext/tools/parser/#{f}"
62 @@default_parsers << GetText.const_get(klass)
63 rescue
64 $stderr.puts(_("'%{klass}' is ignored.") % {:klass => klass})
65 $stderr.puts($!) if $DEBUG
66 end
67 end
68
69 # @return [Hash<Symbol, Object>] Options for parsing. Options
70 # are depend on each parser.
71 # @see RubyParser#parse
72 # @see ErbParser#parse
73 attr_reader :parse_options
74
75 def initialize #:nodoc:
76 @parsers = @@default_parsers.dup
77
78 @input_files = nil
79 @output = nil
80
81 @package_name = "PACKAGE"
82 @package_version = "VERSION"
83 @msgid_bugs_address = ""
84 @copyright_holder = "THE PACKAGE'S COPYRIGHT HOLDER"
85 @copyright_year = "YEAR"
86 @output_encoding = "UTF-8"
87
88 @parse_options = {}
89
90 @po_order = :references
91 @po_format_options = {
92 :max_line_width => POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH,
93 }
94 end
95
96 # The parser object requires to have target?(path) and
97 # parse(path) method.
98 #
99 # @example How to add your parser
100 # require "gettext/tools/xgettext"
101 # class FooParser
102 # def target?(path)
103 # File.extname(path) == ".foo" # *.foo file only.
104 # end
105 # def parse(path, options={})
106 # po = []
107 # # Simple entry
108 # entry = POEntry.new(:normal)
109 # entry.msgid = "hello"
110 # entry.references = ["foo.rb:200", "bar.rb:300"]
111 # entry.add_comment("Comment for the entry")
112 # po << entry
113 # # Plural entry
114 # entry = POEntry.new(:plural)
115 # entry.msgid = "An apple"
116 # entry.msgid_plural = "Apples"
117 # entry.references = ["foo.rb:200", "bar.rb:300"]
118 # po << entry
119 # # Simple entry with the entry context
120 # entry = POEntry.new(:msgctxt)
121 # entry.msgctxt = "context"
122 # entry.msgid = "hello"
123 # entry.references = ["foo.rb:200", "bar.rb:300"]
124 # po << entry
125 # # Plural entry with the message context.
126 # entry = POEntry.new(:msgctxt_plural)
127 # entry.msgctxt = "context"
128 # entry.msgid = "An apple"
129 # entry.msgid_plural = "Apples"
130 # entry.references = ["foo.rb:200", "bar.rb:300"]
131 # po << entry
132 # return po
133 # end
134 # end
135 #
136 # GetText::Tools::XGetText.add_parser(FooParser.new)
137 #
138 # @param [#target?, #parse] parser
139 # It parses target file and extracts translate target entries from the
140 # target file. If there are multiple target files, parser.parse is
141 # called multiple times.
142 # @return [void]
143 def add_parser(parser)
144 @parsers.unshift(parser)
145 end
146
147 def run(*options) # :nodoc:
148 check_command_line_options(*options)
149
150 pot = generate_pot(@input_files)
151
152 if @output.is_a?(String)
153 File.open(File.expand_path(@output), "w+") do |file|
154 file.puts(pot)
155 end
156 else
157 @output.puts(pot)
158 end
159 self
160 end
161
162 def parse(paths) # :nodoc:
163 po = PO.new
164 paths = [paths] if paths.kind_of?(String)
165 paths.each do |path|
166 begin
167 parse_path(path, po)
168 rescue
169 puts(_("Error parsing %{path}") % {:path => path})
170 raise
171 end
172 end
173 po
174 end
175
176 private
177 def now
178 Time.now
179 end
180
181 def header_comment
182 <<-COMMENT
183 SOME DESCRIPTIVE TITLE.
184 Copyright (C) #{@copyright_year} #{@copyright_holder}
185 This file is distributed under the same license as the #{@package_name} package.
186 FIRST AUTHOR <EMAIL@ADDRESS>, #{@copyright_year}.
187
188 COMMENT
189 end
190
191 def header_content
192 time = now.strftime("%Y-%m-%d %H:%M%z")
193
194 <<-CONTENT
195 Project-Id-Version: #{@package_name} #{@package_version}
196 Report-Msgid-Bugs-To: #{@msgid_bugs_address}
197 POT-Creation-Date: #{time}
198 PO-Revision-Date: #{time}
199 Last-Translator: FULL NAME <EMAIL@ADDRESS>
200 Language-Team: LANGUAGE <LL@li.org>
201 Language:
202 MIME-Version: 1.0
203 Content-Type: text/plain; charset=#{@output_encoding}
204 Content-Transfer-Encoding: 8bit
205 Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;
206 CONTENT
207 end
208
209 def generate_pot(paths) # :nodoc:
210 header = POEntry.new(:normal)
211 header.msgid = ""
212 header.msgstr = header_content
213 header.translator_comment = header_comment
214 header.flags << "fuzzy"
215
216 po = parse(paths)
217 po.order = @po_order
218 po[header.msgid] = header
219
220 to_s_options = @po_format_options.merge(:encoding => @output_encoding)
221 po.to_s(to_s_options)
222 end
223
224 def check_command_line_options(*options) # :nodoc:
225 input_files, output = parse_arguments(*options)
226
227 if input_files.empty?
228 raise ArgumentError, _("no input files")
229 end
230
231 output ||= STDOUT
232
233 @input_files = input_files
234 @output = output
235 end
236
237 def parse_arguments(*options) #:nodoc:
238 output = nil
239
240 parser = OptionParser.new
241 banner = _("Usage: %s input.rb [-r parser.rb] [-o output.pot]") % $0
242 parser.banner = banner
243 parser.separator("")
244 description = _("Extract translatable strings from given input files.")
245 parser.separator(description)
246 parser.separator("")
247 parser.separator(_("Specific options:"))
248
249 parser.on("-o", "--output=FILE",
250 _("write output to specified file")) do |out|
251 output = out
252 end
253
254 parser.on("--package-name=NAME",
255 _("set package name in output"),
256 "(#{@package_name})") do |name|
257 @package_name = name
258 end
259
260 parser.on("--package-version=VERSION",
261 _("set package version in output"),
262 "(#{@package_version})") do |version|
263 @package_version = version
264 end
265
266 parser.on("--msgid-bugs-address=EMAIL",
267 _("set report e-mail address for msgid bugs"),
268 "(#{@msgid_bugs_address})") do |address|
269 @msgid_bugs_address = address
270 end
271
272 parser.on("--copyright-holder=HOLDER",
273 _("set copyright holder in output"),
274 "(#{@copyright_holder})") do |holder|
275 @copyright_holder = holder
276 end
277
278 parser.on("--copyright-year=YEAR",
279 _("set copyright year in output"),
280 "(#{@copyright_year})") do |year|
281 @copyright_year = year
282 end
283
284 parser.on("--output-encoding=ENCODING",
285 _("set encoding for output"),
286 "(#{@output_encoding})") do |encoding|
287 @output_encoding = encoding
288 end
289
290 parser.on("--[no-]sort-output",
291 _("Generate sorted output")) do |sort|
292 @po_order = sort ? :references : nil
293 end
294
295 parser.on("--[no-]sort-by-file",
296 _("Sort output by file location")) do |sort_by_file|
297 @po_order = sort_by_file ? :references : :msgid
298 end
299
300 parser.on("--[no-]sort-by-msgid",
301 _("Sort output by msgid")) do |sort_by_msgid|
302 @po_order = sort_by_msgid ? :msgid : :references
303 end
304
305 parser.on("--[no-]location",
306 _("Preserve '#: FILENAME:LINE' lines")) do |location|
307 @po_format_options[:include_reference_comment] = location
308 end
309
310 parser.on("--width=WIDTH", Integer,
311 _("Set output page width"),
312 "(#{@po_format_options[:max_line_width]})") do |width|
313 @po_format_options[:max_line_width] = width
314 end
315
316 parser.on("--[no-]wrap",
317 _("Break long message lines, longer than the output page width, into several lines"),
318 "(#{@po_format_options[:max_line_width] >= 0})") do |wrap|
319 if wrap
320 max_line_width = POEntry::Formatter::DEFAULT_MAX_LINE_WIDTH
321 else
322 max_line_width = -1
323 end
324 @po_format_options[:max_line_width] = max_line_width
325 end
326
327 parser.on("-r", "--require=library",
328 _("require the library before executing xgettext")) do |out|
329 require out
330 end
331
332 parser.on("-c", "--add-comments[=TAG]",
333 _("If TAG is specified, place comment blocks starting with TAG and precedding keyword lines in output file"),
334 _("If TAG is not specified, place all comment blocks preceing keyword lines in output file"),
335 _("(default: %s)") % _("no TAG")) do |tag|
336 @parse_options[:comment_tag] = tag
337 end
338
339 parser.on("-d", "--debug", _("run in debugging mode")) do
340 $DEBUG = true
341 end
342
343 parser.on("-h", "--help", _("display this help and exit")) do
344 puts(parser.help)
345 exit(true)
346 end
347
348 parser.on_tail("--version", _("display version information and exit")) do
349 puts(GetText::VERSION)
350 exit(true)
351 end
352
353 parser.parse!(options)
354
355 [options, output]
356 end
357
358 def parse_path(path, po)
359 @parsers.each do |parser|
360 next unless parser.target?(path)
361
362 # For backward compatibility
363 if parser.method(:parse).arity == 1 or @parse_options.empty?
364 extracted_po = parser.parse(path)
365 else
366 extracted_po = parser.parse(path, @parse_options)
367 end
368 extracted_po.each do |po_entry|
369 if po_entry.kind_of?(Array)
370 po_entry = create_po_entry(*po_entry)
371 end
372
373 if po_entry.msgid.empty?
374 warn _("Warning: The empty \"\" msgid is reserved by " +
375 "gettext. So gettext(\"\") doesn't returns " +
376 "empty string but the header entry in po file.")
377 # TODO: add pommesage.reference to the pot header as below:
378 # # SOME DESCRIPTIVE TITLE.
379 # # Copyright (C) YEAR THE COPYRIGHT HOLDER
380 # # This file is distributed under the same license as the PACKAGE package.
381 # # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
382 # #
383 # #: test/test_gettext.rb:65
384 # #, fuzzy
385 # "#: test/test_gettext.rb:65" line is added.
386 next
387 end
388
389 if @output.is_a?(String)
390 base_path = Pathname.new(@output).dirname.expand_path
391 po_entry.references = po_entry.references.collect do |reference|
392 path, line, = reference.split(/:(\d+)\z/, 2)
393 absolute_path = Pathname.new(path).expand_path
394 begin
395 path = absolute_path.relative_path_from(base_path).to_s
396 rescue ArgumentError
397 raise # Should we ignore it?
398 end
399 "#{path}:#{line}"
400 end
401 end
402
403 existing_entry = po[po_entry.msgctxt, po_entry.msgid]
404 if existing_entry
405 po_entry = existing_entry.merge(po_entry)
406 end
407 po[po_entry.msgctxt, po_entry.msgid] = po_entry
408 end
409 break
410 end
411 end
412
413 def create_po_entry(msgid, *references)
414 type = :normal
415 msgctxt = nil
416 msgid_plural = nil
417
418 if msgid.include?("\004")
419 msgctxt, msgid = msgid.split(/\004/, 2)
420 type = :msgctxt
421 end
422 if msgid.include?("\000")
423 msgid, msgid_plural = msgid.split(/\000/, 2)
424 if type == :msgctxt
425 type = :msgctxt_plural
426 else
427 type = :plural
428 end
429 end
430
431 po_entry = POEntry.new(type)
432 po_entry.msgid = msgid
433 po_entry.msgctxt = msgctxt
434 po_entry.msgid_plural = msgid_plural
435 po_entry.references = references
436 po_entry
437 end
438 end
439 end
440 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2005-2008 Masao Mutoh
4 #
5 # License: Ruby's or LGPL
6 #
7 # This library is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Lesser General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This library is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Lesser General Public License for more details.
16 #
17 # You should have received a copy of the GNU Lesser General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 require "gettext/tools/xgettext"
21 require "gettext/tools/msgfmt"
22 require "gettext/tools/msginit"
23 require "gettext/tools/msgmerge"
24 require "gettext/tools/msgcat"
25 require "gettext/mo"
0 =begin
1 version - version information of gettext
2
3 Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
4 Copyright (C) 2005-2009 Masao Mutoh
5
6 You may redistribute it and/or modify it under the same
7 license terms as Ruby or LGPL.
8 =end
9
10 module GetText
11 VERSION = "3.0.9"
12 end
0 # -*- coding: utf-8 -*-
1
2 =begin
3 gettext.rb - GetText module
4
5 Copyright (C) 2001-2010 Masao Mutoh
6 Copyright (C) 2001-2003 Masahiro Sakai
7
8 Masao Mutoh <mutomasa at gmail.com>
9 Masahiro Sakai <s01397ms@sfc.keio.ac.jp>
10
11 You may redistribute it and/or modify it under the same
12 license terms as Ruby or LGPL.
13 =end
14
15 require 'locale'
16
17 require 'gettext/version'
18 require 'gettext/text_domain_manager'
19
20 module GetText
21 # If the text domain isn't bound when calling GetText.textdomain, this error is raised.
22 class NoboundTextDomainError < RuntimeError
23 def initialize(domainname)
24 @domainname = domainname
25 end
26 def message
27 "#{@domainname} is not bound."
28 end
29 end
30
31 extend self
32
33 def self.included(mod) #:nodoc:
34 mod.extend self
35 end
36
37 # bindtextdomain(domainname, options = {})
38 #
39 # Bind a text domain(%{path}/%{locale}/LC_MESSAGES/%{domainname}.mo) to
40 # your program.
41 # Normally, the texdomain scope becomes the class/module(and parent
42 # classes/included modules).
43 #
44 # * domainname: the text domain name.
45 # * options: options as an Hash.
46 # * :path - the path to the mo-files. When the value is nil, it will search default paths such as
47 # /usr/share/locale, /usr/local/share/locale)
48 # * :output_charset - The output charset. Same with GetText.set_output_charset. Usually, L10n
49 # library doesn't use this option. Application may use this once.
50 # * Returns: the GetText::TextDomainManager.
51 #
52 def bindtextdomain(domainname, *options)
53 bindtextdomain_to(self, domainname, *options)
54 end
55
56 # Includes GetText module and bind a text domain to a class.
57 # * klass: the target ruby class.
58 # * domainname: the text domain name.
59 # * options: options as an Hash. See GetText.bindtextdomain.
60 def bindtextdomain_to(klass, domainname, *options)
61 if options[0].kind_of? Hash
62 opts = options[0]
63 else
64 # for backward compatibility.
65 opts = {}
66 opts[:path] = options[0] if options[0]
67 opts[:output_charset] = options[2] if options[2]
68 end
69 unless (klass.kind_of? GetText or klass.include? GetText)
70 klass.__send__(:include, GetText)
71 end
72 TextDomainManager.bind_to(klass, domainname, opts)
73 end
74
75 # Binds a existed text domain to your program.
76 # This is the same function with GetText.bindtextdomain but simpler(and faster) than bindtextdomain.
77 # Note that you need to call GetText.bindtextdomain first. If the domainname hasn't bound yet,
78 # raises GetText::NoboundTextDomainError.
79 # * domainname: a text domain name.
80 # * Returns: the GetText::TextDomainManager.
81 def textdomain(domainname) #:nodoc:
82 textdomain_to(self, domainname)
83 end
84
85 # Includes GetText module and bind an exsited text domain to a class.
86 # See text domain for more detail.
87 # * klass: the target ruby class.
88 # * domainname: the text domain name.
89
90 def textdomain_to(klass, domainname) #:nodoc:
91 domain = TextDomainManager.text_domain_pool(domainname)
92 raise NoboundTextDomainError.new(domainname) unless domain
93 bindtextdomain_to(klass, domainname)
94 end
95
96 # call-seq:
97 # gettext(msgid)
98 # _(msgid)
99 #
100 # Translates msgid and return the message.
101 # This doesn't make a copy of the message.
102 #
103 # You need to use String#dup if you want to modify the return value
104 # with destructive functions.
105 #
106 # (e.g.1) _("Hello ").dup << "world"
107 #
108 # But e.g.1 should be rewrite to:
109 #
110 # (e.g.2) _("Hello %{val}") % {:val => "world"}
111 #
112 # Because the translator may want to change the position of "world".
113 #
114 # * msgid: the message id.
115 # * Returns: localized text by msgid. If there are not binded mo-file, it will return msgid.
116 def gettext(msgid)
117 TextDomainManager.translate_singular_message(self, msgid)
118 end
119
120 # call-seq:
121 # sgettext(msgid, div = '|')
122 # s_(msgid, div = '|')
123 #
124 # Translates msgid, but if there are no localized text,
125 # it returns a last part of msgid separeted "div".
126 #
127 # * msgid: the message id.
128 # * separator: separator or nil for no seperation.
129 # * Returns: the localized text by msgid. If there are no localized text,
130 # it returns a last part of the msgid separeted by "seperator".
131 # <tt>Movie|Location -> Location</tt>
132 # See: http://www.gnu.org/software/gettext/manual/html_mono/gettext.html#SEC151
133 def sgettext(msgid, seperator = "|")
134 TextDomainManager.translate_singular_message(self, msgid, seperator)
135 end
136
137 # call-seq:
138 # pgettext(msgctxt, msgid)
139 # p_(msgctxt, msgid)
140 #
141 # Translates msgid with msgctxt. This methods is similer with s_().
142 # e.g.) p_("File", "New") == s_("File|New")
143 # p_("File", "Open") == s_("File|Open")
144 #
145 # * msgctxt: the message context.
146 # * msgid: the message id.
147 # * Returns: the localized text by msgid. If there are no localized text,
148 # it returns msgid.
149 # See: http://www.gnu.org/software/autoconf/manual/gettext/Contexts.html
150 def pgettext(msgctxt, msgid)
151 TextDomainManager.translate_singular_message(self, "#{msgctxt}\004#{msgid}", "\004")
152 end
153
154 # call-seq:
155 # ngettext(msgid, msgid_plural, n)
156 # ngettext(msgids, n) # msgids = [msgid, msgid_plural]
157 # n_(msgid, msgid_plural, n)
158 # n_(msgids, n) # msgids = [msgid, msgid_plural]
159 #
160 # The ngettext is similar to the gettext function as it finds the message catalogs in the same way.
161 # But it takes two extra arguments for plural form.
162 #
163 # * msgid: the singular form.
164 # * msgid_plural: the plural form.
165 # * n: a number used to determine the plural form.
166 # * Returns: the localized text which key is msgid_plural if n is plural(follow plural-rule) or msgid.
167 # "plural-rule" is defined in po-file.
168 def ngettext(msgid, msgid_plural, n = nil)
169 TextDomainManager.translate_plural_message(self, msgid, msgid_plural, n)
170 end
171
172 # call-seq:
173 # nsgettext(msgid, msgid_plural, n, div = "|")
174 # nsgettext(msgids, n, div = "|") # msgids = [msgid, msgid_plural]
175 # ns_(msgid, msgid_plural, n, div = "|")
176 # ns_(msgids, n, div = "|") # msgids = [msgid, msgid_plural]
177 #
178 # The nsgettext is similar to the ngettext.
179 # But if there are no localized text,
180 # it returns a last part of msgid separeted "div".
181 #
182 # * msgid: the singular form with "div". (e.g. "Special|An apple")
183 # * msgid_plural: the plural form. (e.g. "%{num} Apples")
184 # * n: a number used to determine the plural form.
185 # * Returns: the localized text which key is msgid_plural if n is plural(follow plural-rule) or msgid.
186 # "plural-rule" is defined in po-file.
187 def nsgettext(msgid, msgid_plural, n="|", seperator = "|")
188 TextDomainManager.translate_plural_message(self, msgid, msgid_plural, n, seperator)
189 end
190
191 # call-seq:
192 # npgettext(msgctxt, msgid, msgid_plural, n)
193 # npgettext(msgctxt, msgids, n) # msgids = [msgid, msgid_plural]
194 # np_(msgctxt, msgid, msgid_plural, n)
195 # np_(msgctxt, msgids, n) # msgids = [msgid, msgid_plural]
196 #
197 # The npgettext is similar to the nsgettext function.
198 # e.g.) np_("Special", "An apple", "%{num} Apples", num) == ns_("Special|An apple", "%{num} Apples", num)
199 # * msgctxt: the message context.
200 # * msgid: the singular form.
201 # * msgid_plural: the plural form.
202 # * n: a number used to determine the plural form.
203 # * Returns: the localized text which key is msgid_plural if n is plural(follow plural-rule) or msgid.
204 # "plural-rule" is defined in po-file.
205 def npgettext(msgctxt, msgids, arg2 = nil, arg3 = nil)
206 if msgids.kind_of?(Array)
207 msgid = msgids[0]
208 msgid_ctxt = "#{msgctxt}\004#{msgid}"
209 msgid_plural = msgids[1]
210 opt1 = arg2
211 opt2 = arg3
212 else
213 msgid = msgids
214 msgid_ctxt = "#{msgctxt}\004#{msgid}"
215 msgid_plural = arg2
216 opt1 = arg3
217 opt2 = nil
218 end
219
220 msgstr = TextDomainManager.translate_plural_message(self, msgid_ctxt, msgid_plural, opt1, opt2)
221 if msgstr == msgid_ctxt
222 msgid
223 else
224 msgstr
225 end
226 end
227
228 # makes dynamic translation messages readable for the gettext parser.
229 # <tt>_(fruit)</tt> cannot be understood by the gettext parser. To help the parser find all your translations,
230 # you can add <tt>fruit = N_("Apple")</tt> which does not translate, but tells the parser: "Apple" needs translation.
231 # * msgid: the message id.
232 # * Returns: msgid.
233 def N_(msgid)
234 msgid
235 end
236
237 # This is same function as N_ but for ngettext.
238 # * msgid: the message id.
239 # * msgid_plural: the plural message id.
240 # * Returns: msgid.
241 def Nn_(msgid, msgid_plural)
242 [msgid, msgid_plural]
243 end
244
245 # Sets charset(String) such as "euc-jp", "sjis", "CP932", "utf-8", ...
246 # You shouldn't use this in your own Libraries.
247 # * charset: an output_charset
248 # * Returns: self
249 def set_output_charset(charset)
250 TextDomainManager.output_charset = charset
251 self
252 end
253
254 # Gets the current output_charset which is set using GetText.set_output_charset.
255 # * Returns: output_charset.
256 def output_charset
257 TextDomainManager.output_charset
258 end
259
260 # Set the locale. This value forces the locale whole the programs.
261 # This method calls Locale.set_app_language_tags, Locale.default, Locale.current.
262 # Use Locale methods if you need to handle locales more flexible.
263 def set_locale(lang)
264 Locale.set_app_language_tags(lang)
265 Locale.default = lang
266 Locale.current = lang
267 end
268
269 # Set the locale to the current thread.
270 # Note that if #set_locale is set, this value is ignored.
271 # If you need, set_locale(nil); set_current_locale(lang)
272 def set_current_locale(lang)
273 Locale.current = lang
274 end
275
276 def locale
277 Locale.current[0]
278 end
279
280 alias :locale= :set_locale #:nodoc:
281 alias :current_locale= :set_current_locale #:nodoc:
282 alias :_ :gettext #:nodoc:
283 alias :n_ :ngettext #:nodoc:
284 alias :s_ :sgettext #:nodoc:
285 alias :ns_ :nsgettext #:nodoc:
286 alias :np_ :npgettext #:nodoc:
287
288 alias :output_charset= :set_output_charset #:nodoc:
289
290 unless defined? XX
291 # This is the workaround to conflict p_ methods with the xx("double x") library.
292 # http://rubyforge.org/projects/codeforpeople/
293 alias :p_ :pgettext #:nodoc:
294 end
295 end
0 require 'hmac'
1 require 'digest/md5'
2
3 module HMAC
4 class MD5 < Base
5 def initialize(key = nil)
6 super(Digest::MD5, 64, 16, key)
7 end
8 public_class_method :new, :digest, :hexdigest
9 end
10 end
0 require 'hmac'
1 require 'digest/rmd160'
2
3 module HMAC
4 class RMD160 < Base
5 def initialize(key = nil)
6 super(Digest::RMD160, 64, 20, key)
7 end
8 public_class_method :new, :digest, :hexdigest
9 end
10 end
0 require 'hmac'
1 require 'digest/sha1'
2
3 module HMAC
4 class SHA1 < Base
5 def initialize(key = nil)
6 super(Digest::SHA1, 64, 20, key)
7 end
8 public_class_method :new, :digest, :hexdigest
9 end
10 end
0 require 'hmac'
1 require 'digest/sha2'
2
3 module HMAC
4 class SHA256 < Base
5 def initialize(key = nil)
6 super(Digest::SHA256, 64, 32, key)
7 end
8 public_class_method :new, :digest, :hexdigest
9 end
10
11 class SHA384 < Base
12 def initialize(key = nil)
13 super(Digest::SHA384, 128, 48, key)
14 end
15 public_class_method :new, :digest, :hexdigest
16 end
17
18 class SHA512 < Base
19 def initialize(key = nil)
20 super(Digest::SHA512, 128, 64, key)
21 end
22 public_class_method :new, :digest, :hexdigest
23 end
24 end
0 # Copyright (C) 2001 Daiki Ueno <ueno@unixuser.org>
1 # This library is distributed under the terms of the Ruby license.
2
3 # This module provides common interface to HMAC engines.
4 # HMAC standard is documented in RFC 2104:
5 #
6 # H. Krawczyk et al., "HMAC: Keyed-Hashing for Message Authentication",
7 # RFC 2104, February 1997
8 #
9 # These APIs are inspired by JCE 1.2's javax.crypto.Mac interface.
10 #
11 # <URL:http://java.sun.com/security/JCE1.2/spec/apidoc/javax/crypto/Mac.html>
12 #
13 # Source repository is at
14 #
15 # http://github.com/topfunky/ruby-hmac/tree/master
16
17 module HMAC
18
19 VERSION = '0.4.0'
20
21 class Base
22 def initialize(algorithm, block_size, output_length, key)
23 @algorithm = algorithm
24 @block_size = block_size
25 @output_length = output_length
26 @initialized = false
27 @key_xor_ipad = ''
28 @key_xor_opad = ''
29 set_key(key) unless key.nil?
30 end
31
32 private
33 def check_status
34 unless @initialized
35 raise RuntimeError,
36 "The underlying hash algorithm has not yet been initialized."
37 end
38 end
39
40 public
41 def set_key(key)
42 # If key is longer than the block size, apply hash function
43 # to key and use the result as a real key.
44 key = @algorithm.digest(key) if key.size > @block_size
45 akey = key.unpack("C*")
46 key_xor_ipad = ("\x36" * @block_size).unpack("C*")
47 key_xor_opad = ("\x5C" * @block_size).unpack("C*")
48 for i in 0 .. akey.size - 1
49 key_xor_ipad[i] ^= akey[i]
50 key_xor_opad[i] ^= akey[i]
51 end
52 @key_xor_ipad = key_xor_ipad.pack("C*")
53 @key_xor_opad = key_xor_opad.pack("C*")
54 @md = @algorithm.new
55 @initialized = true
56 end
57
58 def reset_key
59 @key_xor_ipad.gsub!(/./, '?')
60 @key_xor_opad.gsub!(/./, '?')
61 @key_xor_ipad[0..-1] = ''
62 @key_xor_opad[0..-1] = ''
63 @initialized = false
64 end
65
66 def update(text)
67 check_status
68 # perform inner H
69 md = @algorithm.new
70 md.update(@key_xor_ipad)
71 md.update(text)
72 str = md.digest
73 # perform outer H
74 md = @algorithm.new
75 md.update(@key_xor_opad)
76 md.update(str)
77 @md = md
78 end
79 alias << update
80
81 def digest
82 check_status
83 @md.digest
84 end
85
86 def hexdigest
87 check_status
88 @md.hexdigest
89 end
90 alias to_s hexdigest
91
92 # These two class methods below are safer than using above
93 # instance methods combinatorially because an instance will have
94 # held a key even if it's no longer in use.
95 def Base.digest(key, text)
96 hmac = self.new(key)
97 begin
98 hmac.update(text)
99 hmac.digest
100 ensure
101 hmac.reset_key
102 end
103 end
104
105 def Base.hexdigest(key, text)
106 hmac = self.new(key)
107 begin
108 hmac.update(text)
109 hmac.hexdigest
110 ensure
111 hmac.reset_key
112 end
113 end
114
115 private_class_method :new, :digest, :hexdigest
116 end
117 end
Binary diff not shown
0 module InstanceStorage
1 VERSION = "1.0.0"
2 end
0 # -*- coding: utf-8 -*-
1 require "instance_storage/version"
2
3 # クラスに、インスタンスの辞書をもたせる。
4 # このモジュールをincludeすると、全てのインスタンスは一意な名前(Symbol)をもつようになり、
5 # その名前を通してインスタンスを取得することができるようになる。
6 module InstanceStorage
7
8 attr_reader :name
9
10 alias to_sym name
11
12 def self.included(klass)
13 super
14 klass.class_eval do
15 extend InstanceStorageExtend
16 end
17 end
18
19 def initialize(name)
20 @name = name end
21
22 # 名前を文字列にして返す
23 # ==== Return
24 # 名前文字列
25 def to_s
26 @name.to_s end
27
28 module InstanceStorageExtend
29 def instances_dict
30 @instances ||= {} end
31
32 def storage_lock
33 @storage_lock ||= Mutex.new end
34
35 # 定義されているインスタンスを全て削除する
36 def clear!
37 @instances = @storage_lock = nil end
38
39 # インスタンス _event_name_ を返す。既に有る場合はそのインスタンス、ない場合は新しく作って返す。
40 # ==== Args
41 # [name] インスタンスの名前(Symbol)
42 # ==== Return
43 # Event
44 def [](name)
45 name_sym = name.to_sym
46 if instances_dict.has_key?(name_sym)
47 instances_dict[name_sym]
48 else
49 storage_lock.synchronize{
50 if instances_dict.has_key?(name_sym)
51 instances_dict[name_sym]
52 else
53 instances_dict[name_sym] = self.new(name_sym) end } end end
54
55 # このクラスのインスタンスを全て返す
56 # ==== Return
57 # インスタンスの配列(Array)
58 def instances
59 instances_dict.values end
60
61 # このクラスのインスタンスの名前を全て返す
62 # ==== Return
63 # インスタンスの名前の配列(Array)
64 def instances_name
65 instances_dict.keys end
66
67 # 名前 _name_ に対応するインスタンスが存在するか否かを返す
68 # ==== Args
69 # [name] インスタンスの名前(Symbol)
70 # ==== Return
71 # インスタンスが存在するなら真
72 def instance_exist?(name)
73 instances_dict.has_key? name.to_sym end
74
75 # _name_ に対応するインスタンスが既にあれば真
76 # ==== Args
77 # [name] インスタンスの名前(Symbol)
78 # ==== Return
79 # インスタンスかnil
80 def instance(name)
81 instances_dict[name.to_sym] end
82
83 def destroy(name)
84 instances_dict.delete(name.to_sym) end
85 end
86 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 defined?(::BigDecimal) or require 'bigdecimal'
4
5 class BigDecimal
6 # Import a JSON Marshalled object.
7 #
8 # method used for JSON marshalling support.
9 def self.json_create(object)
10 BigDecimal._load object['b']
11 end
12
13 # Marshal the object to JSON.
14 #
15 # method used for JSON marshalling support.
16 def as_json(*)
17 {
18 JSON.create_id => self.class.name,
19 'b' => _dump,
20 }
21 end
22
23 # return the JSON value
24 def to_json(*)
25 as_json.to_json
26 end
27 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 defined?(::Complex) or require 'complex'
4
5 class Complex
6
7 # Deserializes JSON string by converting Real value <tt>r</tt>, imaginary
8 # value <tt>i</tt>, to a Complex object.
9 def self.json_create(object)
10 Complex(object['r'], object['i'])
11 end
12
13 # Returns a hash, that will be turned into a JSON object and represent this
14 # object.
15 def as_json(*)
16 {
17 JSON.create_id => self.class.name,
18 'r' => real,
19 'i' => imag,
20 }
21 end
22
23 # Stores class name (Complex) along with real value <tt>r</tt> and imaginary value <tt>i</tt> as JSON string
24 def to_json(*)
25 as_json.to_json
26 end
27 end
0 # This file requires the implementations of ruby core's custom objects for
1 # serialisation/deserialisation.
2
3 require 'json/add/date'
4 require 'json/add/date_time'
5 require 'json/add/exception'
6 require 'json/add/range'
7 require 'json/add/regexp'
8 require 'json/add/struct'
9 require 'json/add/symbol'
10 require 'json/add/time'
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 require 'date'
4
5 # Date serialization/deserialization
6 class Date
7
8 # Deserializes JSON string by converting Julian year <tt>y</tt>, month
9 # <tt>m</tt>, day <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> to Date.
10 def self.json_create(object)
11 civil(*object.values_at('y', 'm', 'd', 'sg'))
12 end
13
14 alias start sg unless method_defined?(:start)
15
16 # Returns a hash, that will be turned into a JSON object and represent this
17 # object.
18 def as_json(*)
19 {
20 JSON.create_id => self.class.name,
21 'y' => year,
22 'm' => month,
23 'd' => day,
24 'sg' => start,
25 }
26 end
27
28 # Stores class name (Date) with Julian year <tt>y</tt>, month <tt>m</tt>, day
29 # <tt>d</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
30 def to_json(*args)
31 as_json.to_json(*args)
32 end
33 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 require 'date'
4
5 # DateTime serialization/deserialization
6 class DateTime
7
8 # Deserializes JSON string by converting year <tt>y</tt>, month <tt>m</tt>,
9 # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
10 # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> to DateTime.
11 def self.json_create(object)
12 args = object.values_at('y', 'm', 'd', 'H', 'M', 'S')
13 of_a, of_b = object['of'].split('/')
14 if of_b and of_b != '0'
15 args << Rational(of_a.to_i, of_b.to_i)
16 else
17 args << of_a
18 end
19 args << object['sg']
20 civil(*args)
21 end
22
23 alias start sg unless method_defined?(:start)
24
25 # Returns a hash, that will be turned into a JSON object and represent this
26 # object.
27 def as_json(*)
28 {
29 JSON.create_id => self.class.name,
30 'y' => year,
31 'm' => month,
32 'd' => day,
33 'H' => hour,
34 'M' => min,
35 'S' => sec,
36 'of' => offset.to_s,
37 'sg' => start,
38 }
39 end
40
41 # Stores class name (DateTime) with Julian year <tt>y</tt>, month <tt>m</tt>,
42 # day <tt>d</tt>, hour <tt>H</tt>, minute <tt>M</tt>, second <tt>S</tt>,
43 # offset <tt>of</tt> and Day of Calendar Reform <tt>sg</tt> as JSON string
44 def to_json(*args)
45 as_json.to_json(*args)
46 end
47 end
48
49
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Exception serialization/deserialization
5 class Exception
6
7 # Deserializes JSON string by constructing new Exception object with message
8 # <tt>m</tt> and backtrace <tt>b</tt> serialized with <tt>to_json</tt>
9 def self.json_create(object)
10 result = new(object['m'])
11 result.set_backtrace object['b']
12 result
13 end
14
15 # Returns a hash, that will be turned into a JSON object and represent this
16 # object.
17 def as_json(*)
18 {
19 JSON.create_id => self.class.name,
20 'm' => message,
21 'b' => backtrace,
22 }
23 end
24
25 # Stores class name (Exception) with message <tt>m</tt> and backtrace array
26 # <tt>b</tt> as JSON string
27 def to_json(*args)
28 as_json.to_json(*args)
29 end
30 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 require 'ostruct'
4
5 # OpenStruct serialization/deserialization
6 class OpenStruct
7
8 # Deserializes JSON string by constructing new Struct object with values
9 # <tt>v</tt> serialized by <tt>to_json</tt>.
10 def self.json_create(object)
11 new(object['t'] || object[:t])
12 end
13
14 # Returns a hash, that will be turned into a JSON object and represent this
15 # object.
16 def as_json(*)
17 klass = self.class.name
18 klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
19 {
20 JSON.create_id => klass,
21 't' => table,
22 }
23 end
24
25 # Stores class name (OpenStruct) with this struct's values <tt>v</tt> as a
26 # JSON string.
27 def to_json(*args)
28 as_json.to_json(*args)
29 end
30 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Range serialization/deserialization
5 class Range
6
7 # Deserializes JSON string by constructing new Range object with arguments
8 # <tt>a</tt> serialized by <tt>to_json</tt>.
9 def self.json_create(object)
10 new(*object['a'])
11 end
12
13 # Returns a hash, that will be turned into a JSON object and represent this
14 # object.
15 def as_json(*)
16 {
17 JSON.create_id => self.class.name,
18 'a' => [ first, last, exclude_end? ]
19 }
20 end
21
22 # Stores class name (Range) with JSON array of arguments <tt>a</tt> which
23 # include <tt>first</tt> (integer), <tt>last</tt> (integer), and
24 # <tt>exclude_end?</tt> (boolean) as JSON string.
25 def to_json(*args)
26 as_json.to_json(*args)
27 end
28 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3 defined?(::Rational) or require 'rational'
4
5 class Rational
6 # Deserializes JSON string by converting numerator value <tt>n</tt>,
7 # denominator value <tt>d</tt>, to a Rational object.
8 def self.json_create(object)
9 Rational(object['n'], object['d'])
10 end
11
12 # Returns a hash, that will be turned into a JSON object and represent this
13 # object.
14 def as_json(*)
15 {
16 JSON.create_id => self.class.name,
17 'n' => numerator,
18 'd' => denominator,
19 }
20 end
21
22 # Stores class name (Rational) along with numerator value <tt>n</tt> and denominator value <tt>d</tt> as JSON string
23 def to_json(*)
24 as_json.to_json
25 end
26 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Regexp serialization/deserialization
5 class Regexp
6
7 # Deserializes JSON string by constructing new Regexp object with source
8 # <tt>s</tt> (Regexp or String) and options <tt>o</tt> serialized by
9 # <tt>to_json</tt>
10 def self.json_create(object)
11 new(object['s'], object['o'])
12 end
13
14 # Returns a hash, that will be turned into a JSON object and represent this
15 # object.
16 def as_json(*)
17 {
18 JSON.create_id => self.class.name,
19 'o' => options,
20 's' => source,
21 }
22 end
23
24 # Stores class name (Regexp) with options <tt>o</tt> and source <tt>s</tt>
25 # (Regexp or String) as JSON string
26 def to_json(*)
27 as_json.to_json
28 end
29 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Struct serialization/deserialization
5 class Struct
6
7 # Deserializes JSON string by constructing new Struct object with values
8 # <tt>v</tt> serialized by <tt>to_json</tt>.
9 def self.json_create(object)
10 new(*object['v'])
11 end
12
13 # Returns a hash, that will be turned into a JSON object and represent this
14 # object.
15 def as_json(*)
16 klass = self.class.name
17 klass.to_s.empty? and raise JSON::JSONError, "Only named structs are supported!"
18 {
19 JSON.create_id => klass,
20 'v' => values,
21 }
22 end
23
24 # Stores class name (Struct) with Struct values <tt>v</tt> as a JSON string.
25 # Only named structs are supported.
26 def to_json(*args)
27 as_json.to_json(*args)
28 end
29 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Symbol serialization/deserialization
5 class Symbol
6 # Returns a hash, that will be turned into a JSON object and represent this
7 # object.
8 def as_json(*)
9 {
10 JSON.create_id => self.class.name,
11 's' => to_s,
12 }
13 end
14
15 # Stores class name (Symbol) with String representation of Symbol as a JSON string.
16 def to_json(*a)
17 as_json.to_json(*a)
18 end
19
20 # Deserializes JSON string by converting the <tt>string</tt> value stored in the object to a Symbol
21 def self.json_create(o)
22 o['s'].to_sym
23 end
24 end
0 unless defined?(::JSON::JSON_LOADED) and ::JSON::JSON_LOADED
1 require 'json'
2 end
3
4 # Time serialization/deserialization
5 class Time
6
7 # Deserializes JSON string by converting time since epoch to Time
8 def self.json_create(object)
9 if usec = object.delete('u') # used to be tv_usec -> tv_nsec
10 object['n'] = usec * 1000
11 end
12 if method_defined?(:tv_nsec)
13 at(object['s'], Rational(object['n'], 1000))
14 else
15 at(object['s'], object['n'] / 1000)
16 end
17 end
18
19 # Returns a hash, that will be turned into a JSON object and represent this
20 # object.
21 def as_json(*)
22 nanoseconds = [ tv_usec * 1000 ]
23 respond_to?(:tv_nsec) and nanoseconds << tv_nsec
24 nanoseconds = nanoseconds.max
25 {
26 JSON.create_id => self.class.name,
27 's' => tv_sec,
28 'n' => nanoseconds,
29 }
30 end
31
32 # Stores class name (Time) with number of seconds since epoch and number of
33 # microseconds for Time as JSON string
34 def to_json(*args)
35 as_json.to_json(*args)
36 end
37 end
0 require 'json/version'
1 require 'json/generic_object'
2
3 module JSON
4 class << self
5 # If _object_ is string-like, parse the string and return the parsed result
6 # as a Ruby data structure. Otherwise generate a JSON text from the Ruby
7 # data structure object and return it.
8 #
9 # The _opts_ argument is passed through to generate/parse respectively. See
10 # generate and parse for their documentation.
11 def [](object, opts = {})
12 if object.respond_to? :to_str
13 JSON.parse(object.to_str, opts)
14 else
15 JSON.generate(object, opts)
16 end
17 end
18
19 # Returns the JSON parser class that is used by JSON. This is either
20 # JSON::Ext::Parser or JSON::Pure::Parser.
21 attr_reader :parser
22
23 # Set the JSON parser class _parser_ to be used by JSON.
24 def parser=(parser) # :nodoc:
25 @parser = parser
26 remove_const :Parser if JSON.const_defined_in?(self, :Parser)
27 const_set :Parser, parser
28 end
29
30 # Return the constant located at _path_. The format of _path_ has to be
31 # either ::A::B::C or A::B::C. In any case, A has to be located at the top
32 # level (absolute namespace path?). If there doesn't exist a constant at
33 # the given path, an ArgumentError is raised.
34 def deep_const_get(path) # :nodoc:
35 path.to_s.split(/::/).inject(Object) do |p, c|
36 case
37 when c.empty? then p
38 when JSON.const_defined_in?(p, c) then p.const_get(c)
39 else
40 begin
41 p.const_missing(c)
42 rescue NameError => e
43 raise ArgumentError, "can't get const #{path}: #{e}"
44 end
45 end
46 end
47 end
48
49 # Set the module _generator_ to be used by JSON.
50 def generator=(generator) # :nodoc:
51 old, $VERBOSE = $VERBOSE, nil
52 @generator = generator
53 generator_methods = generator::GeneratorMethods
54 for const in generator_methods.constants
55 klass = deep_const_get(const)
56 modul = generator_methods.const_get(const)
57 klass.class_eval do
58 instance_methods(false).each do |m|
59 m.to_s == 'to_json' and remove_method m
60 end
61 include modul
62 end
63 end
64 self.state = generator::State
65 const_set :State, self.state
66 const_set :SAFE_STATE_PROTOTYPE, State.new
67 const_set :FAST_STATE_PROTOTYPE, State.new(
68 :indent => '',
69 :space => '',
70 :object_nl => "",
71 :array_nl => "",
72 :max_nesting => false
73 )
74 const_set :PRETTY_STATE_PROTOTYPE, State.new(
75 :indent => ' ',
76 :space => ' ',
77 :object_nl => "\n",
78 :array_nl => "\n"
79 )
80 ensure
81 $VERBOSE = old
82 end
83
84 # Returns the JSON generator module that is used by JSON. This is
85 # either JSON::Ext::Generator or JSON::Pure::Generator.
86 attr_reader :generator
87
88 # Returns the JSON generator state class that is used by JSON. This is
89 # either JSON::Ext::Generator::State or JSON::Pure::Generator::State.
90 attr_accessor :state
91
92 # This is create identifier, which is used to decide if the _json_create_
93 # hook of a class should be called. It defaults to 'json_class'.
94 attr_accessor :create_id
95 end
96 self.create_id = 'json_class'
97
98 NaN = 0.0/0
99
100 Infinity = 1.0/0
101
102 MinusInfinity = -Infinity
103
104 # The base exception for JSON errors.
105 class JSONError < StandardError
106 def self.wrap(exception)
107 obj = new("Wrapped(#{exception.class}): #{exception.message.inspect}")
108 obj.set_backtrace exception.backtrace
109 obj
110 end
111 end
112
113 # This exception is raised if a parser error occurs.
114 class ParserError < JSONError; end
115
116 # This exception is raised if the nesting of parsed data structures is too
117 # deep.
118 class NestingError < ParserError; end
119
120 # :stopdoc:
121 class CircularDatastructure < NestingError; end
122 # :startdoc:
123
124 # This exception is raised if a generator or unparser error occurs.
125 class GeneratorError < JSONError; end
126 # For backwards compatibility
127 UnparserError = GeneratorError
128
129 # This exception is raised if the required unicode support is missing on the
130 # system. Usually this means that the iconv library is not installed.
131 class MissingUnicodeSupport < JSONError; end
132
133 module_function
134
135 # Parse the JSON document _source_ into a Ruby data structure and return it.
136 #
137 # _opts_ can have the following
138 # keys:
139 # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
140 # structures. Disable depth checking with :max_nesting => false. It defaults
141 # to 100.
142 # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
143 # defiance of RFC 4627 to be parsed by the Parser. This option defaults
144 # to false.
145 # * *symbolize_names*: If set to true, returns symbols for the names
146 # (keys) in a JSON object. Otherwise strings are returned. Strings are
147 # the default.
148 # * *create_additions*: If set to false, the Parser doesn't create
149 # additions even if a matching class and create_id was found. This option
150 # defaults to false.
151 # * *object_class*: Defaults to Hash
152 # * *array_class*: Defaults to Array
153 def parse(source, opts = {})
154 Parser.new(source, opts).parse
155 end
156
157 # Parse the JSON document _source_ into a Ruby data structure and return it.
158 # The bang version of the parse method defaults to the more dangerous values
159 # for the _opts_ hash, so be sure only to parse trusted _source_ documents.
160 #
161 # _opts_ can have the following keys:
162 # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
163 # structures. Enable depth checking with :max_nesting => anInteger. The parse!
164 # methods defaults to not doing max depth checking: This can be dangerous
165 # if someone wants to fill up your stack.
166 # * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
167 # defiance of RFC 4627 to be parsed by the Parser. This option defaults
168 # to true.
169 # * *create_additions*: If set to false, the Parser doesn't create
170 # additions even if a matching class and create_id was found. This option
171 # defaults to false.
172 def parse!(source, opts = {})
173 opts = {
174 :max_nesting => false,
175 :allow_nan => true
176 }.update(opts)
177 Parser.new(source, opts).parse
178 end
179
180 # Generate a JSON document from the Ruby data structure _obj_ and return
181 # it. _state_ is * a JSON::State object,
182 # * or a Hash like object (responding to to_hash),
183 # * an object convertible into a hash by a to_h method,
184 # that is used as or to configure a State object.
185 #
186 # It defaults to a state object, that creates the shortest possible JSON text
187 # in one line, checks for circular data structures and doesn't allow NaN,
188 # Infinity, and -Infinity.
189 #
190 # A _state_ hash can have the following keys:
191 # * *indent*: a string used to indent levels (default: ''),
192 # * *space*: a string that is put after, a : or , delimiter (default: ''),
193 # * *space_before*: a string that is put before a : pair delimiter (default: ''),
194 # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
195 # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
196 # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
197 # generated, otherwise an exception is thrown if these values are
198 # encountered. This options defaults to false.
199 # * *max_nesting*: The maximum depth of nesting allowed in the data
200 # structures from which JSON is to be generated. Disable depth checking
201 # with :max_nesting => false, it defaults to 100.
202 #
203 # See also the fast_generate for the fastest creation method with the least
204 # amount of sanity checks, and the pretty_generate method for some
205 # defaults for pretty output.
206 def generate(obj, opts = nil)
207 if State === opts
208 state, opts = opts, nil
209 else
210 state = SAFE_STATE_PROTOTYPE.dup
211 end
212 if opts
213 if opts.respond_to? :to_hash
214 opts = opts.to_hash
215 elsif opts.respond_to? :to_h
216 opts = opts.to_h
217 else
218 raise TypeError, "can't convert #{opts.class} into Hash"
219 end
220 state = state.configure(opts)
221 end
222 state.generate(obj)
223 end
224
225 # :stopdoc:
226 # I want to deprecate these later, so I'll first be silent about them, and
227 # later delete them.
228 alias unparse generate
229 module_function :unparse
230 # :startdoc:
231
232 # Generate a JSON document from the Ruby data structure _obj_ and return it.
233 # This method disables the checks for circles in Ruby objects.
234 #
235 # *WARNING*: Be careful not to pass any Ruby data structures with circles as
236 # _obj_ argument because this will cause JSON to go into an infinite loop.
237 def fast_generate(obj, opts = nil)
238 if State === opts
239 state, opts = opts, nil
240 else
241 state = FAST_STATE_PROTOTYPE.dup
242 end
243 if opts
244 if opts.respond_to? :to_hash
245 opts = opts.to_hash
246 elsif opts.respond_to? :to_h
247 opts = opts.to_h
248 else
249 raise TypeError, "can't convert #{opts.class} into Hash"
250 end
251 state.configure(opts)
252 end
253 state.generate(obj)
254 end
255
256 # :stopdoc:
257 # I want to deprecate these later, so I'll first be silent about them, and later delete them.
258 alias fast_unparse fast_generate
259 module_function :fast_unparse
260 # :startdoc:
261
262 # Generate a JSON document from the Ruby data structure _obj_ and return it.
263 # The returned document is a prettier form of the document returned by
264 # #unparse.
265 #
266 # The _opts_ argument can be used to configure the generator. See the
267 # generate method for a more detailed explanation.
268 def pretty_generate(obj, opts = nil)
269 if State === opts
270 state, opts = opts, nil
271 else
272 state = PRETTY_STATE_PROTOTYPE.dup
273 end
274 if opts
275 if opts.respond_to? :to_hash
276 opts = opts.to_hash
277 elsif opts.respond_to? :to_h
278 opts = opts.to_h
279 else
280 raise TypeError, "can't convert #{opts.class} into Hash"
281 end
282 state.configure(opts)
283 end
284 state.generate(obj)
285 end
286
287 # :stopdoc:
288 # I want to deprecate these later, so I'll first be silent about them, and later delete them.
289 alias pretty_unparse pretty_generate
290 module_function :pretty_unparse
291 # :startdoc:
292
293 class << self
294 # The global default options for the JSON.load method:
295 # :max_nesting: false
296 # :allow_nan: true
297 # :quirks_mode: true
298 attr_accessor :load_default_options
299 end
300 self.load_default_options = {
301 :max_nesting => false,
302 :allow_nan => true,
303 :quirks_mode => true,
304 :create_additions => true,
305 }
306
307 # Load a ruby data structure from a JSON _source_ and return it. A source can
308 # either be a string-like object, an IO-like object, or an object responding
309 # to the read method. If _proc_ was given, it will be called with any nested
310 # Ruby object as an argument recursively in depth first order. To modify the
311 # default options pass in the optional _options_ argument as well.
312 #
313 # BEWARE: This method is meant to serialise data from trusted user input,
314 # like from your own database server or clients under your control, it could
315 # be dangerous to allow untrusted users to pass JSON sources into it. The
316 # default options for the parser can be changed via the load_default_options
317 # method.
318 #
319 # This method is part of the implementation of the load/dump interface of
320 # Marshal and YAML.
321 def load(source, proc = nil, options = {})
322 opts = load_default_options.merge options
323 if source.respond_to? :to_str
324 source = source.to_str
325 elsif source.respond_to? :to_io
326 source = source.to_io.read
327 elsif source.respond_to?(:read)
328 source = source.read
329 end
330 if opts[:quirks_mode] && (source.nil? || source.empty?)
331 source = 'null'
332 end
333 result = parse(source, opts)
334 recurse_proc(result, &proc) if proc
335 result
336 end
337
338 # Recursively calls passed _Proc_ if the parsed data structure is an _Array_ or _Hash_
339 def recurse_proc(result, &proc)
340 case result
341 when Array
342 result.each { |x| recurse_proc x, &proc }
343 proc.call result
344 when Hash
345 result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
346 proc.call result
347 else
348 proc.call result
349 end
350 end
351
352 alias restore load
353 module_function :restore
354
355 class << self
356 # The global default options for the JSON.dump method:
357 # :max_nesting: false
358 # :allow_nan: true
359 # :quirks_mode: true
360 attr_accessor :dump_default_options
361 end
362 self.dump_default_options = {
363 :max_nesting => false,
364 :allow_nan => true,
365 :quirks_mode => true,
366 }
367
368 # Dumps _obj_ as a JSON string, i.e. calls generate on the object and returns
369 # the result.
370 #
371 # If anIO (an IO-like object or an object that responds to the write method)
372 # was given, the resulting JSON is written to it.
373 #
374 # If the number of nested arrays or objects exceeds _limit_, an ArgumentError
375 # exception is raised. This argument is similar (but not exactly the
376 # same!) to the _limit_ argument in Marshal.dump.
377 #
378 # The default options for the generator can be changed via the
379 # dump_default_options method.
380 #
381 # This method is part of the implementation of the load/dump interface of
382 # Marshal and YAML.
383 def dump(obj, anIO = nil, limit = nil)
384 if anIO and limit.nil?
385 anIO = anIO.to_io if anIO.respond_to?(:to_io)
386 unless anIO.respond_to?(:write)
387 limit = anIO
388 anIO = nil
389 end
390 end
391 opts = JSON.dump_default_options
392 opts = opts.merge(:max_nesting => limit) if limit
393 result = generate(obj, opts)
394 if anIO
395 anIO.write result
396 anIO
397 else
398 result
399 end
400 rescue JSON::NestingError
401 raise ArgumentError, "exceed depth limit"
402 end
403
404 # Swap consecutive bytes of _string_ in place.
405 def self.swap!(string) # :nodoc:
406 0.upto(string.size / 2) do |i|
407 break unless string[2 * i + 1]
408 string[2 * i], string[2 * i + 1] = string[2 * i + 1], string[2 * i]
409 end
410 string
411 end
412
413 # Shortcut for iconv.
414 if ::String.method_defined?(:encode)
415 # Encodes string using Ruby's _String.encode_
416 def self.iconv(to, from, string)
417 string.encode(to, from)
418 end
419 else
420 require 'iconv'
421 # Encodes string using _iconv_ library
422 def self.iconv(to, from, string)
423 Iconv.conv(to, from, string)
424 end
425 end
426
427 if ::Object.method(:const_defined?).arity == 1
428 def self.const_defined_in?(modul, constant)
429 modul.const_defined?(constant)
430 end
431 else
432 def self.const_defined_in?(modul, constant)
433 modul.const_defined?(constant, false)
434 end
435 end
436 end
437
438 module ::Kernel
439 private
440
441 # Outputs _objs_ to STDOUT as JSON strings in the shortest form, that is in
442 # one line.
443 def j(*objs)
444 objs.each do |obj|
445 puts JSON::generate(obj, :allow_nan => true, :max_nesting => false)
446 end
447 nil
448 end
449
450 # Outputs _objs_ to STDOUT as JSON strings in a pretty format, with
451 # indentation and over many lines.
452 def jj(*objs)
453 objs.each do |obj|
454 puts JSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
455 end
456 nil
457 end
458
459 # If _object_ is string-like, parse the string and return the parsed result as
460 # a Ruby data structure. Otherwise, generate a JSON text from the Ruby data
461 # structure object and return it.
462 #
463 # The _opts_ argument is passed through to generate/parse respectively. See
464 # generate and parse for their documentation.
465 def JSON(object, *args)
466 if object.respond_to? :to_str
467 JSON.parse(object.to_str, args.first)
468 else
469 JSON.generate(object, args.first)
470 end
471 end
472 end
473
474 # Extends any Class to include _json_creatable?_ method.
475 class ::Class
476 # Returns true if this class can be used to create an instance
477 # from a serialised JSON string. The class has to implement a class
478 # method _json_create_ that expects a hash as first parameter. The hash
479 # should include the required data.
480 def json_creatable?
481 respond_to?(:json_create)
482 end
483 end
(New empty file)
0 if ENV['SIMPLECOV_COVERAGE'].to_i == 1
1 require 'simplecov'
2 SimpleCov.start do
3 add_filter "/tests/"
4 end
5 end
6 require 'json/common'
7
8 module JSON
9 # This module holds all the modules/classes that implement JSON's
10 # functionality as C extensions.
11 module Ext
12 require 'json/ext/parser'
13 require 'json/ext/generator'
14 $DEBUG and warn "Using Ext extension for JSON."
15 JSON.parser = Parser
16 JSON.generator = Generator
17 end
18
19 JSON_LOADED = true unless defined?(::JSON::JSON_LOADED)
20 end
0 require 'ostruct'
1
2 module JSON
3 class GenericObject < OpenStruct
4 class << self
5 alias [] new
6
7 def json_creatable?
8 @json_creatable
9 end
10
11 attr_writer :json_creatable
12
13 def json_create(data)
14 data = data.dup
15 data.delete JSON.create_id
16 self[data]
17 end
18
19 def from_hash(object)
20 case
21 when object.respond_to?(:to_hash)
22 result = new
23 object.to_hash.each do |key, value|
24 result[key] = from_hash(value)
25 end
26 result
27 when object.respond_to?(:to_ary)
28 object.to_ary.map { |a| from_hash(a) }
29 else
30 object
31 end
32 end
33
34 def load(source, proc = nil, opts = {})
35 result = ::JSON.load(source, proc, opts.merge(:object_class => self))
36 result.nil? ? new : result
37 end
38
39 def dump(obj, *args)
40 ::JSON.dump(obj, *args)
41 end
42 end
43 self.json_creatable = false
44
45 def to_hash
46 table
47 end
48
49 def [](name)
50 table[name.to_sym]
51 end
52
53 def []=(name, value)
54 __send__ "#{name}=", value
55 end
56
57 def |(other)
58 self.class[other.to_hash.merge(to_hash)]
59 end
60
61 def as_json(*)
62 { JSON.create_id => self.class.name }.merge to_hash
63 end
64
65 def to_json(*a)
66 as_json.to_json(*a)
67 end
68 end
69 end
0 module JSON
1 MAP = {
2 "\x0" => '\u0000',
3 "\x1" => '\u0001',
4 "\x2" => '\u0002',
5 "\x3" => '\u0003',
6 "\x4" => '\u0004',
7 "\x5" => '\u0005',
8 "\x6" => '\u0006',
9 "\x7" => '\u0007',
10 "\b" => '\b',
11 "\t" => '\t',
12 "\n" => '\n',
13 "\xb" => '\u000b',
14 "\f" => '\f',
15 "\r" => '\r',
16 "\xe" => '\u000e',
17 "\xf" => '\u000f',
18 "\x10" => '\u0010',
19 "\x11" => '\u0011',
20 "\x12" => '\u0012',
21 "\x13" => '\u0013',
22 "\x14" => '\u0014',
23 "\x15" => '\u0015',
24 "\x16" => '\u0016',
25 "\x17" => '\u0017',
26 "\x18" => '\u0018',
27 "\x19" => '\u0019',
28 "\x1a" => '\u001a',
29 "\x1b" => '\u001b',
30 "\x1c" => '\u001c',
31 "\x1d" => '\u001d',
32 "\x1e" => '\u001e',
33 "\x1f" => '\u001f',
34 '"' => '\"',
35 '\\' => '\\\\',
36 } # :nodoc:
37
38 # Convert a UTF8 encoded Ruby string _string_ to a JSON string, encoded with
39 # UTF16 big endian characters as \u????, and return it.
40 if defined?(::Encoding)
41 def utf8_to_json(string) # :nodoc:
42 string = string.dup
43 string.force_encoding(::Encoding::ASCII_8BIT)
44 string.gsub!(/["\\\x0-\x1f]/) { MAP[$&] }
45 string.force_encoding(::Encoding::UTF_8)
46 string
47 end
48
49 def utf8_to_json_ascii(string) # :nodoc:
50 string = string.dup
51 string.force_encoding(::Encoding::ASCII_8BIT)
52 string.gsub!(/["\\\x0-\x1f]/n) { MAP[$&] }
53 string.gsub!(/(
54 (?:
55 [\xc2-\xdf][\x80-\xbf] |
56 [\xe0-\xef][\x80-\xbf]{2} |
57 [\xf0-\xf4][\x80-\xbf]{3}
58 )+ |
59 [\x80-\xc1\xf5-\xff] # invalid
60 )/nx) { |c|
61 c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'"
62 s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0]
63 s.force_encoding(::Encoding::ASCII_8BIT)
64 s.gsub!(/.{4}/n, '\\\\u\&')
65 s.force_encoding(::Encoding::UTF_8)
66 }
67 string.force_encoding(::Encoding::UTF_8)
68 string
69 rescue => e
70 raise GeneratorError.wrap(e)
71 end
72
73 def valid_utf8?(string)
74 encoding = string.encoding
75 (encoding == Encoding::UTF_8 || encoding == Encoding::ASCII) &&
76 string.valid_encoding?
77 end
78 module_function :valid_utf8?
79 else
80 def utf8_to_json(string) # :nodoc:
81 string.gsub(/["\\\x0-\x1f]/n) { MAP[$&] }
82 end
83
84 def utf8_to_json_ascii(string) # :nodoc:
85 string = string.gsub(/["\\\x0-\x1f]/) { MAP[$&] }
86 string.gsub!(/(
87 (?:
88 [\xc2-\xdf][\x80-\xbf] |
89 [\xe0-\xef][\x80-\xbf]{2} |
90 [\xf0-\xf4][\x80-\xbf]{3}
91 )+ |
92 [\x80-\xc1\xf5-\xff] # invalid
93 )/nx) { |c|
94 c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'"
95 s = JSON.iconv('utf-16be', 'utf-8', c).unpack('H*')[0]
96 s.gsub!(/.{4}/n, '\\\\u\&')
97 }
98 string
99 rescue => e
100 raise GeneratorError.wrap(e)
101 end
102
103 def valid_utf8?(string)
104 string =~
105 /\A( [\x09\x0a\x0d\x20-\x7e] # ASCII
106 | [\xc2-\xdf][\x80-\xbf] # non-overlong 2-byte
107 | \xe0[\xa0-\xbf][\x80-\xbf] # excluding overlongs
108 | [\xe1-\xec\xee\xef][\x80-\xbf]{2} # straight 3-byte
109 | \xed[\x80-\x9f][\x80-\xbf] # excluding surrogates
110 | \xf0[\x90-\xbf][\x80-\xbf]{2} # planes 1-3
111 | [\xf1-\xf3][\x80-\xbf]{3} # planes 4-15
112 | \xf4[\x80-\x8f][\x80-\xbf]{2} # plane 16
113 )*\z/nx
114 end
115 end
116 module_function :utf8_to_json, :utf8_to_json_ascii, :valid_utf8?
117
118
119 module Pure
120 module Generator
121 # This class is used to create State instances, that are use to hold data
122 # while generating a JSON text from a Ruby data structure.
123 class State
124 # Creates a State object from _opts_, which ought to be Hash to create
125 # a new State instance configured by _opts_, something else to create
126 # an unconfigured instance. If _opts_ is a State object, it is just
127 # returned.
128 def self.from_state(opts)
129 case
130 when self === opts
131 opts
132 when opts.respond_to?(:to_hash)
133 new(opts.to_hash)
134 when opts.respond_to?(:to_h)
135 new(opts.to_h)
136 else
137 SAFE_STATE_PROTOTYPE.dup
138 end
139 end
140
141 # Instantiates a new State object, configured by _opts_.
142 #
143 # _opts_ can have the following keys:
144 #
145 # * *indent*: a string used to indent levels (default: ''),
146 # * *space*: a string that is put after, a : or , delimiter (default: ''),
147 # * *space_before*: a string that is put before a : pair delimiter (default: ''),
148 # * *object_nl*: a string that is put at the end of a JSON object (default: ''),
149 # * *array_nl*: a string that is put at the end of a JSON array (default: ''),
150 # * *check_circular*: is deprecated now, use the :max_nesting option instead,
151 # * *max_nesting*: sets the maximum level of data structure nesting in
152 # the generated JSON, max_nesting = 0 if no maximum should be checked.
153 # * *allow_nan*: true if NaN, Infinity, and -Infinity should be
154 # generated, otherwise an exception is thrown, if these values are
155 # encountered. This options defaults to false.
156 # * *quirks_mode*: Enables quirks_mode for parser, that is for example
157 # generating single JSON values instead of documents is possible.
158 def initialize(opts = {})
159 @indent = ''
160 @space = ''
161 @space_before = ''
162 @object_nl = ''
163 @array_nl = ''
164 @allow_nan = false
165 @ascii_only = false
166 @quirks_mode = false
167 @buffer_initial_length = 1024
168 configure opts
169 end
170
171 # This string is used to indent levels in the JSON text.
172 attr_accessor :indent
173
174 # This string is used to insert a space between the tokens in a JSON
175 # string.
176 attr_accessor :space
177
178 # This string is used to insert a space before the ':' in JSON objects.
179 attr_accessor :space_before
180
181 # This string is put at the end of a line that holds a JSON object (or
182 # Hash).
183 attr_accessor :object_nl
184
185 # This string is put at the end of a line that holds a JSON array.
186 attr_accessor :array_nl
187
188 # This integer returns the maximum level of data structure nesting in
189 # the generated JSON, max_nesting = 0 if no maximum is checked.
190 attr_accessor :max_nesting
191
192 # If this attribute is set to true, quirks mode is enabled, otherwise
193 # it's disabled.
194 attr_accessor :quirks_mode
195
196 # :stopdoc:
197 attr_reader :buffer_initial_length
198
199 def buffer_initial_length=(length)
200 if length > 0
201 @buffer_initial_length = length
202 end
203 end
204 # :startdoc:
205
206 # This integer returns the current depth data structure nesting in the
207 # generated JSON.
208 attr_accessor :depth
209
210 def check_max_nesting # :nodoc:
211 return if @max_nesting.zero?
212 current_nesting = depth + 1
213 current_nesting > @max_nesting and
214 raise NestingError, "nesting of #{current_nesting} is too deep"
215 end
216
217 # Returns true, if circular data structures are checked,
218 # otherwise returns false.
219 def check_circular?
220 !@max_nesting.zero?
221 end
222
223 # Returns true if NaN, Infinity, and -Infinity should be considered as
224 # valid JSON and output.
225 def allow_nan?
226 @allow_nan
227 end
228
229 # Returns true, if only ASCII characters should be generated. Otherwise
230 # returns false.
231 def ascii_only?
232 @ascii_only
233 end
234
235 # Returns true, if quirks mode is enabled. Otherwise returns false.
236 def quirks_mode?
237 @quirks_mode
238 end
239
240 # Configure this State instance with the Hash _opts_, and return
241 # itself.
242 def configure(opts)
243 if opts.respond_to?(:to_hash)
244 opts = opts.to_hash
245 elsif opts.respond_to?(:to_h)
246 opts = opts.to_h
247 else
248 raise TypeError, "can't convert #{opts.class} into Hash"
249 end
250 for key, value in opts
251 instance_variable_set "@#{key}", value
252 end
253 @indent = opts[:indent] if opts.key?(:indent)
254 @space = opts[:space] if opts.key?(:space)
255 @space_before = opts[:space_before] if opts.key?(:space_before)
256 @object_nl = opts[:object_nl] if opts.key?(:object_nl)
257 @array_nl = opts[:array_nl] if opts.key?(:array_nl)
258 @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
259 @ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
260 @depth = opts[:depth] || 0
261 @quirks_mode = opts[:quirks_mode] if opts.key?(:quirks_mode)
262 @buffer_initial_length ||= opts[:buffer_initial_length]
263
264 if !opts.key?(:max_nesting) # defaults to 100
265 @max_nesting = 100
266 elsif opts[:max_nesting]
267 @max_nesting = opts[:max_nesting]
268 else
269 @max_nesting = 0
270 end
271 self
272 end
273 alias merge configure
274
275 # Returns the configuration instance variables as a hash, that can be
276 # passed to the configure method.
277 def to_h
278 result = {}
279 for iv in instance_variables
280 iv = iv.to_s[1..-1]
281 result[iv.to_sym] = self[iv]
282 end
283 result
284 end
285
286 alias to_hash to_h
287
288 # Generates a valid JSON document from object +obj+ and returns the
289 # result. If no valid JSON document can be created this method raises a
290 # GeneratorError exception.
291 def generate(obj)
292 result = obj.to_json(self)
293 JSON.valid_utf8?(result) or raise GeneratorError,
294 "source sequence #{result.inspect} is illegal/malformed utf-8"
295 unless @quirks_mode
296 unless result =~ /\A\s*\[/ && result =~ /\]\s*\Z/ ||
297 result =~ /\A\s*\{/ && result =~ /\}\s*\Z/
298 then
299 raise GeneratorError, "only generation of JSON objects or arrays allowed"
300 end
301 end
302 result
303 end
304
305 # Return the value returned by method +name+.
306 def [](name)
307 if respond_to?(name)
308 __send__(name)
309 else
310 instance_variable_get("@#{name}")
311 end
312 end
313
314 def []=(name, value)
315 if respond_to?(name_writer = "#{name}=")
316 __send__ name_writer, value
317 else
318 instance_variable_set "@#{name}", value
319 end
320 end
321 end
322
323 module GeneratorMethods
324 module Object
325 # Converts this object to a string (calling #to_s), converts
326 # it to a JSON string, and returns the result. This is a fallback, if no
327 # special method #to_json was defined for some object.
328 def to_json(*) to_s.to_json end
329 end
330
331 module Hash
332 # Returns a JSON string containing a JSON object, that is unparsed from
333 # this Hash instance.
334 # _state_ is a JSON::State object, that can also be used to configure the
335 # produced JSON string output further.
336 # _depth_ is used to find out nesting depth, to indent accordingly.
337 def to_json(state = nil, *)
338 state = State.from_state(state)
339 state.check_max_nesting
340 json_transform(state)
341 end
342
343 private
344
345 def json_shift(state)
346 state.object_nl.empty? or return ''
347 state.indent * state.depth
348 end
349
350 def json_transform(state)
351 delim = ','
352 delim << state.object_nl
353 result = '{'
354 result << state.object_nl
355 depth = state.depth += 1
356 first = true
357 indent = !state.object_nl.empty?
358 each { |key,value|
359 result << delim unless first
360 result << state.indent * depth if indent
361 result << key.to_s.to_json(state)
362 result << state.space_before
363 result << ':'
364 result << state.space
365 if value.respond_to?(:to_json)
366 result << value.to_json(state)
367 else
368 result << %{"#{String(value)}"}
369 end
370 first = false
371 }
372 depth = state.depth -= 1
373 result << state.object_nl
374 result << state.indent * depth if indent
375 result << '}'
376 result
377 end
378 end
379
380 module Array
381 # Returns a JSON string containing a JSON array, that is unparsed from
382 # this Array instance.
383 # _state_ is a JSON::State object, that can also be used to configure the
384 # produced JSON string output further.
385 def to_json(state = nil, *)
386 state = State.from_state(state)
387 state.check_max_nesting
388 json_transform(state)
389 end
390
391 private
392
393 def json_transform(state)
394 delim = ','
395 delim << state.array_nl
396 result = '['
397 result << state.array_nl
398 depth = state.depth += 1
399 first = true
400 indent = !state.array_nl.empty?
401 each { |value|
402 result << delim unless first
403 result << state.indent * depth if indent
404 if value.respond_to?(:to_json)
405 result << value.to_json(state)
406 else
407 result << %{"#{String(value)}"}
408 end
409 first = false
410 }
411 depth = state.depth -= 1
412 result << state.array_nl
413 result << state.indent * depth if indent
414 result << ']'
415 end
416 end
417
418 module Integer
419 # Returns a JSON string representation for this Integer number.
420 def to_json(*) to_s end
421 end
422
423 module Float
424 # Returns a JSON string representation for this Float number.
425 def to_json(state = nil, *)
426 state = State.from_state(state)
427 case
428 when infinite?
429 if state.allow_nan?
430 to_s
431 else
432 raise GeneratorError, "#{self} not allowed in JSON"
433 end
434 when nan?
435 if state.allow_nan?
436 to_s
437 else
438 raise GeneratorError, "#{self} not allowed in JSON"
439 end
440 else
441 to_s
442 end
443 end
444 end
445
446 module String
447 if defined?(::Encoding)
448 # This string should be encoded with UTF-8 A call to this method
449 # returns a JSON string encoded with UTF16 big endian characters as
450 # \u????.
451 def to_json(state = nil, *args)
452 state = State.from_state(state)
453 if encoding == ::Encoding::UTF_8
454 string = self
455 else
456 string = encode(::Encoding::UTF_8)
457 end
458 if state.ascii_only?
459 '"' << JSON.utf8_to_json_ascii(string) << '"'
460 else
461 '"' << JSON.utf8_to_json(string) << '"'
462 end
463 end
464 else
465 # This string should be encoded with UTF-8 A call to this method
466 # returns a JSON string encoded with UTF16 big endian characters as
467 # \u????.
468 def to_json(state = nil, *args)
469 state = State.from_state(state)
470 if state.ascii_only?
471 '"' << JSON.utf8_to_json_ascii(self) << '"'
472 else
473 '"' << JSON.utf8_to_json(self) << '"'
474 end
475 end
476 end
477
478 # Module that holds the extinding methods if, the String module is
479 # included.
480 module Extend
481 # Raw Strings are JSON Objects (the raw bytes are stored in an
482 # array for the key "raw"). The Ruby String can be created by this
483 # module method.
484 def json_create(o)
485 o['raw'].pack('C*')
486 end
487 end
488
489 # Extends _modul_ with the String::Extend module.
490 def self.included(modul)
491 modul.extend Extend
492 end
493
494 # This method creates a raw object hash, that can be nested into
495 # other data structures and will be unparsed as a raw string. This
496 # method should be used, if you want to convert raw strings to JSON
497 # instead of UTF-8 strings, e. g. binary data.
498 def to_json_raw_object
499 {
500 JSON.create_id => self.class.name,
501 'raw' => self.unpack('C*'),
502 }
503 end
504
505 # This method creates a JSON text from the result of
506 # a call to to_json_raw_object of this String.
507 def to_json_raw(*args)
508 to_json_raw_object.to_json(*args)
509 end
510 end
511
512 module TrueClass
513 # Returns a JSON string for true: 'true'.
514 def to_json(*) 'true' end
515 end
516
517 module FalseClass
518 # Returns a JSON string for false: 'false'.
519 def to_json(*) 'false' end
520 end
521
522 module NilClass
523 # Returns a JSON string for nil: 'null'.
524 def to_json(*) 'null' end
525 end
526 end
527 end
528 end
529 end
0 require 'strscan'
1
2 module JSON
3 module Pure
4 # This class implements the JSON parser that is used to parse a JSON string
5 # into a Ruby data structure.
6 class Parser < StringScanner
7 STRING = /" ((?:[^\x0-\x1f"\\] |
8 # escaped special characters:
9 \\["\\\/bfnrt] |
10 \\u[0-9a-fA-F]{4} |
11 # match all but escaped special characters:
12 \\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*)
13 "/nx
14 INTEGER = /(-?0|-?[1-9]\d*)/
15 FLOAT = /(-?
16 (?:0|[1-9]\d*)
17 (?:
18 \.\d+(?i:e[+-]?\d+) |
19 \.\d+ |
20 (?i:e[+-]?\d+)
21 )
22 )/x
23 NAN = /NaN/
24 INFINITY = /Infinity/
25 MINUS_INFINITY = /-Infinity/
26 OBJECT_OPEN = /\{/
27 OBJECT_CLOSE = /\}/
28 ARRAY_OPEN = /\[/
29 ARRAY_CLOSE = /\]/
30 PAIR_DELIMITER = /:/
31 COLLECTION_DELIMITER = /,/
32 TRUE = /true/
33 FALSE = /false/
34 NULL = /null/
35 IGNORE = %r(
36 (?:
37 //[^\n\r]*[\n\r]| # line comments
38 /\* # c-style comments
39 (?:
40 [^*/]| # normal chars
41 /[^*]| # slashes that do not start a nested comment
42 \*[^/]| # asterisks that do not end this comment
43 /(?=\*/) # single slash before this comment's end
44 )*
45 \*/ # the End of this comment
46 |[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr
47 )+
48 )mx
49
50 UNPARSED = Object.new
51
52 # Creates a new JSON::Pure::Parser instance for the string _source_.
53 #
54 # It will be configured by the _opts_ hash. _opts_ can have the following
55 # keys:
56 # * *max_nesting*: The maximum depth of nesting allowed in the parsed data
57 # structures. Disable depth checking with :max_nesting => false|nil|0,
58 # it defaults to 100.
59 # * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
60 # defiance of RFC 4627 to be parsed by the Parser. This option defaults
61 # to false.
62 # * *symbolize_names*: If set to true, returns symbols for the names
63 # (keys) in a JSON object. Otherwise strings are returned, which is also
64 # the default.
65 # * *create_additions*: If set to true, the Parser creates
66 # additions when if a matching class and create_id was found. This
67 # option defaults to false.
68 # * *object_class*: Defaults to Hash
69 # * *array_class*: Defaults to Array
70 # * *quirks_mode*: Enables quirks_mode for parser, that is for example
71 # parsing single JSON values instead of documents is possible.
72 def initialize(source, opts = {})
73 opts ||= {}
74 unless @quirks_mode = opts[:quirks_mode]
75 source = convert_encoding source
76 end
77 super source
78 if !opts.key?(:max_nesting) # defaults to 100
79 @max_nesting = 100
80 elsif opts[:max_nesting]
81 @max_nesting = opts[:max_nesting]
82 else
83 @max_nesting = 0
84 end
85 @allow_nan = !!opts[:allow_nan]
86 @symbolize_names = !!opts[:symbolize_names]
87 if opts.key?(:create_additions)
88 @create_additions = !!opts[:create_additions]
89 else
90 @create_additions = false
91 end
92 @create_id = @create_additions ? JSON.create_id : nil
93 @object_class = opts[:object_class] || Hash
94 @array_class = opts[:array_class] || Array
95 @match_string = opts[:match_string]
96 end
97
98 alias source string
99
100 def quirks_mode?
101 !!@quirks_mode
102 end
103
104 def reset
105 super
106 @current_nesting = 0
107 end
108
109 # Parses the current JSON string _source_ and returns the complete data
110 # structure as a result.
111 def parse
112 reset
113 obj = nil
114 if @quirks_mode
115 while !eos? && skip(IGNORE)
116 end
117 if eos?
118 raise ParserError, "source did not contain any JSON!"
119 else
120 obj = parse_value
121 obj == UNPARSED and raise ParserError, "source did not contain any JSON!"
122 end
123 else
124 until eos?
125 case
126 when scan(OBJECT_OPEN)
127 obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
128 @current_nesting = 1
129 obj = parse_object
130 when scan(ARRAY_OPEN)
131 obj and raise ParserError, "source '#{peek(20)}' not in JSON!"
132 @current_nesting = 1
133 obj = parse_array
134 when skip(IGNORE)
135 ;
136 else
137 raise ParserError, "source '#{peek(20)}' not in JSON!"
138 end
139 end
140 obj or raise ParserError, "source did not contain any JSON!"
141 end
142 obj
143 end
144
145 private
146
147 def convert_encoding(source)
148 if source.respond_to?(:to_str)
149 source = source.to_str
150 else
151 raise TypeError, "#{source.inspect} is not like a string"
152 end
153 if defined?(::Encoding)
154 if source.encoding == ::Encoding::ASCII_8BIT
155 b = source[0, 4].bytes.to_a
156 source =
157 case
158 when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
159 source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8)
160 when b.size >= 4 && b[0] == 0 && b[2] == 0
161 source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8)
162 when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
163 source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8)
164 when b.size >= 4 && b[1] == 0 && b[3] == 0
165 source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8)
166 else
167 source.dup
168 end
169 else
170 source = source.encode(::Encoding::UTF_8)
171 end
172 source.force_encoding(::Encoding::ASCII_8BIT)
173 else
174 b = source
175 source =
176 case
177 when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
178 JSON.iconv('utf-8', 'utf-32be', b)
179 when b.size >= 4 && b[0] == 0 && b[2] == 0
180 JSON.iconv('utf-8', 'utf-16be', b)
181 when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
182 JSON.iconv('utf-8', 'utf-32le', b)
183 when b.size >= 4 && b[1] == 0 && b[3] == 0
184 JSON.iconv('utf-8', 'utf-16le', b)
185 else
186 b
187 end
188 end
189 source
190 end
191
192 # Unescape characters in strings.
193 UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr }
194 UNESCAPE_MAP.update({
195 ?" => '"',
196 ?\\ => '\\',
197 ?/ => '/',
198 ?b => "\b",
199 ?f => "\f",
200 ?n => "\n",
201 ?r => "\r",
202 ?t => "\t",
203 ?u => nil,
204 })
205
206 EMPTY_8BIT_STRING = ''
207 if ::String.method_defined?(:encode)
208 EMPTY_8BIT_STRING.force_encoding Encoding::ASCII_8BIT
209 end
210
211 def parse_string
212 if scan(STRING)
213 return '' if self[1].empty?
214 string = self[1].gsub(%r((?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff]))n) do |c|
215 if u = UNESCAPE_MAP[$&[1]]
216 u
217 else # \uXXXX
218 bytes = EMPTY_8BIT_STRING.dup
219 i = 0
220 while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
221 bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
222 i += 1
223 end
224 JSON.iconv('utf-8', 'utf-16be', bytes)
225 end
226 end
227 if string.respond_to?(:force_encoding)
228 string.force_encoding(::Encoding::UTF_8)
229 end
230 if @create_additions and @match_string
231 for (regexp, klass) in @match_string
232 klass.json_creatable? or next
233 string =~ regexp and return klass.json_create(string)
234 end
235 end
236 string
237 else
238 UNPARSED
239 end
240 rescue => e
241 raise ParserError, "Caught #{e.class} at '#{peek(20)}': #{e}"
242 end
243
244 def parse_value
245 case
246 when scan(FLOAT)
247 Float(self[1])
248 when scan(INTEGER)
249 Integer(self[1])
250 when scan(TRUE)
251 true
252 when scan(FALSE)
253 false
254 when scan(NULL)
255 nil
256 when (string = parse_string) != UNPARSED
257 string
258 when scan(ARRAY_OPEN)
259 @current_nesting += 1
260 ary = parse_array
261 @current_nesting -= 1
262 ary
263 when scan(OBJECT_OPEN)
264 @current_nesting += 1
265 obj = parse_object
266 @current_nesting -= 1
267 obj
268 when @allow_nan && scan(NAN)
269 NaN
270 when @allow_nan && scan(INFINITY)
271 Infinity
272 when @allow_nan && scan(MINUS_INFINITY)
273 MinusInfinity
274 else
275 UNPARSED
276 end
277 end
278
279 def parse_array
280 raise NestingError, "nesting of #@current_nesting is too deep" if
281 @max_nesting.nonzero? && @current_nesting > @max_nesting
282 result = @array_class.new
283 delim = false
284 until eos?
285 case
286 when (value = parse_value) != UNPARSED
287 delim = false
288 result << value
289 skip(IGNORE)
290 if scan(COLLECTION_DELIMITER)
291 delim = true
292 elsif match?(ARRAY_CLOSE)
293 ;
294 else
295 raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
296 end
297 when scan(ARRAY_CLOSE)
298 if delim
299 raise ParserError, "expected next element in array at '#{peek(20)}'!"
300 end
301 break
302 when skip(IGNORE)
303 ;
304 else
305 raise ParserError, "unexpected token in array at '#{peek(20)}'!"
306 end
307 end
308 result
309 end
310
311 def parse_object
312 raise NestingError, "nesting of #@current_nesting is too deep" if
313 @max_nesting.nonzero? && @current_nesting > @max_nesting
314 result = @object_class.new
315 delim = false
316 until eos?
317 case
318 when (string = parse_string) != UNPARSED
319 skip(IGNORE)
320 unless scan(PAIR_DELIMITER)
321 raise ParserError, "expected ':' in object at '#{peek(20)}'!"
322 end
323 skip(IGNORE)
324 unless (value = parse_value).equal? UNPARSED
325 result[@symbolize_names ? string.to_sym : string] = value
326 delim = false
327 skip(IGNORE)
328 if scan(COLLECTION_DELIMITER)
329 delim = true
330 elsif match?(OBJECT_CLOSE)
331 ;
332 else
333 raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
334 end
335 else
336 raise ParserError, "expected value in object at '#{peek(20)}'!"
337 end
338 when scan(OBJECT_CLOSE)
339 if delim
340 raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!"
341 end
342 if @create_additions and klassname = result[@create_id]
343 klass = JSON.deep_const_get klassname
344 break unless klass and klass.json_creatable?
345 result = klass.json_create(result)
346 end
347 break
348 when skip(IGNORE)
349 ;
350 else
351 raise ParserError, "unexpected token in object at '#{peek(20)}'!"
352 end
353 end
354 result
355 end
356 end
357 end
358 end
0 if ENV['SIMPLECOV_COVERAGE'].to_i == 1
1 require 'simplecov'
2 SimpleCov.start do
3 add_filter "/tests/"
4 end
5 end
6 require 'json/common'
7 require 'json/pure/parser'
8 require 'json/pure/generator'
9
10 module JSON
11 # This module holds all the modules/classes that implement JSON's
12 # functionality in pure ruby.
13 module Pure
14 $DEBUG and warn "Using Pure library for JSON."
15 JSON.parser = Parser
16 JSON.generator = Generator
17 end
18
19 JSON_LOADED = true unless defined?(::JSON::JSON_LOADED)
20 end
0 module JSON
1 # JSON version
2 VERSION = '1.8.6'
3 VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
4 VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
5 VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
6 VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
7 end
0 require 'json/common'
1
2 ##
3 # = JavaScript Object Notation (JSON)
4 #
5 # JSON is a lightweight data-interchange format. It is easy for us
6 # humans to read and write. Plus, equally simple for machines to generate or parse.
7 # JSON is completely language agnostic, making it the ideal interchange format.
8 #
9 # Built on two universally available structures:
10 # 1. A collection of name/value pairs. Often referred to as an _object_, hash table, record, struct, keyed list, or associative array.
11 # 2. An ordered list of values. More commonly called an _array_, vector, sequence or list.
12 #
13 # To read more about JSON visit: http://json.org
14 #
15 # == Parsing JSON
16 #
17 # To parse a JSON string received by another application or generated within
18 # your existing application:
19 #
20 # require 'json'
21 #
22 # my_hash = JSON.parse('{"hello": "goodbye"}')
23 # puts my_hash["hello"] => "goodbye"
24 #
25 # Notice the extra quotes <tt>''</tt> around the hash notation. Ruby expects
26 # the argument to be a string and can't convert objects like a hash or array.
27 #
28 # Ruby converts your string into a hash
29 #
30 # == Generating JSON
31 #
32 # Creating a JSON string for communication or serialization is
33 # just as simple.
34 #
35 # require 'json'
36 #
37 # my_hash = {:hello => "goodbye"}
38 # puts JSON.generate(my_hash) => "{\"hello\":\"goodbye\"}"
39 #
40 # Or an alternative way:
41 #
42 # require 'json'
43 # puts {:hello => "goodbye"}.to_json => "{\"hello\":\"goodbye\"}"
44 #
45 # <tt>JSON.generate</tt> only allows objects or arrays to be converted
46 # to JSON syntax. <tt>to_json</tt>, however, accepts many Ruby classes
47 # even though it acts only as a method for serialization:
48 #
49 # require 'json'
50 #
51 # 1.to_json => "1"
52 #
53 module JSON
54 require 'json/version'
55
56 begin
57 require 'json/ext'
58 rescue LoadError
59 require 'json/pure'
60 end
61 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2002-2008 Masao Mutoh
4 #
5 # Original: Ruby-GetText-Package-1.92.0.
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require "locale/driver"
22
23 module Locale
24 # Locale::Driver module for CGI.
25 # Detect the user locales and the charset from CGI parameters.
26 # This is a low-level class. Application shouldn't use this directly.
27 module Driver
28 module CGI
29 $stderr.puts self.name + " is loaded." if $DEBUG
30
31 module_function
32 # Gets required locales from CGI parameters. (Based on RFC2616)
33 #
34 # Returns: An Array of Locale::Tag's subclasses
35 # (QUERY_STRING "lang" > COOKIE "lang" > HTTP_ACCEPT_LANGUAGE > "en")
36 #
37 def locales
38 req = Thread.current[:current_request]
39 return nil unless req
40
41 locales = []
42
43 # QUERY_STRING "lang"
44 if langs = req[:query_langs]
45 langs.each do |lang|
46 locales << Locale::Tag.parse(lang)
47 end
48 end
49
50 unless locales.size > 0
51 # COOKIE "lang"
52 if langs = req[:cookie_langs]
53 langs.each do |lang|
54 locales << Locale::Tag.parse(lang) if lang.size > 0
55 end
56 end
57 end
58
59 unless locales.size > 0
60 # HTTP_ACCEPT_LANGUAGE
61 if lang = req[:accept_language] and lang.size > 0
62 # 10.0 is for ruby-1.8.6 which have the bug of str.to_f.
63 # Normally, this should be 1.0.
64 locales += lang.gsub(/\s/, "").split(/,/).map{|v| v.split(";q=")}.map{|j| [j[0], j[1] ? j[1].to_f : 10.0]}.sort{|a,b| -(a[1] <=> b[1])}.map{|v| Locale::Tag.parse(v[0])}
65 end
66 end
67
68 locales.size > 0 ? Locale::TagList.new(locales.uniq) : nil
69 end
70
71 # Gets the charset from CGI parameters. (Based on RFC2616)
72 # * Returns: the charset (HTTP_ACCEPT_CHARSET or nil).
73 def charset
74 req = Thread.current[:current_request]
75 return nil unless req
76
77 charsets = req[:accept_charset]
78 if charsets and charsets.size > 0
79 num = charsets.index(',')
80 charset = num ? charsets[0, num] : charsets
81 charset = nil if charset == "*"
82 else
83 charset = nil
84 end
85 charset
86 end
87
88 # Set a request.
89 #
90 # * query_langs: An Array of QUERY_STRING value "lang".
91 # * cookie_langs: An Array of cookie value "lang".
92 # * accept_language: The value of HTTP_ACCEPT_LANGUAGE
93 # * accept_charset: The value of HTTP_ACCEPT_CHARSET
94 def set_request(query_langs, cookie_langs, accept_language, accept_charset)
95 Locale.clear
96 Thread.current[:current_request] = {
97 :query_langs => query_langs,
98 :cookie_langs => cookie_langs,
99 :accept_language => accept_language,
100 :accept_charset => accept_charset
101 }
102 self
103 end
104
105 # Clear the current request.
106 def clear_current_request
107 Thread.current[:current_request] = nil
108 end
109 end
110
111 MODULES[:cgi] = CGI
112 end
113
114
115 module_function
116 # Sets a request values for lang/charset.
117 #
118 # * query_langs: An Array of QUERY_STRING value "lang".
119 # * cookie_langs: An Array of cookie value "lang".
120 # * accept_language: The value of HTTP_ACCEPT_LANGUAGE
121 # * accept_charset: The value of HTTP_ACCEPT_CHARSET
122 def set_request(query_langs, cookie_langs, accept_language, accept_charset)
123 driver_module.set_request(query_langs, cookie_langs, accept_language, accept_charset)
124 self
125 end
126
127 # Sets a CGI object. This is the convenient function of set_request().
128 #
129 # This method is appeared when Locale.init(:driver => :cgi) is called.
130 #
131 # * cgi: CGI object
132 # * Returns: self
133 def set_cgi(cgi)
134 set_request(cgi.params["lang"], cgi.cookies["lang"],
135 cgi.accept_language, cgi.accept_charset)
136 self
137 end
138
139 # Sets a CGI object.This is the convenient function of set_request().
140 #
141 # This method is appeared when Locale.init(:driver => :cgi) is called.
142 #
143 # * cgi: CGI object
144 # * Returns: cgi
145 def cgi=(cgi)
146 set_cgi(cgi)
147 cgi
148 end
149 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2012 Hleb Valoshka
4 # Copyright (C) 2008 Masao Mutoh
5 #
6 # Original: Ruby-GetText-Package-1.92.0.
7 # License: Ruby's or LGPL
8 #
9 # This library is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Lesser General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This library is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU Lesser General Public License for more details.
18 #
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
22 require 'locale/tag'
23 require 'locale/taglist'
24 require "locale/driver"
25
26 module Locale
27 module Driver
28 # Locale::Driver::Env module.
29 # Detect the user locales and the charset.
30 # All drivers(except CGI) refer environment variables first and use it
31 # as the locale if it's defined.
32 # This is a low-level module. Application shouldn't use this directly.
33 module Env
34 module_function
35
36 # Gets the locale from environment variable.
37 # Priority order except charset is LC_ALL > LC_MESSAGES > LANG.
38 # Priority order for charset is LC_ALL > LC_CTYPE > LANG.
39 # Returns: the locale as Locale::Tag::Posix.
40 def locale
41 lc_all = Private.parse(ENV["LC_ALL"])
42 return lc_all if lc_all
43
44 lc_messages = Private.parse(ENV["LC_MESSAGES"])
45 lang = Private.parse(ENV["LANG"])
46
47 tag = lc_messages || lang
48 return nil if tag.nil?
49
50 lc_ctype = Private.parse(ENV["LC_CTYPE"])
51 tag.charset = lc_ctype.charset if lc_ctype
52
53 tag
54 end
55
56 # Gets the locales from environment variables. (LANGUAGE > LC_ALL > LC_MESSAGES > LANG)
57 # * Returns: an Array of the locale as Locale::Tag::Posix or nil.
58 def locales
59 return nil if (ENV["LC_ALL"] || ENV["LC_MESSAGES"] || ENV["LANG"]) == "C"
60 locales = ENV["LANGUAGE"]
61 if (locales != nil and locales.size > 0)
62 locs = locales.split(/:/).collect{|v| Locale::Tag::Posix.parse(v)}.compact
63 if locs.size > 0
64 return Locale::TagList.new(locs)
65 end
66 elsif (loc = locale)
67 return Locale::TagList.new([loc])
68 end
69 nil
70 end
71
72 # Gets the charset from environment variables
73 # (LC_ALL > LC_CTYPE > LANG) or return nil.
74 # * Returns: the system charset.
75 def charset # :nodoc:
76 [ENV["LC_ALL"], ENV["LC_CTYPE"], ENV["LANG"]].each do |env|
77 tag = Private.parse(env)
78 next if tag.nil?
79 return tag.charset
80 end
81 nil
82 end
83
84 module Private
85 module_function
86 def parse(env_value)
87 return nil if env_value.nil?
88 return nil if env_value.empty?
89 Locale::Tag::Posix.parse(env_value)
90 end
91 end
92 end
93
94 MODULES[:env] = Env
95 end
96 end
97
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2007-2008 Masao Mutoh
4 #
5 # Original: Ruby-GetText-Package-1.92.0.
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require 'java'
22
23 require "locale/driver/env"
24
25 module Locale
26 module Driver
27 # Locale::Driver::JRuby module for JRuby
28 # Detect the user locales and the charset.
29 # This is a low-level class. Application shouldn't use this directly.
30 module JRuby
31 $stderr.puts self.name + " is loaded." if $DEBUG
32
33 module_function
34 def locales #:nodoc:
35 locales = ::Locale::Driver::Env.locales
36 unless locales
37 locale = java.util.Locale.getDefault
38 variant = locale.getVariant
39 variants = []
40 if variant != nil and variant.size > 0
41 variants = [variant]
42 end
43 locales = TagList.new([Locale::Tag::Common.new(locale.getLanguage, nil,
44 locale.getCountry,
45 variants)])
46 end
47 locales
48 end
49
50 def charset #:nodoc:
51 charset = ::Locale::Driver::Env.charset
52 unless charset
53 charset = java.nio.charset.Charset.defaultCharset.name
54 end
55 charset
56 end
57 end
58
59 MODULES[:jruby] = JRuby
60 end
61 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2002-2008 Masao Mutoh
4 #
5 # Original: Ruby-GetText-Package-1.92.0.
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require "locale/driver/env"
22
23 module Locale
24 # Locale::Driver::Posix module for Posix OS (Unix)
25 # Detect the user locales and the charset.
26 # This is a low-level class. Application shouldn't use this directly.
27 module Driver
28 module Posix
29 $stderr.puts self.name + " is loaded." if $DEBUG
30
31 module_function
32 # Gets the locales from environment variables. (LANGUAGE > LC_ALL > LC_MESSAGES > LANG)
33 # Only LANGUAGE accept plural languages such as "nl_BE;
34 # * Returns: an Array of the locale as Locale::Tag::Posix or nil.
35 def locales
36 ::Locale::Driver::Env.locales
37 end
38
39 # Gets the charset from environment variable or the result of
40 # "locale charmap" or nil.
41 # * Returns: the system charset.
42 def charset
43 charset = ::Locale::Driver::Env.charset
44 unless charset
45 charset = `locale charmap`.strip
46 unless $? && $?.success?
47 charset = nil
48 end
49 end
50 charset
51 end
52 end
53
54 MODULES[:posix] = Posix
55 end
56 end
57
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 # Copyright (C) 2002-2010 Masao Mutoh
4 #
5 # Original: Ruby-GetText-Package-1.92.0.
6 # License: Ruby's or LGPL
7 #
8 # This library is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Lesser General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This library is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU Lesser General Public License for more details.
17 #
18 # You should have received a copy of the GNU Lesser General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 require "locale/driver/env"
22 require "locale/driver/win32_table"
23
24 require "fiddle/import"
25
26 module Locale
27 # Locale::Driver::Win32 module for win32.
28 # Detect the user locales and the charset.
29 # This is a low-level class. Application shouldn't use this directly.
30 module Driver
31 module Win32
32 module Kernel32
33 extend Fiddle::Importer
34 dlload "kernel32.dll"
35 extern "int GetThreadLocale()"
36 end
37
38 include Win32Table
39
40 $stderr.puts self.name + " is loaded." if $DEBUG
41
42 @@current_locale_id = nil
43
44 module_function
45
46 # Gets the Win32 charset of the locale.
47 def charset
48 charset = ::Locale::Driver::Env.charset
49 unless charset
50 if locales
51 tag = locales[0].to_rfc.to_s
52 loc = LocaleTable.find{|v| v[1] == tag}
53 loc = LocaleTable.find{|v| v[1] =~ /^#{locales[0].language}/} unless loc
54 charset = loc ? loc[2] : nil
55 else
56 charset = "CP1252"
57 end
58 end
59 charset
60 end
61
62 def thread_locale_id #:nodoc:
63 if @@current_locale_id
64 @@current_locale_id
65 else
66 Kernel32.GetThreadLocale
67 end
68 end
69
70 def set_thread_locale_id(lcid) #:nodoc:
71 # for testing.
72 @@current_locale_id = lcid
73 end
74
75 def locales #:nodoc:
76 locales = ::Locale::Driver::Env.locales
77 unless locales
78 lang = LocaleTable.assoc(thread_locale_id)
79 if lang
80 ret = Locale::Tag::Common.parse(lang[1])
81 locales = Locale::TagList.new([ret])
82 else
83 locales = nil
84 end
85 end
86 locales
87 end
88 end
89
90 MODULES[:win32] = Win32
91 end
92 end
93
0 =begin
1 win32_table.rb - Locale table for win32
2
3 Copyright (C) 2008 Masao Mutoh <mutomasa at gmail.com>
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7
8 Original: Ruby-GetText-Package-1.92.0.
9
10 $Id: win32_table.rb 27 2008-12-03 15:06:50Z mutoh $
11 =end
12
13 module Locale
14 module Driver
15 module Win32Table
16
17 #http://msdn.microsoft.com/ja-jp/goglobal/bb896001(en-us).aspx
18
19 #LangID, locale name, code page
20 LocaleTable = [
21 [0x0036,"af","CP1252"],
22 [0x0436,"af-ZA","CP1252"],
23 [0x001C,"sq","CP1250"],
24 [0x041C,"sq-AL","CP1250"],
25 [0x0484,"gsw-FR","CP1252"],
26 [0x045E,"am-ET","UNICODE"],
27 [0x0001,"ar","CP1256"],
28 [0x1401,"ar-DZ","CP1256"],
29 [0x3C01,"ar-BH","CP1256"],
30 [0x0C01,"ar-EG","CP1256"],
31 [0x0801,"ar-IQ","CP1256"],
32 [0x2C01,"ar-JO","CP1256"],
33 [0x3401,"ar-KW","CP1256"],
34 [0x3001,"ar-LB","CP1256"],
35 [0x1001,"ar-LY","CP1256"],
36 [0x1801,"ar-MA","CP1256"],
37 [0x2001,"ar-OM","CP1256"],
38 [0x4001,"ar-QA","CP1256"],
39 [0x0401,"ar-SA","CP1256"],
40 [0x2801,"ar-SY","CP1256"],
41 [0x1C01,"ar-TN","CP1256"],
42 [0x3801,"ar-AE","CP1256"],
43 [0x2401,"ar-YE","CP1256"],
44 [0x002B,"hy","UNICODE"],
45 [0x042B,"hy-AM","UNICODE"],
46 [0x044D,"as-IN","UNICODE"],
47 [0x002C,"az","CP1254"],
48 [0x082C,"az-Cyrl-AZ","CP1251"],
49 [0x042C,"az-Latn-AZ","CP1254"],
50 [0x046D,"ba-RU","CP1251"],
51 [0x002D,"eu","CP1252"],
52 [0x042D,"eu-ES","CP1252"],
53 [0x0023,"be","CP1251"],
54 [0x0423,"be-BY","CP1251"],
55 [0x0845,"bn-BD","UNICODE"],
56 [0x0445,"bn-IN","UNICODE"],
57 [0x201A,"bs-Cyrl-BA","CP1251"],
58 [0x141A,"bs-Latn-BA","CP1250"],
59 [0x047E,"br-FR","CP1252"],
60 [0x0002,"bg","CP1251"],
61 [0x0402,"bg-BG","CP1251"],
62 [0x0003,"ca","CP1252"],
63 [0x0403,"ca-ES","CP1252"],
64 [0x0C04,"zh-HK","CP950"],
65 [0x1404,"zh-MO","CP950"],
66 [0x0804,"zh-CN","CP936"],
67 [0x0004,"zh-Hans","CP936"],
68 [0x1004,"zh-SG","CP936"],
69 [0x0404,"zh-TW","CP950"],
70 [0x7C04,"zh-Hant","CP950"],
71 [0x0483,"co-FR","CP1252"],
72 [0x001A,"hr","CP1250"],
73 [0x041A,"hr-HR","CP1250"],
74 [0x101A,"hr-BA","CP1250"],
75 [0x0005,"cs","CP1250"],
76 [0x0405,"cs-CZ","CP1250"],
77 [0x0006,"da","CP1252"],
78 [0x0406,"da-DK","CP1252"],
79 [0x048C,"prs-AF","CP1256"],
80 [0x0065,"div","UNICODE"],
81 [0x0465,"div-MV","UNICODE"],
82 [0x0013,"nl","CP1252"],
83 [0x0813,"nl-BE","CP1252"],
84 [0x0413,"nl-NL","CP1252"],
85 [0x0009,"en","CP1252"],
86 [0x0C09,"en-AU","CP1252"],
87 [0x2809,"en-BZ","CP1252"],
88 [0x1009,"en-CA","CP1252"],
89 [0x2409,"en-029","CP1252"],
90 [0x4009,"en-IN","CP1252"],
91 [0x1809,"en-IE","CP1252"],
92 [0x2009,"en-JM","CP1252"],
93 [0x4409,"en-MY","CP1252"],
94 [0x1409,"en-NZ","CP1252"],
95 [0x3409,"en-PH","CP1252"],
96 [0x4809,"en-SG","CP1252"],
97 [0x1C09,"en-ZA","CP1252"],
98 [0x2C09,"en-TT","CP1252"],
99 [0x0809,"en-GB","CP1252"],
100 [0x0409,"en-US","CP1252"],
101 [0x3009,"en-ZW","CP1252"],
102 [0x0025,"et","CP1257"],
103 [0x0425,"et-EE","CP1257"],
104 [0x0038,"fo","CP1252"],
105 [0x0438,"fo-FO","CP1252"],
106 [0x0464,"fil-PH","CP1252"],
107 [0x000B,"fi","CP1252"],
108 [0x040B,"fi-FI","CP1252"],
109 [0x000C,"fr","CP1252"],
110 [0x080C,"fr-BE","CP1252"],
111 [0x0C0C,"fr-CA","CP1252"],
112 [0x040C,"fr-FR","CP1252"],
113 [0x140C,"fr-LU","CP1252"],
114 [0x180C,"fr-MC","CP1252"],
115 [0x100C,"fr-CH","CP1252"],
116 [0x0462,"fy-NL","CP1252"],
117 [0x0056,"gl","CP1252"],
118 [0x0456,"gl-ES","CP1252"],
119 [0x0037,"ka","UNICODE"],
120 [0x0437,"ka-GE","UNICODE"],
121 [0x0007,"de","CP1252"],
122 [0x0C07,"de-AT","CP1252"],
123 [0x0407,"de-DE","CP1252"],
124 [0x1407,"de-LI","CP1252"],
125 [0x1007,"de-LU","CP1252"],
126 [0x0807,"de-CH","CP1252"],
127 [0x0008,"el","CP1253"],
128 [0x0408,"el-GR","CP1253"],
129 [0x046F,"kl-GL","CP1252"],
130 [0x0047,"gu","UNICODE"],
131 [0x0447,"gu-IN","UNICODE"],
132 [0x0468,"ha-Latn-NG","CP1252"],
133 [0x000D,"he","CP1255"],
134 [0x040D,"he-IL","CP1255"],
135 [0x0039,"hi","UNICODE"],
136 [0x0439,"hi-IN","UNICODE"],
137 [0x000E,"hu","CP1250"],
138 [0x040E,"hu-HU","CP1250"],
139 [0x000F,"is","CP1252"],
140 [0x040F,"is-IS","CP1252"],
141 [0x0470,"ig-NG","CP1252"],
142 [0x0021,"id","CP1252"],
143 [0x0421,"id-ID","CP1252"],
144 [0x085D,"iu-Latn-CA","CP1252"],
145 [0x045D,"iu-Cans-CA","UNICODE"],
146 [0x083C,"ga-IE","CP1252"],
147 [0x0434,"xh-ZA","CP1252"],
148 [0x0435,"zu-ZA","CP1252"],
149 [0x0010,"it","CP1252"],
150 [0x0410,"it-IT","CP1252"],
151 [0x0810,"it-CH","CP1252"],
152 [0x0011,"ja","CP932"],
153 [0x0411,"ja-JP","CP932"],
154 [0x004B,"kn","UNICODE"],
155 [0x044B,"kn-IN","UNICODE"],
156 [0x003F,"kk","CP1251"],
157 [0x043F,"kk-KZ","CP1251"],
158 [0x0453,"km-KH","UNICODE"],
159 [0x0486,"qut-GT","CP1252"],
160 [0x0487,"rw-RW","CP1252"],
161 [0x0041,"sw","CP1252"],
162 [0x0441,"sw-KE","CP1252"],
163 [0x0057,"kok","UNICODE"],
164 [0x0457,"kok-IN","UNICODE"],
165 [0x0012,"ko","CP949"],
166 [0x0412,"ko-KR","CP949"],
167 [0x0040,"ky","CP1251"],
168 [0x0440,"ky-KG","CP1251"],
169 [0x0454,"lo-LA","UNICODE"],
170 [0x0026,"lv","CP1257"],
171 [0x0426,"lv-LV","CP1257"],
172 [0x0027,"lt","CP1257"],
173 [0x0427,"lt-LT","CP1257"],
174 [0x082E,"wee-DE","CP1252"],
175 [0x046E,"lb-LU","CP1252"],
176 [0x002F,"mk","CP1251"],
177 [0x042F,"mk-MK","CP1251"],
178 [0x003E,"ms","CP1252"],
179 [0x083E,"ms-BN","CP1252"],
180 [0x043E,"ms-MY","CP1252"],
181 [0x044C,"ml-IN","UNICODE"],
182 [0x043A,"mt-MT","UNICODE"],
183 [0x0481,"mi-NZ","UNICODE"],
184 [0x047A,"arn-CL","CP1252"],
185 [0x004E,"mr","UNICODE"],
186 [0x044E,"mr-IN","UNICODE"],
187 [0x047C,"moh-CA","CP1252"],
188 [0x0050,"mn","CP1251"],
189 [0x0450,"mn-MN","CP1251"],
190 [0x0850,"mn-Mong-CN","UNICODE"],
191 [0x0461,"ne-NP","UNICODE"],
192 [0x0014,"no","CP1252"],
193 [0x0414,"nb-NO","CP1252"],
194 [0x0814,"nn-NO","CP1252"],
195 [0x0482,"oc-FR","CP1252"],
196 [0x0448,"or-IN","UNICODE"],
197 [0x0463,"ps-AF","UNICODE"],
198 [0x0029,"fa","CP1256"],
199 [0x0429,"fa-IR","CP1256"],
200 [0x0015,"pl","CP1250"],
201 [0x0415,"pl-PL","CP1250"],
202 [0x0016,"pt","CP1252"],
203 [0x0416,"pt-BR","CP1252"],
204 [0x0816,"pt-PT","CP1252"],
205 [0x0046,"pa","UNICODE"],
206 [0x0446,"pa-IN","UNICODE"],
207 [0x046B,"quz-BO","CP1252"],
208 [0x086B,"quz-EC","CP1252"],
209 [0x0C6B,"quz-PE","CP1252"],
210 [0x0018,"ro","CP1250"],
211 [0x0418,"ro-RO","CP1250"],
212 [0x0417,"rm-CH","CP1252"],
213 [0x0019,"ru","CP1251"],
214 [0x0419,"ru-RU","CP1251"],
215 [0x243B,"smn-FI","CP1252"],
216 [0x103B,"smj-NO","CP1252"],
217 [0x143B,"smj-SE","CP1252"],
218 [0x0C3B,"se-FI","CP1252"],
219 [0x043B,"se-NO","CP1252"],
220 [0x083B,"se-SE","CP1252"],
221 [0x203B,"sms-FI","CP1252"],
222 [0x183B,"sma-NO","CP1252"],
223 [0x1C3B,"sma-SE","CP1252"],
224 [0x004F,"sa","UNICODE"],
225 [0x044F,"sa-IN","UNICODE"],
226 [0x7C1A,"sr","CP1251"],
227 [0x1C1A,"sr-Cyrl-BA","CP1251"],
228 [0x0C1A,"sr-Cyrl-SP","CP1251"],
229 [0x181A,"sr-Latn-BA","CP1250"],
230 [0x081A,"sr-Latn-SP","CP1250"],
231 [0x046C,"nso-ZA","CP1252"],
232 [0x0432,"tn-ZA","CP1252"],
233 [0x045B,"si-LK","UNICODE"],
234 [0x001B,"sk","CP1250"],
235 [0x041B,"sk-SK","CP1250"],
236 [0x0024,"sl","CP1250"],
237 [0x0424,"sl-SI","CP1250"],
238 [0x000A,"es","CP1252"],
239 [0x2C0A,"es-AR","CP1252"],
240 [0x400A,"es-BO","CP1252"],
241 [0x340A,"es-CL","CP1252"],
242 [0x240A,"es-CO","CP1252"],
243 [0x140A,"es-CR","CP1252"],
244 [0x1C0A,"es-DO","CP1252"],
245 [0x300A,"es-EC","CP1252"],
246 [0x440A,"es-SV","CP1252"],
247 [0x100A,"es-GT","CP1252"],
248 [0x480A,"es-HN","CP1252"],
249 [0x080A,"es-MX","CP1252"],
250 [0x4C0A,"es-NI","CP1252"],
251 [0x180A,"es-PA","CP1252"],
252 [0x3C0A,"es-PY","CP1252"],
253 [0x280A,"es-PE","CP1252"],
254 [0x500A,"es-PR","CP1252"],
255 [0x0C0A,"es-ES","CP1252"],
256 [0x540A,"es-US","CP1252"],
257 [0x380A,"es-UY","CP1252"],
258 [0x200A,"es-VE","CP1252"],
259 [0x001D,"sv","CP1252"],
260 [0x081D,"sv-FI","CP1252"],
261 [0x041D,"sv-SE","CP1252"],
262 [0x005A,"syr","UNICODE"],
263 [0x045A,"syr-SY","UNICODE"],
264 [0x0428,"tg-Cyrl-TJ","CP1251"],
265 [0x085F,"tmz-Latn-DZ","CP1252"],
266 [0x0049,"ta","UNICODE"],
267 [0x0449,"ta-IN","UNICODE"],
268 [0x0044,"tt","CP1251"],
269 [0x0444,"tt-RU","CP1251"],
270 [0x004A,"te","UNICODE"],
271 [0x044A,"te-IN","UNICODE"],
272 [0x001E,"th","CP874"],
273 [0x041E,"th-TH","CP874"],
274 [0x0451,"bo-CN","UNICODE"],
275 [0x001F,"tr","CP1254"],
276 [0x041F,"tr-TR","CP1254"],
277 [0x0442,"tk-TM","CP1250"],
278 [0x0480,"ug-CN","CP1256"],
279 [0x0022,"uk","CP1251"],
280 [0x0422,"uk-UA","CP1251"],
281 [0x042E,"wen-DE","CP1252"],
282 [0x0020,"ur","CP1256"],
283 [0x0420,"ur-PK","CP1256"],
284 [0x0043,"uz","CP1254"],
285 [0x0843,"uz-Cyrl-UZ","CP1251"],
286 [0x0443,"uz-Latn-UZ","CP1254"],
287 [0x002A,"vi","CP1258"],
288 [0x042A,"vi-VN","CP1258"],
289 [0x0452,"cy-GB","CP1252"],
290 [0x0488,"wo-SN","CP1252"],
291 [0x0485,"sah-RU","CP1251"],
292 [0x0478,"ii-CN","UNICODE"],
293 [0x046A,"yo-NG","CP1252"],
294 ]
295 end
296 end
297 end
0 # -*- coding: utf-8 -*-
1 #
2 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
3 #
4 # License: Ruby's or LGPL
5 #
6 # This library is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Lesser General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Lesser General Public License for more details.
15 #
16 # You should have received a copy of the GNU Lesser General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19 module Locale
20 module Driver
21 MODULES = {}
22 end
23 end
0 # encoding: UTF-8
1 =begin
2
3 language.rb - Locale::Info::Language class
4
5 Copyright (C) 2008 Masao Mutoh
6
7 Original Author:: Brian Pontarelli
8
9 $Id: language.rb 27 2008-12-03 15:06:50Z mutoh $
10 =end
11
12 require 'zlib'
13
14 module Locale
15
16 module Info
17 # This class contains all the of the ISO information for the ISO 639-3
18 # languages. This class is immutable once constructed.
19 class Language
20 attr_reader :two_code, :three_code, :scope, :type, :name
21
22 #
23 # Constructs a new Language instance.
24 #
25 # * code The 2 or 3 digit ISO 639-3 language code.
26 # * scope A single character that defines the ISO scope of the language - <tt>(I)ndividual</tt>,
27 # <tt>(M)acrolanguage</tt>, or <tt>(S)pecial</tt>.
28 # * type A single character that defines the ISO type of the language - <tt>(A)ncient</tt>,
29 # <tt>(C)onstructed</tt>, <tt>(E)xtinct</tt>, <tt>(H)istorical</tt>, <tt>(L)iving</tt>,
30 # or <tt>(S)pecial</tt>.
31 # * name The name of the language.
32 #
33 def initialize(two_code, three_code, scope, type, name)
34 @two_code, @three_code, @scope, @type, @name = two_code, three_code, scope, type, name
35
36 @individual = (scope == "I")
37 @macro = (scope == "M")
38 @special = (scope == "S")
39 @constructed = (type == "C")
40 @living = (type == "L")
41 @extinct = (type == "E")
42 @ancient = (type == "A")
43 @historical = (type == "H")
44 @special_type = (type == "S")
45 end
46
47 # Returns true if the language is an individual language according to the ISO 639-3 data.
48 def individual?; @individual; end
49
50 # Returns true if the language is a macro language according to the ISO 639-3 data.
51 def macro?; @macro; end
52
53 # Returns true if the language is a special language according to the ISO 639-3 data.
54 def special?; @special; end
55
56 # Returns true if the language is a constructed language according to the ISO 639-3 data.
57 def constructed?; @constructed; end
58
59 # Returns true if the language is a living language according to the ISO 639-3 data.
60 def living?; @living; end
61
62 # Returns true if the language is an extinct language according to the ISO 639-3 data.
63 def extinct?; @extinct; end
64
65 # Returns true if the language is an ancient language according to the ISO 639-3 data.
66 def ancient?; @ancient; end
67
68 # Returns true if the language is an historical language according to the ISO 639-3 data.
69 def historical?; @historical; end
70
71 # Returns true if the language is a special type language according to the ISO 639-3 data.
72 def special_type?; @special_type; end
73
74 # Returns the two or three code.
75 def to_s
76 if two_code and two_code.size > 0
77 two_code
78 else
79 three_code
80 end
81 end
82
83 # Returns this object is valid as ISO 639 data.
84 def iso_language?
85 @@lang_two_codes[two_code] != nil || @@lang_three_codes[three_code] != nil
86 end
87 end
88
89 @@lang_two_codes = Hash.new
90 @@lang_three_codes = Hash.new
91
92 Zlib::GzipReader.open(File.dirname(__FILE__) + "/../data/languages.tab.gz") do |gz|
93 gz.readlines.each do |l|
94 l.force_encoding('UTF-8') if l.respond_to?(:force_encoding)
95 unless l =~ /^\s*$/
96 parts = l.split(/\t/)
97 lang = Language.new(parts[2], parts[0], parts[3], parts[4], parts[5].strip)
98 @@lang_three_codes[parts[0]] = lang
99 @@lang_two_codes[parts[2]] = lang if parts[2].length > 0
100 end
101 end
102 end
103
104 module_function
105
106 # Returns a hash of all the ISO languages. The hash is {String, language} where
107 # the string is the 3 digit language code from the ISO 639 data. This contains
108 # all of the data from the ISO 639-3 data (7600 Languages).
109 #
110 # Need to require 'locale/info' or 'locale/language'.
111 def three_languages
112 @@lang_three_codes
113 end
114
115 # Returns a hash of all the ISO languages. The hash is {String, language} where
116 # the string is the 2 digit language code from the ISO 639-1 data. This contains
117 # all of the data from the ISO 639-1 data (186 Languages).
118 #
119 # Need to require 'locale/info' or 'locale/language'.
120 def two_languages
121 @@lang_two_codes
122 end
123
124 # Returns the language for the given 2 or 3 digit code.
125 #
126 # Need to require 'locale/info' or 'locale/language'.
127 def get_language(code)
128 @@lang_three_codes[code] || @@lang_two_codes[code]
129 end
130
131 # Returns the language code is valid.
132 #
133 # Need to require 'locale/info' or 'locale/language'.
134 def language_code?(code)
135 get_language(code) != nil
136 end
137 end
138 end
0 # encoding: UTF-8
1 =begin
2
3 region.rb - Locale::Info::Region class
4
5 Copyright (C) 2008 Masao Mutoh
6
7 First Author:: Brian Pontarelli
8
9 $Id: region.rb 27 2008-12-03 15:06:50Z mutoh $
10 =end
11
12 require 'zlib'
13
14 module Locale
15
16 module Info
17 # This class models out a region/country from the ISO 3166 standard for region codes.
18 # In ISO3166, it's called "Country" but Ruby/Locale the word "Region" instead.
19 class Region
20 attr_reader :code, :name
21
22 # code:: The 2 or 3 digit ISO 3166 region code.
23 # name:: The name of the region.
24 def initialize(code, name)
25 @code = code
26 @name = name
27 end
28
29 def iso_region?
30 @@regions[code] != nil
31 end
32
33 def to_s
34 "#{code}"
35 end
36 end
37
38 @@regions = Hash.new
39 Zlib::GzipReader.open(File.dirname(__FILE__) + "/../data/regions.tab.gz") do |gz|
40 gz.readlines.each do |l|
41 l.force_encoding('UTF-8') if l.respond_to?(:force_encoding)
42 unless l =~ /^\s*$/
43 parts = l.split(/\t/)
44 region = Region.new(parts[0], parts[1].strip)
45 @@regions[parts[0]] = region
46 end
47 end
48 end
49
50 module_function
51
52 # Returns a hash of all the ISO regions. The hash is {String, Region} where
53 # the string is the 2 digit region code from the ISO 3166 data.
54 #
55 # You need to require 'locale/info' or 'locale/region'.
56 def regions
57 @@regions
58 end
59
60 # Returns the region for the given code.
61 #
62 # You need to require 'locale/info' or 'locale/info/region'.
63 def get_region(code)
64 @@regions[code]
65 end
66
67 # Returns the region code is valid.
68 #
69 # You need to require 'locale/info' or 'locale/info/region'.
70 def valid_region_code?(code)
71 @@regions[code] != nil
72 end
73 end
74 end
0 =begin
1
2 info.rb - Load Locale::Info::Language and Locale::Info::Region.
3
4 Copyright (C) 2008 Masao Mutoh
5
6 $Id: info.rb 27 2008-12-03 15:06:50Z mutoh $
7 =end
8
9 require 'locale/info/language'
10 require 'locale/info/region'
11
0 # Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
1 #
2 # License: Ruby's or LGPL
3 #
4 # This library is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Lesser General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This library is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Lesser General Public License for more details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 require "locale"
18
19 module Locale
20 class Middleware
21 def initialize(application, options={})
22 @application = application
23 @options = options
24 Locale.init(:driver => :cgi)
25 end
26
27 def call(environment)
28 request = Rack::Request.new(environment)
29 Locale.set_request([request["lang"]],
30 [request.cookies["lang"]],
31 environment["HTTP_ACCEPT_LANGUAGE"],
32 environment["HTTP_ACCEPT_CHARSET"])
33 @application.call(environment)
34 end
35 end
36 end
37
0 =begin
1 locale/tag/cldr.rb - Locale::Tag::CLDR
2
3 Copyright (C) 2008,2009 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7 =end
8
9 require 'locale/tag/common'
10
11 module Locale
12 module Tag
13
14 # Unicode locale identifier class for CLDR-1.6.1.
15 # (Unicode Common Locale Data Repository).
16 class Cldr < Common
17
18 VARIANT = "(#{ALPHANUM}{5,8}|#{DIGIT}#{ALPHANUM}{3})"
19 EXTENSION = "#{ALPHANUM}+=[a-z0-9\-]+"
20
21 TAG_RE = /\A#{LANGUAGE}(?:[-_]#{SCRIPT})?
22 (?:[-_]#{REGION})?((?:[-_]#{VARIANT})*
23 (?:@(#{EXTENSION};?)+)*)\Z/ix
24
25 attr_reader :extensions
26
27 class << self
28 # Parse the language tag and return the new Locale::Tag::CLDR.
29 def parse(tag)
30 if tag =~ /\APOSIX\Z/ # This is the special case of POSIX locale but match this regexp.
31 nil
32 elsif tag =~ TAG_RE
33 lang, script, region, subtag = $1, $2, $3, $4
34
35 extensions = {}
36 subtag.scan(/#{EXTENSION}/i).each{|v|
37 subtag.sub!(v, "")
38 key, type = v.split("=")
39 extensions[key] = type
40 }
41 variants = subtag.scan(/#{VARIANT}/i).collect{|v| v[0].upcase}
42
43 ret = self.new(lang, script, region, variants, extensions)
44 ret.tag = tag
45 ret
46 else
47 nil
48 end
49 end
50 end
51
52 # Create Locale::Tag::Cldr.
53 #
54 # variants should be upcase.
55 def initialize(language, script = nil, region = nil,
56 variants = [], extensions = {})
57 @extensions = extensions
58 super(language, script, region, variants.map{|v| v.upcase})
59 end
60
61 # Sets the extensions as an Hash.
62 def extensions=(val)
63 @extensions = val
64 end
65
66 private
67 def convert_to(klass) # :nodoc:
68 if klass == Cldr
69 klass.new(language, script, region, variants, extensions)
70 elsif klass == Rfc
71 exts = []
72 @extensions.to_a.sort.each do |k, v|
73 exts << "k-#{k[0,8]}-#{v[0,8]}"
74 end
75
76 klass.new(language, script, region, variants, exts)
77 else
78 super
79 end
80 end
81
82 # Returns the language tag.
83 # (e.g.) "ja_Hira_JP_VARIANT1_VARIANT2@foo1=var1;foo2=var2"
84 #
85 # This is used in internal only. Use to_s instead.
86 def to_string
87 s = super
88 if @extensions.size > 0
89 s << "@" << @extensions.to_a.sort.map{|k, v| "#{k}=#{v}"}.join(";")
90 end
91 s
92 end
93 end
94 end
95 end
0 =begin
1 locale/tag/common.rb - Locale::Tag::Common
2
3 Copyright (C) 2008,2009 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7 =end
8
9 require 'locale/tag/simple'
10
11 module Locale
12 module Tag
13 # Common Language tag class for Ruby.
14 # Java and MS Windows use this format.
15 #
16 # * ja (language: RFC4646)
17 # * ja_JP (country: RFC4646(2 alpha or 3 digit))
18 # * ja-JP
19 # * ja_Hira_JP (script: 4 characters)
20 # * ja-Hira-JP
21 # * ja_Hira_JP_MOBILE (variants: more than 2 characters or 3 digit)
22 # * ja_Hira_JP_MOBILE_IPHONE (2 variants example)
23 #
24 class Common < Simple
25 LANGUAGE = "(#{ALPHA}{2,3}|#{ALPHA}{4}|#{ALPHA}{5,8})" #RFC4646 (ISO639/reserved/registered)
26 SCRIPT = "(#{ALPHA}{4})"
27 VARIANT = "(#{ALPHANUM}{3,}|#{DIGIT}#{ALPHANUM}{3})" #RFC3066 compatible
28
29 TAG_RE = /\A#{LANGUAGE}(?:[-_]#{SCRIPT})?
30 (?:[-_]#{REGION})?((?:[-_]#{VARIANT})*)\Z/ix
31
32 attr_reader :script, :variants
33
34 class << self
35 # Parse the language tag and return the new Locale::Tag::Common.
36 def parse(tag)
37 if tag =~ /\APOSIX\Z/ # This is the special case of POSIX locale but match this regexp.
38 nil
39 elsif tag =~ TAG_RE
40 lang, script, region, subtag = $1, $2, $3, $4
41 variants = subtag.scan(/(^|[-_])#{VARIANT}(?=([-_]|$))/i).collect{|v| v[1]}
42
43 ret = self.new(lang, script, region, variants)
44 ret.tag = tag
45 ret
46 else
47 nil
48 end
49 end
50 end
51
52 # Create a Locale::Tag::Common.
53 def initialize(language, script = nil, region = nil, variants = [])
54 @script, @variants = script, variants
55 @script = @script.capitalize if @script
56 super(language, region)
57 end
58
59 # Set the script (with capitalize)
60 def script=(val)
61 @script = val
62 @script = @script.capitalize if @script
63 @script
64 end
65
66 # Set the variants as an Array.
67 def variants=(val)
68 @variants = val
69 end
70
71 # Returns an Array of tag-candidates order by priority.
72 # Use Locale.candidates instead of this method.
73 #
74 # Locale::Tag::Rfc, Cldr don't have their own candidates,
75 # because it's meaningless to compare the extensions, privateuse, etc.
76 def candidates
77 [self.class.new(language, script, region, variants), #ja-Kana-JP-FOO
78 self.class.new(language, script, region), #ja-Kana-JP
79 self.class.new(language, nil, region, variants), #ja-JP-FOO
80 self.class.new(language, nil, region), #ja-JP
81 self.class.new(language, script, nil, variants), #ja-Kana-FOO
82 self.class.new(language, script), #ja-Kana
83 self.class.new(language, nil, nil, variants), #ja-FOO
84 self.class.new(language)] #ja
85 end
86
87 private
88 def convert_to(klass) #:nodoc:
89 if klass == Simple
90 super
91 elsif klass == Posix
92 if variants.size > 0
93 var = variants.join("-")
94 else
95 var = nil
96 end
97 klass.new(language, region, nil, var)
98 elsif klass == Cldr
99 klass.new(language, script, region, variants.map{|v| v.upcase})
100 else
101 klass.new(language, script, region, variants)
102 end
103 end
104
105 # Returns the common language tag with "_".
106 # <language>_<Script>_<REGION>_VARIANTS1_VARIANTS2
107 # (e.g.) "ja_Hira_JP_VARIANTS1_VARIANTS2"
108 #
109 # This is used in internal only. Use to_s instead.
110 def to_string
111 s = @language.dup
112
113 s << "_" << @script if @script
114 s << "_" << @region if @region
115
116 @variants.each do |v|
117 s << "_#{v}"
118 end
119 s
120 end
121 end
122 end
123 end
0 =begin
1 locale/tag/irregular.rb - Locale::Tag::Irregular
2
3 Copyright (C) 2008 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7
8 $Id: irregular.rb 27 2008-12-03 15:06:50Z mutoh $
9 =end
10
11 require 'locale/tag/simple'
12
13 module Locale
14
15 module Tag
16 # Broken tag class.
17 class Irregular < Simple
18
19 def initialize(tag)
20 tag = "en" if tag == nil or tag == ""
21 super(tag.to_s)
22 @tag = tag
23 end
24
25 # Returns an Array of tag-candidates order by priority.
26 def candidates
27 [Irregular.new(tag)]
28 end
29
30 # Conver to the klass(the class of Language::Tag)
31 private
32 def convert_to(klass)
33 klass.new(tag)
34 end
35 end
36 end
37 end
0 =begin
1 locale/tag/posix.rb - Locale::Tag::Posix
2
3 Copyright (C) 2008 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7
8 $Id: posix.rb 27 2008-12-03 15:06:50Z mutoh $
9 =end
10
11 module Locale
12 module Tag
13
14 # Locale tag class for POSIX locale
15 # * ja
16 # * ja_JP
17 # * ja_JP.UTF-8
18 # * ja_JP.UTF-8@Osaka
19 # * C/POSIX (-> en_US)
20 class Posix < Simple
21 LANGUAGE = "([a-z]{2,})"
22 TAG_RE = /\A#{LANGUAGE}(?:_#{REGION})?(?:\.([^@]+))?(?:@(.*))?\Z/i
23
24 attr_reader :charset, :modifier
25
26 def initialize(language, region = nil, charset = nil, modifier = nil)
27 @charset, @modifier = charset, modifier
28 super(language, region)
29 end
30
31 def self.parse(tag)
32 if tag =~ /^(C|POSIX)$/
33 ret = self.new("en", "US")
34 ret.tag = tag
35 ret
36 elsif tag =~ TAG_RE
37 ret = self.new($1, $2, $3, $4)
38 ret.tag = tag
39 ret
40 else
41 nil
42 end
43 end
44
45 # Returns the language tag.
46 # <language>_<COUNTRY>.<CHARSET>@<MODIFIER>
47 # (e.g.) "ja_JP.EUC-JP@Modifier"
48 def to_s
49 s = @language.dup
50 s << "_#{@region}" if @region
51 s << ".#{@charset}" if @charset
52 s << "@#{@modifier}" if @modifier
53 s
54 end
55
56 # Set the charset.
57 def charset=(val)
58 @charset = val
59 end
60
61 # Set the modifier as a String
62 def modifier=(val)
63 @modifier = val
64 end
65
66 # Returns an Array of tag-candidates order by priority.
67 # Use Locale.candidates instead of this method.
68 def candidates
69 [self.class.new(language, region, charset, modifier), #ja_JP.UTF-8@Modifier
70 self.class.new(language, region, charset), #ja_JP.UTF-8
71 self.class.new(language, region, nil, modifier), #ja_JP@Modifier
72 self.class.new(language, region, nil, nil), #ja_JP@Modifier
73 self.class.new(language, nil, charset, modifier), #ja.UTF-8@Modifier
74 self.class.new(language, nil, charset), #ja.UTF-8
75 self.class.new(language, nil, nil, modifier), #ja@Modifier
76 self.class.new(language)] #ja
77 end
78
79 # A modifier is converted to a variant.
80 # If the modifier is less than 5 characters, it is not canonical value.
81 private
82 def convert_to(klass)
83 if klass == Simple
84 super
85 elsif klass == Posix
86 klass.new(language, region, charset, modifier)
87 else
88 klass.new(language, nil, region, modifier ? [modifier] : [])
89 end
90 end
91
92 end
93 end
94 end
0 =begin
1 locale/tag/rfc.rb - Locale::Tag::Rfc
2
3 Copyright (C) 2008,2009 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7 =end
8
9 require 'locale/tag/common'
10
11 module Locale
12 module Tag
13
14 # Language tag class for RFC4646(BCP47).
15 class Rfc < Common
16 SINGLETON = '[a-wyz0-9]'
17 VARIANT = "(#{ALPHANUM}{5,8}|#{DIGIT}#{ALPHANUM}{3})"
18 EXTENSION = "(#{SINGLETON}(?:-#{ALPHANUM}{2,8})+)"
19 PRIVATEUSE = "(x(?:-#{ALPHANUM}{1,8})+)"
20 GRANDFATHERED = "#{ALPHA}{1,3}(?:-#{ALPHANUM}{2,8}){1,2}"
21
22 TAG_RE = /\A#{LANGUAGE}(?:-#{SCRIPT})?
23 (?:-#{REGION})?((?:-#{VARIANT})*
24 (?:-#{EXTENSION})*(?:-#{PRIVATEUSE})?)\Z/ix
25
26 attr_reader :extensions, :privateuse
27
28 class << self
29 # Parse the language tag and return the new Locale::Tag::Rfc.
30 def parse(tag)
31 if tag =~ /\APOSIX\Z/ # This is the special case of POSIX locale but match this regexp.
32 nil
33 elsif tag =~ TAG_RE
34 lang, script, region, subtag = $1, $2, $3, $4
35 extensions = []
36 variants = []
37 if subtag =~ /#{PRIVATEUSE}/
38 subtag, privateuse = $`, $1
39 # Private use for CLDR.
40 if /x-ldml(.*)/ =~ privateuse
41 p_subtag = $1
42 extensions = p_subtag.scan(/(^|-)#{EXTENSION}/i).collect{|v| p_subtag.sub!(v[1], ""); v[1]}
43 variants = p_subtag.scan(/(^|-)#{VARIANT}(?=(-|$))/i).collect{|v| v[1]}
44 end
45 end
46 extensions += subtag.scan(/(^|-)#{EXTENSION}/i).collect{|v| subtag.sub!(v[1], ""); v[1]}
47 variants += subtag.scan(/(^|-)#{VARIANT}(?=(-|$))/i).collect{|v| v[1]}
48
49 ret = self.new(lang, script, region, variants, extensions, privateuse)
50 ret.tag = tag
51 ret
52 else
53 nil
54 end
55 end
56 end
57
58 def initialize(language, script = nil, region = nil, variants = [],
59 extensions = [], privateuse = nil)
60 @extensions, @privateuse = extensions, privateuse
61 super(language, script, region, variants)
62 end
63
64 # Sets the extensions as an Array.
65 def extensions=(val)
66 @extensions = val
67 end
68
69 # Sets the privateuse as a String
70 def privateuse=(val)
71 @privateuse = val
72 end
73
74 private
75 def convert_to(klass)
76 if klass == Rfc
77 klass.new(language, script, region, variants, extensions, privateuse)
78 elsif klass == Cldr
79 exts = {}
80 extensions.sort.each do |v|
81 if v =~ /^k-(#{ALPHANUM}{2,})-(.*)$/i
82 exts[$1] = $2
83 end
84 end
85 klass.new(language, script, region, variants, exts)
86 else
87 super
88 end
89 end
90
91 # Returns the language tag
92 # <language>-<Script>-<REGION>-<variants>-<extensions>-<PRIVATEUSE>
93 # (e.g.) "ja-Hira-JP-variant"
94 #
95 # This is used in internal only. Use to_s instead.
96 def to_string
97 s = super.gsub(/_/, "-")
98 @extensions.sort.each do |v|
99 s << "-#{v}"
100 end
101 s << "-#{@privateuse}" if @privateuse
102 s
103 end
104
105 end
106 end
107 end
0 =begin
1 locale/tag/simple.rb - Locale::Tag::Simple
2
3 Copyright (C) 2008,2009 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7 =end
8
9 module Locale
10 module Tag
11 # Abstract language tag class.
12 # This class has <language>, <region> which
13 # all of language tag specifications have.
14 #
15 # * ja (language: ISO 639 (2 or 3 alpha))
16 # * ja_JP (country: RFC4646 (ISO3166/UN M.49) (2 alpha or 3 digit)
17 # * ja-JP
18 # * ja-392
19 class Simple
20 ALPHA = '[a-z]'
21 DIGIT = '[0-9]'
22 ALPHANUM = "[a-zA-Z0-9]"
23
24 LANGUAGE = "(#{ALPHA}{2,3})" # ISO 639
25 REGION = "(#{ALPHA}{2}|#{DIGIT}{3})" # RFC4646 (ISO3166/UN M.49)
26
27 TAG_RE = /\A#{LANGUAGE}(?:[_-]#{REGION})?\Z/i
28
29 attr_reader :language, :region
30
31 # tag is set when .parse method is called.
32 # This value is used when the program want to know the original
33 # String.
34 attr_accessor :tag
35
36 # call-seq:
37 # to_common
38 # to_posix
39 # to_rfc
40 # to_cldr
41 #
42 # Convert to each tag classes.
43 [:simple, :common, :posix, :rfc, :cldr].each do |name|
44 class_eval <<-EOS
45 def to_#{name}
46 convert_to(#{name.to_s.capitalize})
47 end
48 EOS
49 end
50
51 class << self
52 # Parse the language tag and return the new Locale::Tag::Simple.
53 def parse(tag)
54 if tag =~ TAG_RE
55 ret = self.new($1, $2)
56 ret.tag = tag
57 ret
58 else
59 nil
60 end
61 end
62 end
63
64 # Create a Locale::Tag::Simple
65 def initialize(language, region = nil)
66 raise "language can't be nil." unless language
67 @language, @region = language, region
68 @language = @language.downcase if @language
69 @region = @region.upcase if @region
70 end
71
72 # Returns the language tag as the String.
73 # <language>_<REGION>
74 # (e.g.) "ja_JP"
75 def to_s
76 to_string
77 end
78
79 def to_str #:nodoc:
80 to_s
81 end
82
83 def <=>(other)
84 self.to_s <=> other.to_s
85 end
86
87 def ==(other) #:nodoc:
88 other != nil and hash == other.hash
89 end
90
91 def eql?(other) #:nodoc:
92 self.==(other)
93 end
94
95 def hash #:nodoc:
96 "#{self.class}:#{to_s}".hash
97 end
98
99 def inspect #:nodoc:
100 %Q[#<#{self.class}: #{to_s}>]
101 end
102
103 # For backward compatibility.
104 def country; region end
105
106 # Set the language (with downcase)
107 def language=(val)
108 @language = val
109 @language = @language.downcase if @language
110 @language
111 end
112
113 # Set the region (with upcase)
114 def region=(val)
115 @region = val
116 @region = @region.upcase if @region
117 @region
118 end
119
120 # Returns an Array of tag-candidates order by priority.
121 # Use Locale.candidates instead of this method.
122 def candidates
123 [self.class.new(language, region), self.class.new(language)]
124 end
125
126 # Convert to the klass(the class of Language::Tag)
127 private
128 def convert_to(klass) #:nodoc:
129 if klass == Simple || klass == Posix
130 klass.new(language, region)
131 else
132 klass.new(language, nil, region)
133 end
134 end
135
136 # Return simple language tag which format is"<lanuguage>_<REGION>".
137 # This is to use internal only. Use to_s instead.
138 def to_string
139 s = @language.dup
140 s << "_" << @region if @region
141 s
142 end
143 end
144 end
145 end
0 =begin
1 tag.rb - Locale::Tag module
2
3 Copyright (C) 2008,2009 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7 =end
8
9 require 'locale/tag/simple'
10 require 'locale/tag/irregular'
11 require 'locale/tag/common'
12 require 'locale/tag/rfc'
13 require 'locale/tag/cldr'
14 require 'locale/tag/posix'
15
16 module Locale
17
18 # Language tag / locale identifiers.
19 module Tag
20 module_function
21 # Parse a language tag/locale name and return Locale::Tag
22 # object.
23 # * tag: a tag as a String. e.g.) ja-Hira-JP
24 # * Returns: a Locale::Tag subclass.
25 def parse(tag)
26 # Common is not used here.
27 [Simple, Common, Rfc, Cldr, Posix].each do |parser|
28 ret = parser.parse(tag)
29 return ret if ret
30 end
31 Locale::Tag::Irregular.new(tag)
32 end
33 end
34 end
35
0 =begin
1 taglist.rb - Locale module
2
3 Copyright (C) 2008 Masao Mutoh
4
5 You may redistribute it and/or modify it under the same
6 license terms as Ruby.
7
8 $Id: taglist.rb 27 2008-12-03 15:06:50Z mutoh $
9 =end
10
11 module Locale
12 # This provides the subclass of Array which behaves like
13 # the first(top priority) Locale::Tag object.
14 # "Locale.current.language" is same with "Locale.current[0].language".
15 #
16 # Locale.current returns an Array of Tag(s) now.
17 # But the old Locale.current(Ruby-GetText) and Locale.get
18 # returns Locale::Object (similier with Locale::Tag::Posix).
19 # This is the class for backward compatibility.
20 #
21 # It is recommanded to use Locale.current[0] or
22 # Locale.candidates to find the current locale instead
23 # of this function.
24 #
25 class TagList < Array
26 # Returns the top priority language. (simple)
27 def language
28 self[0].language
29 end
30 # Returns the top priority region/country. (simple)
31 def country
32 self[0].region
33 end
34 # Returns the top priority region/country. (simple)
35 def region
36 self[0].region
37 end
38 # Returns the top priority script. (common)
39 def script
40 self[0].script
41 end
42 # Returns the top priority charset. (posix)
43 def charset
44 top_priority_charset = nil
45 first_tag = self[0]
46 if first_tag.respond_to?(:charset)
47 top_priority_charset = first_tag.charset
48 end
49 top_priority_charset ||= ::Locale.driver_module.charset
50 top_priority_charset
51 end
52
53 # Returns the top priority modifier. (posix)
54 def modifier
55 (self[0].respond_to? :modifier) ? self[0].modifier : nil
56 end
57
58 # Returns the top priority variants.(common, rfc, cldr)
59 def variants
60 (self[0].respond_to? :variants) ? self[0].variants : nil
61 end
62
63 # Returns the top priority extensions.(common, rfc, cldr)
64 def extensions
65 (self[0].respond_to? :extensions) ? self[0].extensions : nil
66 end
67
68 # Returns the top priority privateuse(rfc)
69 def privateuse
70 (self[0].respond_to? :privateuse) ? self[0].privateuse : nil
71 end
72
73 def to_str
74 self[0].to_str
75 end
76
77 def to_s
78 self[0].to_s
79 end
80
81 def to_common
82 self[0].to_common
83 end
84
85 def to_simple
86 self[0].to_simple
87 end
88
89 def to_rfc
90 self[0].to_rfc
91 end
92
93 def to_cldr
94 self[0].to_cldr
95 end
96
97 def to_posix
98 self[0].to_posix
99 end
100 end
101 end
0 =begin
1 version - version information of Ruby-Locale
2
3 Copyright (C) 2008 Masao Mutoh
4 Copyright (C) 2013-2015 Kouhei Sutou <kou@clear-code.com>
5
6 You may redistribute it and/or modify it under the same
7 license terms as Ruby.
8 =end
9 module Locale
10 VERSION = "2.1.2"
11 end
12
0 =begin
1 locale.rb - Locale module
2
3 Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
4 Copyright (C) 2002-2009 Masao Mutoh
5
6 You may redistribute it and/or modify it under the same
7 license terms as Ruby.
8
9 Original: Ruby-GetText-Package-1.92.0.
10
11 $Id: locale.rb 27 2008-12-03 15:06:50Z mutoh $
12 =end
13
14 require 'locale/tag'
15 require 'locale/taglist'
16 require 'locale/driver'
17 require 'locale/version'
18
19 # Locale module manages the locale informations of the application.
20 # These functions are the most important APIs in this library.
21 # Almost of all i18n/l10n programs use this APIs only.
22 module Locale
23 @@default_tag = nil
24 @@driver_name = nil
25
26 module_function
27 def require_driver(name) #:nodoc:
28 require "locale/driver/#{name}"
29 @@driver_name = name.to_sym
30 end
31
32 def create_language_tag(tag) #:nodoc:
33 case tag
34 when nil
35 when Locale::Tag::Simple
36 tag
37 when Locale::TagList
38 tag[0]
39 else
40 Locale::Tag.parse(tag)
41 end
42 end
43
44 # Initialize Locale library.
45 # Usually, you don't need to call this directly, because
46 # this is called when Locale's methods are called.
47 #
48 # If you use this library with CGI or the kind of CGI.
49 # You need to call Locale.init(:driver => :cgi).
50 #
51 # ==== For Framework designers/programers:
52 # If your framework is for WWW, call this once like: Locale.init(:driver => :cgi).
53 #
54 # ==== To Application programers:
55 # If your framework doesn't use ruby-locale and the application is for WWW,
56 # call this once like: Locale.init(:driver => :cgi).
57 #
58 # ==== To Library authors:
59 # Don't call this, even if your application is only for WWW.
60 #
61 # * opts: Options as a Hash.
62 # * :driver - The driver. :cgi if you use Locale module with CGI,
63 # nil if you use system locale.
64 # (ex) Locale.init(:driver => :cgi)
65 #
66 def init(opts = {})
67 if opts[:driver]
68 require_driver opts[:driver]
69 else
70 if /cygwin|mingw|win32/ =~ RUBY_PLATFORM
71 require_driver 'win32'
72 elsif /java/ =~ RUBY_PLATFORM
73 require_driver 'jruby'
74 else
75 require_driver 'posix'
76 end
77 end
78 end
79
80 # Gets the driver module.
81 #
82 # Usually you don't need to call this method.
83 #
84 # * Returns: the driver module.
85 def driver_module
86 Locale.init if @@driver_name.nil?
87 Driver::MODULES[@@driver_name]
88 end
89
90 DEFAULT_LANGUAGE_TAG = Locale::Tag::Simple.new("en") #:nodoc:
91
92 # Sets the default locale as the language tag
93 # (Locale::Tag's class or String(such as "ja_JP")).
94 #
95 # * tag: the default language_tag
96 # * Returns: self.
97 def set_default(tag)
98 Thread.list.each do |thread|
99 thread[:current_languages] = nil
100 thread[:candidates_caches] = nil
101 end
102 @@default_tag = create_language_tag(tag)
103 self
104 end
105
106 # Same as Locale.set_default.
107 #
108 # * locale: the default locale (Locale::Tag's class) or a String such as "ja-JP".
109 # * Returns: locale.
110 def default=(tag)
111 set_default(tag)
112 @@default_tag
113 end
114
115 # Gets the default locale(language tag).
116 #
117 # If the default language tag is not set, this returns nil.
118 #
119 # * Returns: the default locale (Locale::Tag's class).
120 def default
121 @@default_tag || DEFAULT_LANGUAGE_TAG
122 end
123
124 # Sets the locales of the current thread order by the priority.
125 # Each thread has a current locales.
126 # The system locale/default locale is used if the thread doesn't have current locales.
127 #
128 # * tag: Locale::Language::Tag's class or the language tag as a String. nil if you need to
129 # clear current locales.
130 # * charset: the charset (override the charset even if the locale name has charset) or nil.
131 # * Returns: self
132 #
133 # (e.g.)
134 # Locale.set_current("ja_JP.eucJP")
135 # Locale.set_current("ja-JP")
136 # Locale.set_current("en_AU", "en_US", ...)
137 # Locale.set_current(Locale::Tag::Simple.new("ja", "JP"), ...)
138 def set_current(*tags)
139 languages = nil
140 if tags[0]
141 languages = Locale::TagList.new
142 tags.each do |tag|
143 case tag
144 when Locale::TagList
145 languages.concat(tag)
146 else
147 languages << create_language_tag(tag)
148 end
149 end
150 end
151 Thread.current[:current_languages] = languages
152 Thread.current[:candidates_caches] = nil
153 self
154 end
155
156 # Sets a current locale. This is a single argument version of Locale.set_current.
157 #
158 # * tag: the language tag such as "ja-JP"
159 # * Returns: an Array of the current locale (Locale::Tag's class).
160 #
161 # Locale.current = "ja-JP"
162 # Locale.current = "ja_JP.eucJP"
163 def current=(tag)
164 set_current(tag)
165 Thread.current[:current_languages]
166 end
167
168 # Gets the current locales (Locale::Tag's class).
169 # If the current locale is not set, this returns system/default locale.
170 #
171 # This method returns the current language tags even if it isn't included in app_language_tags.
172 #
173 # Usually, the programs should use Locale.candidates to find the correct locale, not this method.
174 #
175 # * Returns: an Array of the current locales (Locale::Tag's class).
176 def current
177 unless Thread.current[:current_languages]
178 loc = driver_module.locales
179 Thread.current[:current_languages] = loc ? loc : Locale::TagList.new([default])
180 end
181 Thread.current[:current_languages]
182 end
183
184 # Deprecated.
185 def get #:nodoc:
186 current
187 end
188
189 # Deprecated.
190 def set(tag) #:nodoc:
191 set_current(tag)
192 end
193
194 # Returns the language tags which are variations of the current locales order by priority.
195 #
196 # For example, if the current locales are ["fr", "ja_JP", "en_US", "en-Latn-GB-VARIANT"],
197 # then returns ["fr", "ja_JP", "en_US", "en-Latn-GB-VARIANT", "en_Latn_GB", "en_GB", "ja", "en"].
198 # "en" is the default locale(You can change it using set_default).
199 # The default locale is added at the end of the list even if it isn't exist.
200 #
201 # Usually, this method is used to find the locale data as the path(or a kind of IDs).
202 # * options: options as a Hash or nil.
203 # * :supported_language_tags -
204 # An Array of the language tags order by the priority. This option
205 # restricts the locales which are supported by the library/application.
206 # Default is nil if you don't need to restrict the locales.
207 # (e.g.1) ["fr_FR", "en_GB", "en_US", ...]
208 # * :type -
209 # The type of language tag. :common, :rfc, :cldr, :posix and
210 # :simple are available. Default value is :common
211 def candidates(options = {})
212 opts = {
213 :supported_language_tags => nil,
214 :current => current,
215 :type => :common,
216 }.merge(options)
217
218 Thread.current[:candidates_caches] ||= {}
219 Thread.current[:candidates_caches][opts] ||=
220 collect_candidates(opts[:type], opts[:current],
221 opts[:supported_language_tags])
222 end
223
224 # collect tag candidates.
225 # The result is shared from all threads.
226 def collect_candidates(type, tags, supported_tags) # :nodoc:
227 candidate_tags = tags.collect{|v| v.send("to_#{type}").candidates}
228 default_tags = default.send("to_#{type}").candidates
229 if app_language_tags
230 app_tags = app_language_tags.collect{|v| v.send("to_#{type}")}.flatten.uniq
231 end
232 if supported_tags
233 supported_tags = supported_tags.collect{|v| Locale::Tag.parse(v).send("to_#{type}")}.flatten.uniq
234 end
235
236 tags = []
237 unless candidate_tags.empty?
238 (0...candidate_tags[0].size).each {|i|
239 tags += candidate_tags.collect{|v| v[i]}
240 }
241 end
242 tags += default_tags
243 tags.uniq!
244
245 all_tags = nil
246 if app_tags
247 if supported_tags
248 all_tags = app_tags & supported_tags
249 else
250 all_tags = app_tags
251 end
252 elsif supported_tags
253 all_tags = supported_tags
254 end
255 if all_tags
256 tags &= all_tags
257 tags = default_tags.uniq if tags.size == 0
258 end
259
260 Locale::TagList.new(tags)
261 end
262
263 # Gets the current charset.
264 #
265 # This returns the current user/system charset. This value is
266 # read only, so you can't set it by yourself.
267 #
268 # * Returns: the current charset.
269 def charset
270 driver_module.charset || "UTF-8"
271 end
272
273 # Clear current locale.
274 # * Returns: self
275 def clear
276 Thread.current[:current_languages] = nil
277 Thread.current[:candidates_caches] = nil
278 self
279 end
280
281 # Clear all locales and charsets of all threads.
282 # This doesn't clear the default and app_language_tags.
283 # Use Locale.default = nil to unset the default locale.
284 # * Returns: self
285 def clear_all
286 Thread.list.each do |thread|
287 thread[:current_languages] = nil
288 thread[:candidates_caches] = nil
289 end
290 self
291 end
292
293 @@app_language_tags = nil
294 # Set the language tags which is supported by the Application.
295 # This value is same with supported_language_tags in Locale.candidates
296 # to restrict the result but is the global setting.
297 # If you set a language tag, the application works as the single locale
298 # application.
299 #
300 # If the current locale is not included in app_language_tags,
301 # Locale.default value is used.
302 # Use Locale.set_default() to set correct language
303 # if "en" is not included in the language tags.
304 #
305 # Set nil if clear the value.
306 #
307 # Note that the libraries/plugins shouldn't set this value.
308 #
309 # (e.g.) Locale.set_app_language_tags("fr_FR", "en-GB", "en_US", ...)
310 def set_app_language_tags(*tags)
311 if tags[0]
312 @@app_language_tags = tags.collect{|v| Locale::Tag.parse(v)}
313 else
314 @@app_language_tags = nil
315 end
316
317 clear_all
318 self
319 end
320
321 # Returns the app_language_tags. Default is nil. See set_app_language_tags for more details.
322 def app_language_tags
323 @@app_language_tags
324 end
325
326 end
0 # frozen_string_literal: true
1
2 module Kernel
3 # Returns the object's singleton class.
4 unless respond_to?(:singleton_class)
5 def singleton_class
6 class << self
7 self
8 end
9 end
10 end # exists in 1.9.2
11 end
0 # frozen_string_literal: true
1
2 module Memoist
3 VERSION = '0.16.0'.freeze
4 end
0 # frozen_string_literal: true
1
2 require 'memoist/version'
3 require 'memoist/core_ext/singleton_class'
4
5 module Memoist
6 def self.extended(extender)
7 Memoist.memoist_eval(extender) do
8 unless singleton_class.method_defined?(:memoized_methods)
9 def self.memoized_methods
10 @_memoized_methods ||= []
11 end
12 end
13 end
14 end
15
16 def self.memoized_ivar_for(method_name, identifier = nil)
17 "@#{memoized_prefix(identifier)}_#{escape_punctuation(method_name)}"
18 end
19
20 def self.unmemoized_method_for(method_name, identifier = nil)
21 "#{unmemoized_prefix(identifier)}_#{method_name}".to_sym
22 end
23
24 def self.memoized_prefix(identifier = nil)
25 if identifier
26 "_memoized_#{identifier}"
27 else
28 '_memoized'.freeze
29 end
30 end
31
32 def self.unmemoized_prefix(identifier = nil)
33 if identifier
34 "_unmemoized_#{identifier}"
35 else
36 '_unmemoized'.freeze
37 end
38 end
39
40 def self.escape_punctuation(string)
41 string = string.is_a?(String) ? string.dup : string.to_s
42
43 return string unless string.end_with?('?'.freeze, '!'.freeze)
44
45 # A String can't end in both ? and !
46 if string.sub!(/\?\Z/, '_query'.freeze)
47 else
48 string.sub!(/!\Z/, '_bang'.freeze)
49 end
50 string
51 end
52
53 def self.memoist_eval(klass, *args, &block)
54 if klass.respond_to?(:class_eval)
55 klass.class_eval(*args, &block)
56 else
57 klass.singleton_class.class_eval(*args, &block)
58 end
59 end
60
61 def self.extract_reload!(method, args)
62 if args.length == method.arity.abs + 1 && (args.last == true || args.last == :reload)
63 reload = args.pop
64 end
65 reload
66 end
67
68 module InstanceMethods
69 def memoize_all
70 prime_cache
71 end
72
73 def unmemoize_all
74 flush_cache
75 end
76
77 def memoized_structs(names)
78 ref_obj = self.class.respond_to?(:class_eval) ? singleton_class : self
79 structs = ref_obj.all_memoized_structs
80 return structs if names.empty?
81
82 structs.select { |s| names.include?(s.memoized_method) }
83 end
84
85 def prime_cache(*method_names)
86 memoized_structs(method_names).each do |struct|
87 if struct.arity == 0
88 __send__(struct.memoized_method)
89 else
90 instance_variable_set(struct.ivar, {})
91 end
92 end
93 end
94
95 def flush_cache(*method_names)
96 memoized_structs(method_names).each do |struct|
97 remove_instance_variable(struct.ivar) if instance_variable_defined?(struct.ivar)
98 end
99 end
100 end
101
102 MemoizedMethod = Struct.new(:memoized_method, :ivar, :arity)
103
104 def all_memoized_structs
105 @all_memoized_structs ||= begin
106 structs = memoized_methods.dup
107
108 # Collect the memoized_methods of ancestors in ancestor order
109 # unless we already have it since self or parents could be overriding
110 # an ancestor method.
111 ancestors.grep(Memoist).each do |ancestor|
112 ancestor.memoized_methods.each do |m|
113 structs << m unless structs.any? { |am| am.memoized_method == m.memoized_method }
114 end
115 end
116 structs
117 end
118 end
119
120 def clear_structs
121 @all_memoized_structs = nil
122 end
123
124 def memoize(*method_names)
125 identifier = method_names.pop[:identifier] if method_names.last.is_a?(Hash)
126
127 method_names.each do |method_name|
128 unmemoized_method = Memoist.unmemoized_method_for(method_name, identifier)
129 memoized_ivar = Memoist.memoized_ivar_for(method_name, identifier)
130
131 Memoist.memoist_eval(self) do
132 include InstanceMethods
133
134 if method_defined?(unmemoized_method)
135 warn "Already memoized #{method_name}"
136 return
137 end
138 alias_method unmemoized_method, method_name
139
140 mm = MemoizedMethod.new(method_name, memoized_ivar, instance_method(method_name).arity)
141 memoized_methods << mm
142 if mm.arity == 0
143
144 # define a method like this;
145
146 # def mime_type(reload=true)
147 # skip_cache = reload || !instance_variable_defined?("@_memoized_mime_type")
148 # set_cache = skip_cache && !frozen?
149 #
150 # if skip_cache
151 # value = _unmemoized_mime_type
152 # else
153 # value = @_memoized_mime_type
154 # end
155 #
156 # if set_cache
157 # @_memoized_mime_type = value
158 # end
159 #
160 # value
161 # end
162
163 module_eval <<-EOS, __FILE__, __LINE__ + 1
164 def #{method_name}(reload = false)
165 skip_cache = reload || !instance_variable_defined?("#{memoized_ivar}")
166 set_cache = skip_cache && !frozen?
167
168 if skip_cache
169 value = #{unmemoized_method}
170 else
171 value = #{memoized_ivar}
172 end
173
174 if set_cache
175 #{memoized_ivar} = value
176 end
177
178 value
179 end
180 EOS
181 else
182
183 # define a method like this;
184
185 # def mime_type(*args)
186 # reload = Memoist.extract_reload!(method(:_unmemoized_mime_type), args)
187 #
188 # skip_cache = reload || !memoized_with_args?(:mime_type, args)
189 # set_cache = skip_cache && !frozen
190 #
191 # if skip_cache
192 # value = _unmemoized_mime_type(*args)
193 # else
194 # value = @_memoized_mime_type[args]
195 # end
196 #
197 # if set_cache
198 # @_memoized_mime_type ||= {}
199 # @_memoized_mime_type[args] = value
200 # end
201 #
202 # value
203 # end
204
205 module_eval <<-EOS, __FILE__, __LINE__ + 1
206 def #{method_name}(*args)
207 reload = Memoist.extract_reload!(method(#{unmemoized_method.inspect}), args)
208
209 skip_cache = reload || !(instance_variable_defined?(#{memoized_ivar.inspect}) && #{memoized_ivar} && #{memoized_ivar}.has_key?(args))
210 set_cache = skip_cache && !frozen?
211
212 if skip_cache
213 value = #{unmemoized_method}(*args)
214 else
215 value = #{memoized_ivar}[args]
216 end
217
218 if set_cache
219 #{memoized_ivar} ||= {}
220 #{memoized_ivar}[args] = value
221 end
222
223 value
224 end
225 EOS
226 end
227
228 if private_method_defined?(unmemoized_method)
229 private method_name
230 elsif protected_method_defined?(unmemoized_method)
231 protected method_name
232 end
233 end
234 end
235 # return a chainable method_name symbol if we can
236 method_names.length == 1 ? method_names.first : method_names
237 end
238 end
0 class OAuth::CLI
1 class AuthorizeCommand < BaseCommand
2
3 def required_options
4 [:uri]
5 end
6
7 def _run
8 request_token = get_request_token
9
10 if request_token.callback_confirmed?
11 puts "Server appears to support OAuth 1.0a; enabling support."
12 options[:version] = "1.0a"
13 end
14
15 puts "Please visit this url to authorize:"
16 puts request_token.authorize_url
17
18 # parameters for OAuth 1.0a
19 oauth_verifier = ask_user_for_verifier
20
21 verbosely_get_access_token(request_token, oauth_verifier)
22 end
23
24 def get_request_token
25 consumer = get_consumer
26 scope_options = options[:scope] ? { "scope" => options[:scope] } : {}
27 consumer.get_request_token({ :oauth_callback => options[:oauth_callback] }, scope_options)
28 rescue OAuth::Unauthorized => e
29 alert "A problem occurred while attempting to authorize:"
30 alert e
31 alert e.request.body
32 end
33
34 def get_consumer
35 OAuth::Consumer.new \
36 options[:oauth_consumer_key],
37 options[:oauth_consumer_secret],
38 :access_token_url => options[:access_token_url],
39 :authorize_url => options[:authorize_url],
40 :request_token_url => options[:request_token_url],
41 :scheme => options[:scheme],
42 :http_method => options[:method].to_s.downcase.to_sym
43 end
44
45
46 def ask_user_for_verifier
47 if options[:version] == "1.0a"
48 puts "Please enter the verification code provided by the SP (oauth_verifier):"
49 @stdin.gets.chomp
50 else
51 puts "Press return to continue..."
52 @stdin.gets
53 nil
54 end
55 end
56
57 def verbosely_get_access_token(request_token, oauth_verifier)
58 access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
59
60 puts "Response:"
61 access_token.params.each do |k,v|
62 puts " #{k}: #{v}" unless k.is_a?(Symbol)
63 end
64 rescue OAuth::Unauthorized => e
65 alert "A problem occurred while attempting to obtain an access token:"
66 alert e
67 alert e.request.body
68 end
69 end
70 end
0 class OAuth::CLI
1 class BaseCommand
2 def initialize(stdout, stdin, stderr, arguments)
3 @stdout, @stdin, @stderr = stdout, stdin, stderr
4
5 @options = {}
6 option_parser.parse!(arguments)
7 end
8
9 def run
10 missing = required_options - options.keys
11 if missing.empty?
12 _run
13 else
14 show_missing(missing)
15 puts option_parser.help
16 end
17 end
18
19 def required_options
20 []
21 end
22
23 protected
24
25 attr_reader :options
26
27 def show_missing(array)
28 array = array.map { |s| "--#{s}" }.join(' ')
29 OAuth::CLI.puts_red "Options missing to OAuth CLI: #{array}"
30 end
31
32 def xmpp?
33 options[:xmpp]
34 end
35
36 def verbose?
37 options[:verbose]
38 end
39
40 def puts(string=nil)
41 @stdout.puts(string)
42 end
43
44 def alert(string=nil)
45 @stderr.puts(string)
46 end
47
48 def parameters
49 @parameters ||= begin
50 escaped_pairs = options[:params].collect do |pair|
51 if pair =~ /:/
52 Hash[*pair.split(":", 2)].collect do |k,v|
53 [CGI.escape(k.strip), CGI.escape(v.strip)] * "="
54 end
55 else
56 pair
57 end
58 end
59
60 querystring = escaped_pairs * "&"
61 cli_params = CGI.parse(querystring)
62
63 {
64 "oauth_consumer_key" => options[:oauth_consumer_key],
65 "oauth_nonce" => options[:oauth_nonce],
66 "oauth_timestamp" => options[:oauth_timestamp],
67 "oauth_token" => options[:oauth_token],
68 "oauth_signature_method" => options[:oauth_signature_method],
69 "oauth_version" => options[:oauth_version]
70 }.reject { |_k,v| v.nil? || v == "" }.merge(cli_params)
71 end
72 end
73
74 def option_parser
75 @option_parser ||= OptionParser.new do |opts|
76 opts.banner = "Usage: oauth <command> [ARGS]"
77
78 _option_parser_defaults
79 _option_parser_common(opts)
80 _option_parser_sign_and_query(opts)
81 _option_parser_authorization(opts)
82 end
83 end
84
85 def _option_parser_defaults
86 options[:oauth_nonce] = OAuth::Helper.generate_key
87 options[:oauth_signature_method] = "HMAC-SHA1"
88 options[:oauth_timestamp] = OAuth::Helper.generate_timestamp
89 options[:oauth_version] = "1.0"
90 options[:method] = :post
91 options[:params] = []
92 options[:scheme] = :header
93 options[:version] = "1.0"
94 end
95
96 def _option_parser_common(opts)
97 ## Common Options
98
99 opts.on("-B", "--body", "Use the request body for OAuth parameters.") do
100 options[:scheme] = :body
101 end
102
103 opts.on("--consumer-key KEY", "Specifies the consumer key to use.") do |v|
104 options[:oauth_consumer_key] = v
105 end
106
107 opts.on("--consumer-secret SECRET", "Specifies the consumer secret to use.") do |v|
108 options[:oauth_consumer_secret] = v
109 end
110
111 opts.on("-H", "--header", "Use the 'Authorization' header for OAuth parameters (default).") do
112 options[:scheme] = :header
113 end
114
115 opts.on("-Q", "--query-string", "Use the query string for OAuth parameters.") do
116 options[:scheme] = :query_string
117 end
118
119 opts.on("-O", "--options FILE", "Read options from a file") do |v|
120 arguments = open(v).readlines.map { |l| l.chomp.split(" ") }.flatten
121 options2 = parse_options(arguments)
122 options.merge!(options2)
123 end
124 end
125
126 def _option_parser_sign_and_query(opts)
127 opts.separator("\n options for signing and querying")
128
129 opts.on("--method METHOD", "Specifies the method (e.g. GET) to use when signing.") do |v|
130 options[:method] = v
131 end
132
133 opts.on("--nonce NONCE", "Specifies the nonce to use.") do |v|
134 options[:oauth_nonce] = v
135 end
136
137 opts.on("--parameters PARAMS", "Specifies the parameters to use when signing.") do |v|
138 options[:params] << v
139 end
140
141 opts.on("--signature-method METHOD", "Specifies the signature method to use; defaults to HMAC-SHA1.") do |v|
142 options[:oauth_signature_method] = v
143 end
144
145 opts.on("--token TOKEN", "Specifies the token to use.") do |v|
146 options[:oauth_token] = v
147 end
148
149 opts.on("--secret SECRET", "Specifies the token secret to use.") do |v|
150 options[:oauth_token_secret] = v
151 end
152
153 opts.on("--timestamp TIMESTAMP", "Specifies the timestamp to use.") do |v|
154 options[:oauth_timestamp] = v
155 end
156
157 opts.on("--realm REALM", "Specifies the realm to use.") do |v|
158 options[:realm] = v
159 end
160
161 opts.on("--uri URI", "Specifies the URI to use when signing.") do |v|
162 options[:uri] = v
163 end
164
165 opts.on("--version [VERSION]", "Specifies the OAuth version to use.") do |v|
166 options[:oauth_version] = v
167 end
168
169 opts.on("--no-version", "Omit oauth_version.") do
170 options[:oauth_version] = nil
171 end
172
173 opts.on("--xmpp", "Generate XMPP stanzas.") do
174 options[:xmpp] = true
175 options[:method] ||= "iq"
176 end
177
178 opts.on("-v", "--verbose", "Be verbose.") do
179 options[:verbose] = true
180 end
181 end
182
183 def _option_parser_authorization(opts)
184 opts.separator("\n options for authorization")
185
186 opts.on("--access-token-url URL", "Specifies the access token URL.") do |v|
187 options[:access_token_url] = v
188 end
189
190 opts.on("--authorize-url URL", "Specifies the authorization URL.") do |v|
191 options[:authorize_url] = v
192 end
193
194 opts.on("--callback-url URL", "Specifies a callback URL.") do |v|
195 options[:oauth_callback] = v
196 end
197
198 opts.on("--request-token-url URL", "Specifies the request token URL.") do |v|
199 options[:request_token_url] = v
200 end
201
202 opts.on("--scope SCOPE", "Specifies the scope (Google-specific).") do |v|
203 options[:scope] = v
204 end
205 end
206 end
207 end
0 class OAuth::CLI
1 class HelpCommand < BaseCommand
2 def run
3 puts <<-EOT
4 Usage: oauth COMMAND [ARGS]
5
6 Available oauth commands are:
7 a, authorize Obtain an access token and secret for a user
8 q, query Query a protected resource
9 s, sign Generate an OAuth signature
10
11 In addition to those, there are:
12 v, version Displays the current version of the library (or --version, -v)
13 h, help Displays this help (or --help, -h)
14
15 Tip: All commands can be run without args for specific help.
16
17
18 EOT
19 end
20 end
21 end
0 class OAuth::CLI
1 class QueryCommand < BaseCommand
2 extend OAuth::Helper
3
4 def required_options
5 [:oauth_consumer_key, :oauth_consumer_secret, :oauth_token, :oauth_token_secret]
6 end
7
8 def _run
9 consumer = OAuth::Consumer.new(options[:oauth_consumer_key], options[:oauth_consumer_secret], scheme: options[:scheme])
10
11 access_token = OAuth::AccessToken.new(consumer, options[:oauth_token], options[:oauth_token_secret])
12
13 # append params to the URL
14 uri = URI.parse(options[:uri])
15 params = parameters.map { |k,v| Array(v).map { |v2| "#{OAuth::Helper.escape(k)}=#{OAuth::Helper.escape(v2)}" } * "&" }
16 uri.query = [uri.query, *params].reject { |x| x.nil? } * "&"
17 puts uri.to_s
18
19 response = access_token.request(options[:method].to_s.downcase.to_sym, uri.to_s)
20 puts "#{response.code} #{response.message}"
21 puts response.body
22 end
23 end
24 end
0 class OAuth::CLI
1 class SignCommand < BaseCommand
2
3 def required_options
4 [:oauth_consumer_key, :oauth_consumer_secret, :oauth_token, :oauth_token_secret]
5 end
6
7 def _run
8 request = OAuth::RequestProxy.proxy \
9 "method" => options[:method],
10 "uri" => options[:uri],
11 "parameters" => parameters
12
13 if verbose?
14 puts_verbose_parameters(request)
15 end
16
17 request.sign! \
18 :consumer_secret => options[:oauth_consumer_secret],
19 :token_secret => options[:oauth_token_secret]
20
21 if verbose?
22 puts_verbose_request(request)
23 else
24 puts request.oauth_signature
25 end
26 end
27
28 def puts_verbose_parameters(request)
29 puts "OAuth parameters:"
30 request.oauth_parameters.each do |k,v|
31 puts " " + [k, v] * ": "
32 end
33 puts
34
35 if request.non_oauth_parameters.any?
36 puts "Parameters:"
37 request.non_oauth_parameters.each do |k,v|
38 puts " " + [k, v] * ": "
39 end
40 puts
41 end
42 end
43
44 def puts_verbose_request(request)
45 puts "Method: #{request.method}"
46 puts "URI: #{request.uri}"
47 puts "Normalized params: #{request.normalized_parameters}" unless options[:xmpp]
48 puts "Signature base string: #{request.signature_base_string}"
49
50 if xmpp?
51 puts
52 puts "XMPP Stanza:"
53 puts xmpp_output(request)
54 puts
55 puts "Note: You may want to use bare JIDs in your URI."
56 puts
57 else
58 puts "OAuth Request URI: #{request.signed_uri}"
59 puts "Request URI: #{request.signed_uri(false)}"
60 puts "Authorization header: #{request.oauth_header(:realm => options[:realm])}"
61 end
62 puts "Signature: #{request.oauth_signature}"
63 puts "Escaped signature: #{OAuth::Helper.escape(request.oauth_signature)}"
64 end
65
66 def xmpp_output(request)
67 <<-EOS
68 <oauth xmlns='urn:xmpp:oauth:0'>
69 <oauth_consumer_key>#{request.oauth_consumer_key}</oauth_consumer_key>
70 <oauth_token>#{request.oauth_token}</oauth_token>
71 <oauth_signature_method>#{request.oauth_signature_method}</oauth_signature_method>
72 <oauth_signature>#{request.oauth_signature}</oauth_signature>
73 <oauth_timestamp>#{request.oauth_timestamp}</oauth_timestamp>
74 <oauth_nonce>#{request.oauth_nonce}</oauth_nonce>
75 <oauth_version>#{request.oauth_version}</oauth_version>
76 </oauth>
77 EOS
78 end
79 end
80 end
0 class OAuth::CLI
1 class VersionCommand < BaseCommand
2 def run
3 puts "OAuth Gem #{OAuth::VERSION}"
4 end
5 end
6 end
0 require 'optparse'
1 require 'oauth/cli/base_command'
2 require 'oauth/cli/help_command'
3 require 'oauth/cli/query_command'
4 require 'oauth/cli/authorize_command'
5 require 'oauth/cli/sign_command'
6 require 'oauth/cli/version_command'
7 require 'active_support/core_ext/string/inflections'
8
9 module OAuth
10 class CLI
11 def self.puts_red(string)
12 puts "\033[0;91m#{string}\033[0m"
13 end
14
15 ALIASES = {
16 'h' => 'help',
17 'v' => 'version',
18 'q' => 'query',
19 'a' => 'authorize',
20 's' => 'sign',
21 }
22
23 def initialize(stdout, stdin, stderr, command, arguments)
24 klass = get_command_class(parse_command(command))
25 @command = klass.new(stdout, stdin, stderr, arguments)
26 @help_command = HelpCommand.new(stdout, stdin, stderr, [])
27 end
28
29 def run
30 @command.run
31 end
32
33 private
34
35 def get_command_class(command)
36 Object.const_get("OAuth::CLI::#{command.camelize}Command")
37 end
38
39 def parse_command(command)
40 case command = command.to_s.downcase
41 when '--version', '-v'
42 'version'
43 when '--help', '-h', nil, ''
44 'help'
45 when *ALIASES.keys
46 ALIASES[command]
47 when *ALIASES.values
48 command
49 else
50 OAuth::CLI.puts_red "Command '#{command}' not found"
51 'help'
52 end
53 end
54 end
55 end
0 if defined? ActionDispatch
1 require 'oauth/request_proxy/rack_request'
2 require 'oauth/request_proxy/action_dispatch_request'
3 require 'action_dispatch/testing/test_process'
4 else
5 require 'oauth/request_proxy/action_controller_request'
6 require 'action_controller/test_process'
7 end
8
9 module ActionController
10 class Base
11 if defined? ActionDispatch
12 def process_with_new_base_test(request, response=nil)
13 request.apply_oauth! if request.respond_to?(:apply_oauth!)
14 super(request, response)
15 end
16 else
17 def process_with_oauth(request, response=nil)
18 request.apply_oauth! if request.respond_to?(:apply_oauth!)
19 process_without_oauth(request, response)
20 end
21 alias_method_chain :process, :oauth
22 end
23 end
24
25 class TestRequest
26 def self.use_oauth=(bool)
27 @use_oauth = bool
28 end
29
30 def self.use_oauth?
31 @use_oauth
32 end
33
34 def configure_oauth(consumer = nil, token = nil, options = {})
35 @oauth_options = { :consumer => consumer,
36 :token => token,
37 :scheme => 'header',
38 :signature_method => nil,
39 :nonce => nil,
40 :timestamp => nil }.merge(options)
41 end
42
43 def apply_oauth!
44 return unless ActionController::TestRequest.use_oauth? && @oauth_options
45
46 @oauth_helper = OAuth::Client::Helper.new(self, @oauth_options.merge(:request_uri => (respond_to?(:fullpath) ? fullpath : request_uri)))
47 @oauth_helper.amend_user_agent_header(env)
48
49 self.send("set_oauth_#{@oauth_options[:scheme]}")
50 end
51
52 def set_oauth_header
53 env['Authorization'] = @oauth_helper.header
54 end
55
56 def set_oauth_parameters
57 @query_parameters = @oauth_helper.parameters_with_oauth
58 @query_parameters.merge!(:oauth_signature => @oauth_helper.signature)
59 end
60
61 def set_oauth_query_string
62 end
63 end
64 end
0 require 'em-http'
1 require 'oauth/helper'
2 require 'oauth/request_proxy/em_http_request'
3
4 # Extensions for em-http so that we can use consumer.sign! with an EventMachine::HttpClient
5 # instance. This is purely syntactic sugar.
6 class EventMachine::HttpClient
7
8 attr_reader :oauth_helper
9
10 # Add the OAuth information to an HTTP request. Depending on the <tt>options[:scheme]</tt> setting
11 # this may add a header, additional query string parameters, or additional POST body parameters.
12 # The default scheme is +header+, in which the OAuth parameters as put into the +Authorization+
13 # header.
14 #
15 # * http - Configured Net::HTTP instance, ignored in this scenario except for getting host.
16 # * consumer - OAuth::Consumer instance
17 # * token - OAuth::Token instance
18 # * options - Request-specific options (e.g. +request_uri+, +consumer+, +token+, +scheme+,
19 # +signature_method+, +nonce+, +timestamp+)
20 #
21 # This method also modifies the <tt>User-Agent</tt> header to add the OAuth gem version.
22 #
23 # See Also: {OAuth core spec version 1.0, section 5.4.1}[http://oauth.net/core/1.0#rfc.section.5.4.1]
24 def oauth!(http, consumer = nil, token = nil, options = {})
25 options = { :request_uri => normalized_oauth_uri(http),
26 :consumer => consumer,
27 :token => token,
28 :scheme => 'header',
29 :signature_method => nil,
30 :nonce => nil,
31 :timestamp => nil }.merge(options)
32
33 @oauth_helper = OAuth::Client::Helper.new(self, options)
34 self.__send__(:"set_oauth_#{options[:scheme]}")
35 end
36
37 # Create a string suitable for signing for an HTTP request. This process involves parameter
38 # normalization as specified in the OAuth specification. The exact normalization also depends
39 # on the <tt>options[:scheme]</tt> being used so this must match what will be used for the request
40 # itself. The default scheme is +header+, in which the OAuth parameters as put into the +Authorization+
41 # header.
42 #
43 # * http - Configured Net::HTTP instance
44 # * consumer - OAuth::Consumer instance
45 # * token - OAuth::Token instance
46 # * options - Request-specific options (e.g. +request_uri+, +consumer+, +token+, +scheme+,
47 # +signature_method+, +nonce+, +timestamp+)
48 #
49 # See Also: {OAuth core spec version 1.0, section 9.1.1}[http://oauth.net/core/1.0#rfc.section.9.1.1]
50 def signature_base_string(http, consumer = nil, token = nil, options = {})
51 options = { :request_uri => normalized_oauth_uri(http),
52 :consumer => consumer,
53 :token => token,
54 :scheme => 'header',
55 :signature_method => nil,
56 :nonce => nil,
57 :timestamp => nil }.merge(options)
58
59 OAuth::Client::Helper.new(self, options).signature_base_string
60 end
61
62 # This code was lifted from the em-http-request because it was removed from
63 # the gem June 19, 2010
64 # see: http://github.com/igrigorik/em-http-request/commit/d536fc17d56dbe55c487eab01e2ff9382a62598b
65 def normalize_uri
66 @normalized_uri ||= begin
67 uri = @uri.dup
68 encoded_query = encode_query(@uri, @options[:query])
69 path, query = encoded_query.split("?", 2)
70 uri.query = query unless encoded_query.empty?
71 uri.path = path
72 uri
73 end
74 end
75
76 protected
77
78 def combine_query(path, query, uri_query)
79 combined_query = if query.kind_of?(Hash)
80 query.map { |k, v| encode_param(k, v) }.join('&')
81 else
82 query.to_s
83 end
84 if !uri_query.to_s.empty?
85 combined_query = [combined_query, uri_query].reject {|part| part.empty?}.join("&")
86 end
87 combined_query.to_s.empty? ? path : "#{path}?#{combined_query}"
88 end
89
90 # Since we expect to get the host etc details from the http instance (...),
91 # we create a fake url here. Surely this is a horrible, horrible idea?
92 def normalized_oauth_uri(http)
93 uri = URI.parse(normalize_uri.path)
94 uri.host = http.address
95 uri.port = http.port
96
97 if http.respond_to?(:use_ssl?) && http.use_ssl?
98 uri.scheme = "https"
99 else
100 uri.scheme = "http"
101 end
102 uri.to_s
103 end
104
105 def set_oauth_header
106 headers = (self.options[:head] ||= {})
107 headers['Authorization'] = @oauth_helper.header
108 end
109
110 def set_oauth_body
111 raise NotImplementedError, 'please use the set_oauth_header method instead'
112 end
113
114 def set_oauth_query_string
115 raise NotImplementedError, 'please use the set_oauth_header method instead'
116 end
117
118 end
0 require 'oauth/client'
1 require 'oauth/consumer'
2 require 'oauth/helper'
3 require 'oauth/token'
4 require 'oauth/signature/hmac/sha1'
5
6 module OAuth::Client
7 class Helper
8 include OAuth::Helper
9
10 def initialize(request, options = {})
11 @request = request
12 @options = options
13 @options[:signature_method] ||= 'HMAC-SHA1'
14 end
15
16 def options
17 @options
18 end
19
20 def nonce
21 options[:nonce] ||= generate_key
22 end
23
24 def timestamp
25 options[:timestamp] ||= generate_timestamp
26 end
27
28 def oauth_parameters
29 {
30 'oauth_body_hash' => options[:body_hash],
31 'oauth_callback' => options[:oauth_callback],
32 'oauth_consumer_key' => options[:consumer].key,
33 'oauth_token' => options[:token] ? options[:token].token : '',
34 'oauth_signature_method' => options[:signature_method],
35 'oauth_timestamp' => timestamp,
36 'oauth_nonce' => nonce,
37 'oauth_verifier' => options[:oauth_verifier],
38 'oauth_version' => (options[:oauth_version] || '1.0'),
39 'oauth_session_handle' => options[:oauth_session_handle]
40 }.reject { |k,v| v.to_s == "" }
41 end
42
43 def signature(extra_options = {})
44 OAuth::Signature.sign(@request, { :uri => options[:request_uri],
45 :consumer => options[:consumer],
46 :token => options[:token],
47 :unsigned_parameters => options[:unsigned_parameters]
48 }.merge(extra_options) )
49 end
50
51 def signature_base_string(extra_options = {})
52 OAuth::Signature.signature_base_string(@request, { :uri => options[:request_uri],
53 :consumer => options[:consumer],
54 :token => options[:token],
55 :parameters => oauth_parameters}.merge(extra_options) )
56 end
57
58 def token_request?
59 @options[:token_request].eql?(true)
60 end
61
62 def hash_body
63 @options[:body_hash] = OAuth::Signature.body_hash(@request, :parameters => oauth_parameters)
64 end
65
66 def amend_user_agent_header(headers)
67 @oauth_ua_string ||= "OAuth gem v#{OAuth::VERSION}"
68 # Net::HTTP in 1.9 appends Ruby
69 if headers['User-Agent'] && headers['User-Agent'] != 'Ruby'
70 headers['User-Agent'] += " (#{@oauth_ua_string})"
71 else
72 headers['User-Agent'] = @oauth_ua_string
73 end
74 end
75
76 def header
77 parameters = oauth_parameters
78 parameters.merge!('oauth_signature' => signature(options.merge(:parameters => parameters)))
79
80 header_params_str = parameters.sort.map { |k,v| "#{k}=\"#{escape(v)}\"" }.join(', ')
81
82 realm = "realm=\"#{options[:realm]}\", " if options[:realm]
83 "OAuth #{realm}#{header_params_str}"
84 end
85
86 def parameters
87 OAuth::RequestProxy.proxy(@request).parameters
88 end
89
90 def parameters_with_oauth
91 oauth_parameters.merge(parameters)
92 end
93 end
94 end
0 require 'oauth/helper'
1 require 'oauth/request_proxy/net_http'
2
3 class Net::HTTPGenericRequest
4 include OAuth::Helper
5
6 attr_reader :oauth_helper
7
8 # Add the OAuth information to an HTTP request. Depending on the <tt>options[:scheme]</tt> setting
9 # this may add a header, additional query string parameters, or additional POST body parameters.
10 # The default scheme is +header+, in which the OAuth parameters as put into the +Authorization+
11 # header.
12 #
13 # * http - Configured Net::HTTP instance
14 # * consumer - OAuth::Consumer instance
15 # * token - OAuth::Token instance
16 # * options - Request-specific options (e.g. +request_uri+, +consumer+, +token+, +scheme+,
17 # +signature_method+, +nonce+, +timestamp+)
18 #
19 # This method also modifies the <tt>User-Agent</tt> header to add the OAuth gem version.
20 #
21 # See Also: {OAuth core spec version 1.0, section 5.4.1}[http://oauth.net/core/1.0#rfc.section.5.4.1],
22 # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html,
23 # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html#when_to_include]
24 def oauth!(http, consumer = nil, token = nil, options = {})
25 helper_options = oauth_helper_options(http, consumer, token, options)
26 @oauth_helper = OAuth::Client::Helper.new(self, helper_options)
27 @oauth_helper.amend_user_agent_header(self)
28 @oauth_helper.hash_body if oauth_body_hash_required?
29 self.send("set_oauth_#{helper_options[:scheme]}")
30 end
31
32 # Create a string suitable for signing for an HTTP request. This process involves parameter
33 # normalization as specified in the OAuth specification. The exact normalization also depends
34 # on the <tt>options[:scheme]</tt> being used so this must match what will be used for the request
35 # itself. The default scheme is +header+, in which the OAuth parameters as put into the +Authorization+
36 # header.
37 #
38 # * http - Configured Net::HTTP instance
39 # * consumer - OAuth::Consumer instance
40 # * token - OAuth::Token instance
41 # * options - Request-specific options (e.g. +request_uri+, +consumer+, +token+, +scheme+,
42 # +signature_method+, +nonce+, +timestamp+)
43 #
44 # See Also: {OAuth core spec version 1.0, section 5.4.1}[http://oauth.net/core/1.0#rfc.section.5.4.1],
45 # {OAuth Request Body Hash 1.0 Draft 4}[http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html,
46 # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html#when_to_include]
47 def signature_base_string(http, consumer = nil, token = nil, options = {})
48 helper_options = oauth_helper_options(http, consumer, token, options)
49 @oauth_helper = OAuth::Client::Helper.new(self, helper_options)
50 @oauth_helper.hash_body if oauth_body_hash_required?
51 @oauth_helper.signature_base_string
52 end
53
54 private
55
56 def oauth_helper_options(http, consumer, token, options)
57 { :request_uri => oauth_full_request_uri(http,options),
58 :consumer => consumer,
59 :token => token,
60 :scheme => 'header',
61 :signature_method => nil,
62 :nonce => nil,
63 :timestamp => nil }.merge(options)
64 end
65
66 def oauth_full_request_uri(http,options)
67 uri = URI.parse(self.path)
68 uri.host = http.address
69 uri.port = http.port
70
71 if options[:request_endpoint] && options[:site]
72 is_https = options[:site].match(%r(^https://))
73 uri.host = options[:site].gsub(%r(^https?://), '')
74 uri.port ||= is_https ? 443 : 80
75 end
76
77 if http.respond_to?(:use_ssl?) && http.use_ssl?
78 uri.scheme = "https"
79 else
80 uri.scheme = "http"
81 end
82
83 uri.to_s
84 end
85
86 def oauth_body_hash_required?
87 !@oauth_helper.token_request? && request_body_permitted? && !content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
88 end
89
90 def set_oauth_header
91 self['Authorization'] = @oauth_helper.header
92 end
93
94 # FIXME: if you're using a POST body and query string parameters, this method
95 # will move query string parameters into the body unexpectedly. This may
96 # cause problems with non-x-www-form-urlencoded bodies submitted to URLs
97 # containing query string params. If duplicate parameters are present in both
98 # places, all instances should be included when calculating the signature
99 # base string.
100
101 def set_oauth_body
102 self.set_form_data(@oauth_helper.stringify_keys(@oauth_helper.parameters_with_oauth))
103 params_with_sig = @oauth_helper.parameters.merge(:oauth_signature => @oauth_helper.signature)
104 self.set_form_data(@oauth_helper.stringify_keys(params_with_sig))
105 end
106
107 def set_oauth_query_string
108 oauth_params_str = @oauth_helper.oauth_parameters.map { |k,v| [escape(k), escape(v)] * "=" }.join("&")
109 uri = URI.parse(path)
110 if uri.query.to_s == ""
111 uri.query = oauth_params_str
112 else
113 uri.query = uri.query + "&" + oauth_params_str
114 end
115
116 @path = uri.to_s
117
118 @path << "&oauth_signature=#{escape(oauth_helper.signature)}"
119 end
120 end
0 module OAuth
1 module Client
2 end
3 end
0 require 'net/http'
1 require 'net/https'
2 require 'oauth/oauth'
3 require 'oauth/client/net_http'
4 require 'oauth/errors'
5 require 'cgi'
6
7 module OAuth
8 class Consumer
9 # determine the certificate authority path to verify SSL certs
10 CA_FILES = %W(#{ENV['SSL_CERT_FILE']} /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt /usr/share/curl/curl-ca-bundle.crt)
11 CA_FILES.each do |ca_file|
12 if File.exist?(ca_file)
13 CA_FILE = ca_file
14 break
15 end
16 end
17 CA_FILE = nil unless defined?(CA_FILE)
18
19 @@default_options = {
20 # Signature method used by server. Defaults to HMAC-SHA1
21 :signature_method => 'HMAC-SHA1',
22
23 # default paths on site. These are the same as the defaults set up by the generators
24 :request_token_path => '/oauth/request_token',
25 :authorize_path => '/oauth/authorize',
26 :access_token_path => '/oauth/access_token',
27
28 :proxy => nil,
29 # How do we send the oauth values to the server see
30 # http://oauth.net/core/1.0/#consumer_req_param for more info
31 #
32 # Possible values:
33 #
34 # :header - via the Authorize header (Default) ( option 1. in spec)
35 # :body - url form encoded in body of POST request ( option 2. in spec)
36 # :query_string - via the query part of the url ( option 3. in spec)
37 :scheme => :header,
38
39 # Default http method used for OAuth Token Requests (defaults to :post)
40 :http_method => :post,
41
42 # Add a custom ca_file for consumer
43 # :ca_file => '/etc/certs.pem'
44
45 # Possible values:
46 #
47 # nil, false - no debug output
48 # true - uses $stdout
49 # some_value - uses some_value
50 :debug_output => nil,
51
52 :oauth_version => "1.0"
53 }
54
55 attr_accessor :options, :key, :secret
56 attr_writer :site, :http
57
58 # Create a new consumer instance by passing it a configuration hash:
59 #
60 # @consumer = OAuth::Consumer.new(key, secret, {
61 # :site => "http://term.ie",
62 # :scheme => :header,
63 # :http_method => :post,
64 # :request_token_path => "/oauth/example/request_token.php",
65 # :access_token_path => "/oauth/example/access_token.php",
66 # :authorize_path => "/oauth/example/authorize.php"
67 # })
68 #
69 # Start the process by requesting a token
70 #
71 # @request_token = @consumer.get_request_token
72 # session[:request_token] = @request_token
73 # redirect_to @request_token.authorize_url
74 #
75 # When user returns create an access_token
76 #
77 # @access_token = @request_token.get_access_token
78 # @photos=@access_token.get('/photos.xml')
79 #
80 def initialize(consumer_key, consumer_secret, options = {})
81 @key = consumer_key
82 @secret = consumer_secret
83
84 # ensure that keys are symbols
85 @options = @@default_options.merge(options.inject({}) do |opts, (key, value)|
86 opts[key.to_sym] = value
87 opts
88 end)
89 end
90
91 # The default http method
92 def http_method
93 @http_method ||= @options[:http_method] || :post
94 end
95
96 def debug_output
97 @debug_output ||= begin
98 case @options[:debug_output]
99 when nil, false
100 when true
101 $stdout
102 else
103 @options[:debug_output]
104 end
105 end
106 end
107
108 # The HTTP object for the site. The HTTP Object is what you get when you do Net::HTTP.new
109 def http
110 @http ||= create_http
111 end
112
113 # Contains the root URI for this site
114 def uri(custom_uri = nil)
115 if custom_uri
116 @uri = custom_uri
117 @http = create_http # yike, oh well. less intrusive this way
118 else # if no custom passed, we use existing, which, if unset, is set to site uri
119 @uri ||= URI.parse(site)
120 end
121 end
122
123 def get_access_token(request_token, request_options = {}, *arguments, &block)
124 response = token_request(http_method, (access_token_url? ? access_token_url : access_token_path), request_token, request_options, *arguments, &block)
125 OAuth::AccessToken.from_hash(self, response)
126 end
127
128 # Makes a request to the service for a new OAuth::RequestToken
129 #
130 # @request_token = @consumer.get_request_token
131 #
132 # To include OAuth parameters:
133 #
134 # @request_token = @consumer.get_request_token \
135 # :oauth_callback => "http://example.com/cb"
136 #
137 # To include application-specific parameters:
138 #
139 # @request_token = @consumer.get_request_token({}, :foo => "bar")
140 #
141 # TODO oauth_callback should be a mandatory parameter
142 def get_request_token(request_options = {}, *arguments, &block)
143 # if oauth_callback wasn't provided, it is assumed that oauth_verifiers
144 # will be exchanged out of band
145 request_options[:oauth_callback] ||= OAuth::OUT_OF_BAND unless request_options[:exclude_callback]
146
147 if block_given?
148 response = token_request(http_method,
149 (request_token_url? ? request_token_url : request_token_path),
150 nil,
151 request_options,
152 *arguments, &block)
153 else
154 response = token_request(http_method, (request_token_url? ? request_token_url : request_token_path), nil, request_options, *arguments)
155 end
156 OAuth::RequestToken.from_hash(self, response)
157 end
158
159 # Creates, signs and performs an http request.
160 # It's recommended to use the OAuth::Token classes to set this up correctly.
161 # request_options take precedence over consumer-wide options when signing
162 # a request.
163 # arguments are POST and PUT bodies (a Hash, string-encoded parameters, or
164 # absent), followed by additional HTTP headers.
165 #
166 # @consumer.request(:get, '/people', @token, { :scheme => :query_string })
167 # @consumer.request(:post, '/people', @token, {}, @person.to_xml, { 'Content-Type' => 'application/xml' })
168 #
169 def request(http_method, path, token = nil, request_options = {}, *arguments)
170 if path !~ /^\//
171 @http = create_http(path)
172 _uri = URI.parse(path)
173 path = "#{_uri.path}#{_uri.query ? "?#{_uri.query}" : ""}"
174 end
175
176 # override the request with your own, this is useful for file uploads which Net::HTTP does not do
177 req = create_signed_request(http_method, path, token, request_options, *arguments)
178 return nil if block_given? and yield(req) == :done
179 rsp = http.request(req)
180 # check for an error reported by the Problem Reporting extension
181 # (http://wiki.oauth.net/ProblemReporting)
182 # note: a 200 may actually be an error; check for an oauth_problem key to be sure
183 if !(headers = rsp.to_hash["www-authenticate"]).nil? &&
184 (h = headers.select { |hdr| hdr =~ /^OAuth / }).any? &&
185 h.first =~ /oauth_problem/
186
187 # puts "Header: #{h.first}"
188
189 # TODO doesn't handle broken responses from api.login.yahoo.com
190 # remove debug code when done
191 params = OAuth::Helper.parse_header(h.first)
192
193 # puts "Params: #{params.inspect}"
194 # puts "Body: #{rsp.body}"
195
196 raise OAuth::Problem.new(params.delete("oauth_problem"), rsp, params)
197 end
198
199 rsp
200 end
201
202 # Creates and signs an http request.
203 # It's recommended to use the Token classes to set this up correctly
204 def create_signed_request(http_method, path, token = nil, request_options = {}, *arguments)
205 request = create_http_request(http_method, path, *arguments)
206 sign!(request, token, request_options)
207 request
208 end
209
210 # Creates a request and parses the result as url_encoded. This is used internally for the RequestToken and AccessToken requests.
211 def token_request(http_method, path, token = nil, request_options = {}, *arguments)
212 request_options[:token_request] ||= true
213 response = request(http_method, path, token, request_options, *arguments)
214 case response.code.to_i
215
216 when (200..299)
217 if block_given?
218 yield response.body
219 else
220 # symbolize keys
221 # TODO this could be considered unexpected behavior; symbols or not?
222 # TODO this also drops subsequent values from multi-valued keys
223 CGI.parse(response.body).inject({}) do |h,(k,v)|
224 h[k.strip.to_sym] = v.first
225 h[k.strip] = v.first
226 h
227 end
228 end
229 when (300..399)
230 # this is a redirect
231 uri = URI.parse(response['location'])
232 response.error! if uri.path == path # careful of those infinite redirects
233 self.token_request(http_method, uri.path, token, request_options, arguments)
234 when (400..499)
235 raise OAuth::Unauthorized, response
236 else
237 response.error!
238 end
239 end
240
241 # Sign the Request object. Use this if you have an externally generated http request object you want to sign.
242 def sign!(request, token = nil, request_options = {})
243 request.oauth!(http, self, token, options.merge(request_options))
244 end
245
246 # Return the signature_base_string
247 def signature_base_string(request, token = nil, request_options = {})
248 request.signature_base_string(http, self, token, options.merge(request_options))
249 end
250
251 def site
252 @options[:site].to_s
253 end
254
255 def request_endpoint
256 return nil if @options[:request_endpoint].nil?
257 @options[:request_endpoint].to_s
258 end
259
260 def scheme
261 @options[:scheme]
262 end
263
264 def request_token_path
265 @options[:request_token_path]
266 end
267
268 def authorize_path
269 @options[:authorize_path]
270 end
271
272 def access_token_path
273 @options[:access_token_path]
274 end
275
276 # TODO this is ugly, rewrite
277 def request_token_url
278 @options[:request_token_url] || site + request_token_path
279 end
280
281 def request_token_url?
282 @options.has_key?(:request_token_url)
283 end
284
285 def authorize_url
286 @options[:authorize_url] || site + authorize_path
287 end
288
289 def authorize_url?
290 @options.has_key?(:authorize_url)
291 end
292
293 def access_token_url
294 @options[:access_token_url] || site + access_token_path
295 end
296
297 def access_token_url?
298 @options.has_key?(:access_token_url)
299 end
300
301 def proxy
302 @options[:proxy]
303 end
304
305 protected
306
307 # Instantiates the http object
308 def create_http(_url = nil)
309
310
311 if !request_endpoint.nil?
312 _url = request_endpoint
313 end
314
315
316 if _url.nil? || _url[0] =~ /^\//
317 our_uri = URI.parse(site)
318 else
319 our_uri = URI.parse(_url)
320 end
321
322
323 if proxy.nil?
324 http_object = Net::HTTP.new(our_uri.host, our_uri.port)
325 else
326 proxy_uri = proxy.is_a?(URI) ? proxy : URI.parse(proxy)
327 http_object = Net::HTTP.new(our_uri.host, our_uri.port, proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
328 end
329
330 http_object.use_ssl = (our_uri.scheme == 'https')
331
332 if @options[:ca_file] || CA_FILE
333 http_object.ca_file = @options[:ca_file] || CA_FILE
334 http_object.verify_mode = OpenSSL::SSL::VERIFY_PEER
335 http_object.verify_depth = 5
336 else
337 http_object.verify_mode = OpenSSL::SSL::VERIFY_NONE
338 end
339
340 http_object.read_timeout = http_object.open_timeout = @options[:timeout] || 30
341 http_object.open_timeout = @options[:open_timeout] if @options[:open_timeout]
342 http_object.ssl_version = @options[:ssl_version] if @options[:ssl_version]
343 http_object.set_debug_output(debug_output) if debug_output
344
345 http_object
346 end
347
348 # create the http request object for a given http_method and path
349 def create_http_request(http_method, path, *arguments)
350 http_method = http_method.to_sym
351
352 if [:post, :put, :patch].include?(http_method)
353 data = arguments.shift
354 end
355
356 # if the base site contains a path, add it now
357 # only add if the site host matches the current http object's host
358 # (in case we've specified a full url for token requests)
359 uri = URI.parse(site)
360 path = uri.path + path if uri.path && uri.path != '/' && uri.host == http.address
361
362 headers = arguments.first.is_a?(Hash) ? arguments.shift : {}
363
364 case http_method
365 when :post
366 request = Net::HTTP::Post.new(path,headers)
367 request["Content-Length"] = '0' # Default to 0
368 when :put
369 request = Net::HTTP::Put.new(path,headers)
370 request["Content-Length"] = '0' # Default to 0
371 when :patch
372 request = Net::HTTP::Patch.new(path,headers)
373 request["Content-Length"] = '0' # Default to 0
374 when :get
375 request = Net::HTTP::Get.new(path,headers)
376 when :delete
377 request = Net::HTTP::Delete.new(path,headers)
378 when :head
379 request = Net::HTTP::Head.new(path,headers)
380 else
381 raise ArgumentError, "Don't know how to handle http_method: :#{http_method.to_s}"
382 end
383
384 if data.is_a?(Hash)
385 request.body = OAuth::Helper.normalize(data)
386 request.content_type = 'application/x-www-form-urlencoded'
387 elsif data
388 if data.respond_to?(:read)
389 request.body_stream = data
390 if data.respond_to?(:length)
391 request["Content-Length"] = data.length.to_s
392 elsif data.respond_to?(:stat) && data.stat.respond_to?(:size)
393 request["Content-Length"] = data.stat.size.to_s
394 else
395 raise ArgumentError, "Don't know how to send a body_stream that doesn't respond to .length or .stat.size"
396 end
397 else
398 request.body = data.to_s
399 request["Content-Length"] = request.body.length.to_s
400 end
401 end
402
403 request
404 end
405
406 def marshal_dump(*args)
407 {:key => @key, :secret => @secret, :options => @options}
408 end
409
410 def marshal_load(data)
411 initialize(data[:key], data[:secret], data[:options])
412 end
413
414 end
415 end
0 module OAuth
1 class Error < StandardError
2 end
3 end
0 module OAuth
1 class Problem < OAuth::Unauthorized
2 attr_reader :problem, :params
3 def initialize(problem, request = nil, params = {})
4 super(request)
5 @problem = problem
6 @params = params
7 end
8
9 def to_s
10 problem
11 end
12 end
13 end
0 module OAuth
1 class Unauthorized < OAuth::Error
2 attr_reader :request
3 def initialize(request = nil)
4 @request = request
5 end
6
7 def to_s
8 [request.code, request.message] * " "
9 end
10 end
11 end
0 require 'oauth/errors/error'
1 require 'oauth/errors/unauthorized'
2 require 'oauth/errors/problem'
0 require 'openssl'
1 require 'base64'
2
3 module OAuth
4 module Helper
5 extend self
6
7 # Escape +value+ by URL encoding all non-reserved character.
8 #
9 # See Also: {OAuth core spec version 1.0, section 5.1}[http://oauth.net/core/1.0#rfc.section.5.1]
10 def escape(value)
11 _escape(value.to_s.to_str)
12 rescue ArgumentError
13 _escape(value.to_s.to_str.force_encoding(Encoding::UTF_8))
14 end
15
16 def _escape(string)
17 URI::DEFAULT_PARSER.escape(string, OAuth::RESERVED_CHARACTERS)
18 end
19
20 def unescape(value)
21 URI::DEFAULT_PARSER.unescape(value.gsub('+', '%2B'))
22 end
23
24 # Generate a random key of up to +size+ bytes. The value returned is Base64 encoded with non-word
25 # characters removed.
26 def generate_key(size=32)
27 Base64.encode64(OpenSSL::Random.random_bytes(size)).gsub(/\W/, '')
28 end
29
30 alias_method :generate_nonce, :generate_key
31
32 def generate_timestamp #:nodoc:
33 Time.now.to_i.to_s
34 end
35
36 # Normalize a +Hash+ of parameter values. Parameters are sorted by name, using lexicographical
37 # byte value ordering. If two or more parameters share the same name, they are sorted by their value.
38 # Parameters are concatenated in their sorted order into a single string. For each parameter, the name
39 # is separated from the corresponding value by an "=" character, even if the value is empty. Each
40 # name-value pair is separated by an "&" character.
41 #
42 # See Also: {OAuth core spec version 1.0, section 9.1.1}[http://oauth.net/core/1.0#rfc.section.9.1.1]
43 def normalize(params)
44 params.sort.map do |k, values|
45 if values.is_a?(Array)
46 # make sure the array has an element so we don't lose the key
47 values << nil if values.empty?
48 # multiple values were provided for a single key
49 values.sort.collect do |v|
50 [escape(k),escape(v)] * "="
51 end
52 elsif values.is_a?(Hash)
53 normalize_nested_query(values, k)
54 else
55 [escape(k),escape(values)] * "="
56 end
57 end * "&"
58 end
59
60 #Returns a string representation of the Hash like in URL query string
61 # build_nested_query({:level_1 => {:level_2 => ['value_1','value_2']}}, 'prefix'))
62 # #=> ["prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_1", "prefix%5Blevel_1%5D%5Blevel_2%5D%5B%5D=value_2"]
63 def normalize_nested_query(value, prefix = nil)
64 case value
65 when Array
66 value.map do |v|
67 normalize_nested_query(v, "#{prefix}[]")
68 end.flatten.sort
69 when Hash
70 value.map do |k, v|
71 normalize_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
72 end.flatten.sort
73 else
74 [escape(prefix), escape(value)] * "="
75 end
76 end
77
78 # Parse an Authorization / WWW-Authenticate header into a hash. Takes care of unescaping and
79 # removing surrounding quotes. Raises a OAuth::Problem if the header is not parsable into a
80 # valid hash. Does not validate the keys or values.
81 #
82 # hash = parse_header(headers['Authorization'] || headers['WWW-Authenticate'])
83 # hash['oauth_timestamp']
84 # #=>"1234567890"
85 #
86 def parse_header(header)
87 # decompose
88 params = header[6,header.length].split(/[,=&]/)
89
90 # odd number of arguments - must be a malformed header.
91 raise OAuth::Problem.new("Invalid authorization header") if params.size % 2 != 0
92
93 params.map! do |v|
94 # strip and unescape
95 val = unescape(v.strip)
96 # strip quotes
97 val.sub(/^\"(.*)\"$/, '\1')
98 end
99
100 # convert into a Hash
101 Hash[*params.flatten]
102 end
103
104 def stringify_keys(hash)
105 new_h = {}
106 hash.each do |k, v|
107 new_h[k.to_s] = v.is_a?(Hash) ? stringify_keys(v) : v
108 end
109 new_h
110 end
111 end
112 end
0 module OAuth
1 # request tokens are passed between the consumer and the provider out of
2 # band (i.e. callbacks cannot be used), per section 6.1.1
3 OUT_OF_BAND = "oob"
4
5 # required parameters, per sections 6.1.1, 6.3.1, and 7
6 PARAMETERS = %w(oauth_callback oauth_consumer_key oauth_token
7 oauth_signature_method oauth_timestamp oauth_nonce oauth_verifier
8 oauth_version oauth_signature oauth_body_hash)
9
10 # reserved character regexp, per section 5.1
11 RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~]/
12 end
0 require 'action_controller'
1 require 'action_controller/test_process'
2
3 module OAuth
4 module OAuthTestHelper
5 def mock_incoming_request_with_query(request)
6 incoming = ActionController::TestRequest.new(request.to_hash)
7 incoming.request_uri = request.path
8 incoming.host = request.uri.host
9 incoming.env["SERVER_PORT"] = request.uri.port
10 incoming.env['REQUEST_METHOD'] = request.http_method
11 incoming
12 end
13
14 def mock_incoming_request_with_authorize_header(request)
15 incoming = ActionController::TestRequest.new
16 incoming.request_uri = request.path
17 incoming.host = request.uri.host
18 incoming.env["HTTP_AUTHORIZATION"] = request.to_auth_string
19 incoming.env["SERVER_PORT"] = request.uri.port
20 incoming.env['REQUEST_METHOD'] = request.http_method
21 incoming
22 end
23 end
24 end
0 require 'active_support'
1 require "active_support/version"
2 require 'action_controller'
3 require 'uri'
4
5 if
6 Gem::Version.new(ActiveSupport::VERSION::STRING) < Gem::Version.new("3")
7 then # rails 2.x
8 require 'action_controller/request'
9 unless ActionController::Request::HTTP_METHODS.include?("patch")
10 ActionController::Request::HTTP_METHODS << "patch"
11 ActionController::Request::HTTP_METHOD_LOOKUP["PATCH"] = :patch
12 ActionController::Request::HTTP_METHOD_LOOKUP["patch"] = :patch
13 end
14
15 elsif
16 Gem::Version.new(ActiveSupport::VERSION::STRING) < Gem::Version.new("4")
17 then # rails 3.x
18 require 'action_dispatch/http/request'
19 unless ActionDispatch::Request::HTTP_METHODS.include?("patch")
20 ActionDispatch::Request::HTTP_METHODS << "patch"
21 ActionDispatch::Request::HTTP_METHOD_LOOKUP["PATCH"] = :patch
22 ActionDispatch::Request::HTTP_METHOD_LOOKUP["patch"] = :patch
23 end
24
25 else # rails 4.x and later - already has patch
26 require 'action_dispatch/http/request'
27 end
28
29 module OAuth::RequestProxy
30 class ActionControllerRequest < OAuth::RequestProxy::Base
31 proxies(defined?(ActionDispatch::AbstractRequest) ? ActionDispatch::AbstractRequest : ActionDispatch::Request)
32
33 def method
34 request.method.to_s.upcase
35 end
36
37 def uri
38 request.url
39 end
40
41 def parameters
42 if options[:clobber_request]
43 options[:parameters] || {}
44 else
45 params = request_params.merge(query_params).merge(header_params)
46 params.stringify_keys! if params.respond_to?(:stringify_keys!)
47 params.merge(options[:parameters] || {})
48 end
49 end
50
51 # Override from OAuth::RequestProxy::Base to avoid roundtrip
52 # conversion to Hash or Array and thus preserve the original
53 # parameter names
54 def parameters_for_signature
55 params = []
56 params << options[:parameters].to_query if options[:parameters]
57
58 unless options[:clobber_request]
59 params << header_params.to_query
60 params << request.query_string unless query_string_blank?
61
62 if request.post? && request.content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
63 params << request.raw_post
64 end
65 end
66
67 params.
68 join('&').split('&').
69 reject { |s| s.match(/\A\s*\z/) }.
70 map { |p| p.split('=').map{|esc| CGI.unescape(esc)} }.
71 reject { |kv| kv[0] == 'oauth_signature'}
72 end
73
74 protected
75
76 def query_params
77 request.query_parameters
78 end
79
80 def request_params
81 request.request_parameters
82 end
83
84 end
85 end
0 require 'oauth/request_proxy/rack_request'
1
2 module OAuth::RequestProxy
3 class ActionDispatchRequest < OAuth::RequestProxy::RackRequest
4 proxies ActionDispatch::Request
5 end
6 end
0 require 'oauth/request_proxy'
1 require 'oauth/helper'
2
3 module OAuth::RequestProxy
4 class Base
5 include OAuth::Helper
6
7 def self.proxies(klass)
8 OAuth::RequestProxy.available_proxies[klass] = self
9 end
10
11 attr_accessor :request, :options, :unsigned_parameters
12
13 def initialize(request, options = {})
14 @request = request
15 @unsigned_parameters = (options[:unsigned_parameters] || []).map {|param| param.to_s}
16 @options = options
17 end
18
19 ## OAuth parameters
20
21 def oauth_callback
22 parameters['oauth_callback']
23 end
24
25 def oauth_consumer_key
26 parameters['oauth_consumer_key']
27 end
28
29 def oauth_nonce
30 parameters['oauth_nonce']
31 end
32
33 def oauth_signature
34 # TODO can this be nil?
35 [parameters['oauth_signature']].flatten.first || ""
36 end
37
38 def oauth_signature_method
39 case parameters['oauth_signature_method']
40 when Array
41 parameters['oauth_signature_method'].first
42 else
43 parameters['oauth_signature_method']
44 end
45 end
46
47 def oauth_timestamp
48 parameters['oauth_timestamp']
49 end
50
51 def oauth_token
52 parameters['oauth_token']
53 end
54
55 def oauth_verifier
56 parameters['oauth_verifier']
57 end
58
59 def oauth_version
60 parameters["oauth_version"]
61 end
62
63 # TODO deprecate these
64 alias_method :consumer_key, :oauth_consumer_key
65 alias_method :token, :oauth_token
66 alias_method :nonce, :oauth_nonce
67 alias_method :timestamp, :oauth_timestamp
68 alias_method :signature, :oauth_signature
69 alias_method :signature_method, :oauth_signature_method
70
71 ## Parameter accessors
72
73 def parameters
74 raise NotImplementedError, "Must be implemented by subclasses"
75 end
76
77 def parameters_for_signature
78 parameters.select { |k,v| not signature_and_unsigned_parameters.include?(k) }
79 end
80
81 def oauth_parameters
82 parameters.select { |k,v| OAuth::PARAMETERS.include?(k) }.reject { |k,v| v == "" }
83 end
84
85 def non_oauth_parameters
86 parameters.reject { |k,v| OAuth::PARAMETERS.include?(k) }
87 end
88
89 def signature_and_unsigned_parameters
90 unsigned_parameters+["oauth_signature"]
91 end
92
93 # See 9.1.2 in specs
94 def normalized_uri
95 u = URI.parse(uri)
96 "#{u.scheme.downcase}://#{u.host.downcase}#{(u.scheme.downcase == 'http' && u.port != 80) || (u.scheme.downcase == 'https' && u.port != 443) ? ":#{u.port}" : ""}#{(u.path && u.path != '') ? u.path : '/'}"
97 end
98
99 # See 9.1.1. in specs Normalize Request Parameters
100 def normalized_parameters
101 normalize(parameters_for_signature)
102 end
103
104 def sign(options = {})
105 OAuth::Signature.sign(self, options)
106 end
107
108 def sign!(options = {})
109 parameters["oauth_signature"] = sign(options)
110 @signed = true
111 signature
112 end
113
114 # See 9.1 in specs
115 def signature_base_string
116 base = [method, normalized_uri, normalized_parameters]
117 base.map { |v| escape(v) }.join("&")
118 end
119
120 # Has this request been signed yet?
121 def signed?
122 @signed
123 end
124
125 # URI, including OAuth parameters
126 def signed_uri(with_oauth = true)
127 if signed?
128 if with_oauth
129 params = parameters
130 else
131 params = non_oauth_parameters
132 end
133
134 [uri, normalize(params)] * "?"
135 else
136 STDERR.puts "This request has not yet been signed!"
137 end
138 end
139
140 # Authorization header for OAuth
141 def oauth_header(options = {})
142 header_params_str = oauth_parameters.map { |k,v| "#{k}=\"#{escape(v)}\"" }.join(', ')
143
144 realm = "realm=\"#{options[:realm]}\", " if options[:realm]
145 "OAuth #{realm}#{header_params_str}"
146 end
147
148 def query_string_blank?
149 if uri = request.env['REQUEST_URI']
150 uri.split('?', 2)[1].nil?
151 else
152 request.query_string.match(/\A\s*\z/)
153 end
154 end
155
156 protected
157
158 def header_params
159 %w( X-HTTP_AUTHORIZATION Authorization HTTP_AUTHORIZATION ).each do |header|
160 next unless request.env.include?(header)
161
162 header = request.env[header]
163 next unless header[0,6] == 'OAuth '
164
165 # parse the header into a Hash
166 oauth_params = OAuth::Helper.parse_header(header)
167
168 # remove non-OAuth parameters
169 oauth_params.reject! { |k,v| k !~ /^oauth_/ }
170
171 return oauth_params
172 end
173
174 return {}
175 end
176 end
177 end
0 require 'oauth/request_proxy/base'
1 require 'curb'
2 require 'uri'
3 require 'cgi'
4
5 module OAuth::RequestProxy::Curl
6 class Easy < OAuth::RequestProxy::Base
7 # Proxy for signing Curl::Easy requests
8 # Usage example:
9 # oauth_params = {:consumer => oauth_consumer, :token => access_token}
10 # req = Curl::Easy.new(uri)
11 # oauth_helper = OAuth::Client::Helper.new(req, oauth_params.merge(:request_uri => uri))
12 # req.headers.merge!({"Authorization" => oauth_helper.header})
13 # req.http_get
14 # response = req.body_str
15 proxies ::Curl::Easy
16
17 def method
18 nil
19 end
20
21 def uri
22 options[:uri].to_s
23 end
24
25 def parameters
26 if options[:clobber_request]
27 options[:parameters]
28 else
29 post_parameters.merge(query_parameters).merge(options[:parameters] || {})
30 end
31 end
32
33 private
34
35 def query_parameters
36 query = URI.parse(request.url).query
37 return(query ? CGI.parse(query) : {})
38 end
39
40 def post_parameters
41 post_body = {}
42
43 # Post params are only used if posting form data
44 if (request.headers['Content-Type'] && request.headers['Content-Type'].to_s.downcase.start_with?("application/x-www-form-urlencoded"))
45
46 request.post_body.split("&").each do |str|
47 param = str.split("=")
48 post_body[param[0]] = param[1]
49 end
50 end
51 post_body
52 end
53 end
54 end
0 require 'oauth/request_proxy/base'
1 # em-http also uses adddressable so there is no need to require uri.
2 require 'em-http'
3 require 'cgi'
4
5 module OAuth::RequestProxy::EventMachine
6 class HttpRequest < OAuth::RequestProxy::Base
7
8 # A Proxy for use when you need to sign EventMachine::HttpClient instances.
9 # It needs to be called once the client is construct but before data is sent.
10 # Also see oauth/client/em-http
11 proxies ::EventMachine::HttpClient
12
13 # Request in this con
14
15 def method
16 request.method
17 end
18
19 def uri
20 request.normalize_uri.to_s
21 end
22
23 def parameters
24 if options[:clobber_request]
25 options[:parameters]
26 else
27 all_parameters
28 end
29 end
30
31 protected
32
33 def all_parameters
34 merged_parameters({}, post_parameters, query_parameters, options[:parameters])
35 end
36
37 def query_parameters
38 CGI.parse(request.normalize_uri.query.to_s)
39 end
40
41 def post_parameters
42 headers = request.options[:head] || {}
43 form_encoded = headers['Content-Type'].to_s.downcase.start_with?("application/x-www-form-urlencoded")
44 if ['POST', 'PUT'].include?(method) && form_encoded
45 CGI.parse(request.normalize_body.to_s)
46 else
47 {}
48 end
49 end
50
51 def merged_parameters(params, *extra_params)
52 extra_params.compact.each do |params_pairs|
53 params_pairs.each_pair do |key, value|
54 if params.has_key?(key)
55 params[key] += value
56 else
57 params[key] = [value].flatten
58 end
59 end
60 end
61 params
62 end
63
64 end
65 end
0 require 'xmpp4r'
1 require 'oauth/request_proxy/base'
2
3 module OAuth
4 module RequestProxy
5 class JabberRequest < OAuth::RequestProxy::Base
6 proxies Jabber::Iq
7 proxies Jabber::Presence
8 proxies Jabber::Message
9
10 def parameters
11 return @params if @params
12
13 @params = {}
14
15 oauth = @request.get_elements('//oauth').first
16 return @params unless oauth
17
18 %w( oauth_token oauth_consumer_key oauth_signature_method oauth_signature
19 oauth_timestamp oauth_nonce oauth_version ).each do |param|
20 next unless element = oauth.first_element(param)
21 @params[param] = element.text
22 end
23
24 @params
25 end
26
27 def method
28 @request.name
29 end
30
31 def uri
32 [@request.from.strip.to_s, @request.to.strip.to_s].join("&")
33 end
34
35 def normalized_uri
36 uri
37 end
38 end
39 end
40 end
0 require 'oauth/request_proxy/base'
1
2 module OAuth
3 module RequestProxy
4 # RequestProxy for Hashes to facilitate simpler signature creation.
5 # Usage:
6 # request = OAuth::RequestProxy.proxy \
7 # "method" => "iq",
8 # "uri" => [from, to] * "&",
9 # "parameters" => {
10 # "oauth_consumer_key" => oauth_consumer_key,
11 # "oauth_token" => oauth_token,
12 # "oauth_signature_method" => "HMAC-SHA1"
13 # }
14 #
15 # signature = OAuth::Signature.sign \
16 # request,
17 # :consumer_secret => oauth_consumer_secret,
18 # :token_secret => oauth_token_secret,
19 class MockRequest < OAuth::RequestProxy::Base
20 proxies Hash
21
22 def parameters
23 @request["parameters"]
24 end
25
26 def method
27 @request["method"]
28 end
29
30 def normalized_uri
31 super
32 rescue
33 # if this is a non-standard URI, it may not parse properly
34 # in that case, assume that it's already been normalized
35 uri
36 end
37
38 def uri
39 @request["uri"]
40 end
41 end
42 end
43 end
0 require 'oauth/request_proxy/base'
1 require 'net/http'
2 require 'uri'
3 require 'cgi'
4
5 module OAuth::RequestProxy::Net
6 module HTTP
7 class HTTPRequest < OAuth::RequestProxy::Base
8 proxies ::Net::HTTPGenericRequest
9
10 def method
11 request.method
12 end
13
14 def uri
15 options[:uri].to_s
16 end
17
18 def parameters
19 if options[:clobber_request]
20 options[:parameters]
21 else
22 all_parameters
23 end
24 end
25
26 def body
27 request.body
28 end
29
30 private
31
32 def all_parameters
33 request_params = CGI.parse(query_string)
34 # request_params.each{|k,v| request_params[k] = [nil] if v == []}
35
36 if options[:parameters]
37 options[:parameters].each do |k,v|
38 if request_params.has_key?(k) && v
39 request_params[k] << v
40 else
41 request_params[k] = [v]
42 end
43 end
44 end
45 request_params
46 end
47
48 def query_string
49 params = [ query_params, auth_header_params ]
50 params << post_params if (method.to_s.upcase == 'POST' || method.to_s.upcase == 'PUT') && form_url_encoded?
51 params.compact.join('&')
52 end
53
54 def form_url_encoded?
55 request['Content-Type'] != nil && request['Content-Type'].to_s.downcase.start_with?('application/x-www-form-urlencoded')
56 end
57
58 def query_params
59 URI.parse(request.path).query
60 end
61
62 def post_params
63 request.body
64 end
65
66 def auth_header_params
67 return nil unless request['Authorization'] && request['Authorization'][0,5] == 'OAuth'
68 request['Authorization']
69 end
70 end
71 end
72 end
0 require 'oauth/request_proxy/base'
1 require 'uri'
2 require 'rack'
3
4 module OAuth::RequestProxy
5 class RackRequest < OAuth::RequestProxy::Base
6 proxies Rack::Request
7
8 def method
9 request.env["rack.methodoverride.original_method"] || request.request_method
10 end
11
12 def uri
13 request.url
14 end
15
16 def parameters
17 if options[:clobber_request]
18 options[:parameters] || {}
19 else
20 params = request_params.merge(query_params).merge(header_params)
21 params.merge(options[:parameters] || {})
22 end
23 end
24
25 def signature
26 parameters['oauth_signature']
27 end
28
29 protected
30
31 def query_params
32 request.GET
33 end
34
35 def request_params
36 if request.content_type and request.content_type.to_s.downcase.start_with?("application/x-www-form-urlencoded")
37 request.POST
38 else
39 {}
40 end
41 end
42 end
43 end
0 require 'oauth/request_proxy/base'
1 require 'rest-client'
2 require 'uri'
3 require 'cgi'
4
5 module OAuth::RequestProxy::RestClient
6 class Request < OAuth::RequestProxy::Base
7 proxies RestClient::Request
8
9 def method
10 request.method.to_s.upcase
11 end
12
13 def uri
14 request.url
15 end
16
17 def parameters
18 if options[:clobber_request]
19 options[:parameters] || {}
20 else
21 post_parameters.merge(query_params).merge(options[:parameters] || {})
22 end
23 end
24
25 protected
26
27 def query_params
28 query = URI.parse(request.url).query
29 query ? CGI.parse(query) : {}
30 end
31
32 def request_params
33 end
34
35 def post_parameters
36 # Post params are only used if posting form data
37 if method == 'POST' || method == 'PUT'
38 OAuth::Helper.stringify_keys(query_string_to_hash(request.payload.to_s) || {})
39 else
40 {}
41 end
42 end
43
44 private
45
46 def query_string_to_hash(query)
47 keyvals = query.split('&').inject({}) do |result, q|
48 k,v = q.split('=')
49 if !v.nil?
50 result.merge({k => v})
51 elsif !result.key?(k)
52 result.merge({k => true})
53 else
54 result
55 end
56 end
57 keyvals
58 end
59
60 end
61 end
0 require 'oauth/request_proxy/base'
1 require 'typhoeus'
2 require 'typhoeus/request'
3 require 'uri'
4 require 'cgi'
5
6 module OAuth::RequestProxy::Typhoeus
7 class Request < OAuth::RequestProxy::Base
8 # Proxy for signing Typhoeus::Request requests
9 # Usage example:
10 # oauth_params = {:consumer => oauth_consumer, :token => access_token}
11 # req = Typhoeus::Request.new(uri, options)
12 # oauth_helper = OAuth::Client::Helper.new(req, oauth_params.merge(:request_uri => uri))
13 # req.options[:headers].merge!({"Authorization" => oauth_helper.header})
14 # hydra = Typhoeus::Hydra.new()
15 # hydra.queue(req)
16 # hydra.run
17 # response = req.response
18 proxies Typhoeus::Request
19
20 def method
21 request_method = request.options[:method].to_s.upcase
22 request_method.empty? ? 'GET' : request_method
23 end
24
25 def uri
26 options[:uri].to_s
27 end
28
29 def parameters
30 if options[:clobber_request]
31 options[:parameters]
32 else
33 post_parameters.merge(query_parameters).merge(options[:parameters] || {})
34 end
35 end
36
37 private
38
39 def query_parameters
40 query = URI.parse(request.url).query
41 query ? CGI.parse(query) : {}
42 end
43
44 def post_parameters
45 # Post params are only used if posting form data
46 if method == 'POST'
47 OAuth::Helper.stringify_keys(request.options[:params] || {})
48 else
49 {}
50 end
51 end
52 end
53 end
0 module OAuth
1 module RequestProxy
2 def self.available_proxies #:nodoc:
3 @available_proxies ||= {}
4 end
5
6 def self.proxy(request, options = {})
7 return request if request.kind_of?(OAuth::RequestProxy::Base)
8
9 klass = available_proxies[request.class]
10
11 # Search for possible superclass matches.
12 if klass.nil?
13 request_parent = available_proxies.keys.find { |rc| request.kind_of?(rc) }
14 klass = available_proxies[request_parent]
15 end
16
17 raise UnknownRequestType, request.class.to_s unless klass
18 klass.new(request, options)
19 end
20
21 class UnknownRequestType < Exception; end
22 end
23 end
0 require 'oauth/helper'
1 require 'oauth/consumer'
2
3 module OAuth
4 # This is mainly used to create consumer credentials and can pretty much be ignored if you want to create your own
5 class Server
6 include OAuth::Helper
7 attr_accessor :base_url
8
9 @@server_paths = {
10 :request_token_path => "/oauth/request_token",
11 :authorize_path => "/oauth/authorize",
12 :access_token_path => "/oauth/access_token"
13 }
14
15 # Create a new server instance
16 def initialize(base_url, paths = {})
17 @base_url = base_url
18 @paths = @@server_paths.merge(paths)
19 end
20
21 def generate_credentials
22 [generate_key(16), generate_key]
23 end
24
25 def generate_consumer_credentials(params = {})
26 Consumer.new(*generate_credentials)
27 end
28
29 # mainly for testing purposes
30 def create_consumer
31 creds = generate_credentials
32 Consumer.new(creds[0], creds[1],
33 {
34 :site => base_url,
35 :request_token_path => request_token_path,
36 :authorize_path => authorize_path,
37 :access_token_path => access_token_path
38 })
39 end
40
41 def request_token_path
42 @paths[:request_token_path]
43 end
44
45 def request_token_url
46 base_url + request_token_path
47 end
48
49 def authorize_path
50 @paths[:authorize_path]
51 end
52
53 def authorize_url
54 base_url + authorize_path
55 end
56
57 def access_token_path
58 @paths[:access_token_path]
59 end
60
61 def access_token_url
62 base_url + access_token_path
63 end
64 end
65 end
0 require 'oauth/signature'
1 require 'oauth/helper'
2 require 'oauth/request_proxy/base'
3 require 'base64'
4
5 module OAuth::Signature
6 class Base
7 include OAuth::Helper
8
9 attr_accessor :options
10 attr_reader :token_secret, :consumer_secret, :request
11
12 def self.implements(signature_method = nil)
13 return @implements if signature_method.nil?
14 @implements = signature_method
15 OAuth::Signature.available_methods[@implements] = self
16 end
17
18 def initialize(request, options = {}, &block)
19 raise TypeError unless request.kind_of?(OAuth::RequestProxy::Base)
20 @request = request
21 @options = options
22
23 ## consumer secret was determined beforehand
24
25 @consumer_secret = options[:consumer].secret if options[:consumer]
26
27 # presence of :consumer_secret option will override any Consumer that's provided
28 @consumer_secret = options[:consumer_secret] if options[:consumer_secret]
29
30 ## token secret was determined beforehand
31
32 @token_secret = options[:token].secret if options[:token]
33
34 # presence of :token_secret option will override any Token that's provided
35 @token_secret = options[:token_secret] if options[:token_secret]
36
37 # override secrets based on the values returned from the block (if any)
38 if block_given?
39 # consumer secret and token secret need to be looked up based on pieces of the request
40 secrets = yield block.arity == 1 ? request : [token, consumer_key, nonce, request.timestamp]
41 if secrets.is_a?(Array) && secrets.size == 2
42 @token_secret = secrets[0]
43 @consumer_secret = secrets[1]
44 end
45 end
46 end
47
48 def signature
49 Base64.encode64(digest).chomp.gsub(/\n/,'')
50 end
51
52 def ==(cmp_signature)
53 signature == cmp_signature
54 end
55
56 def verify
57 self == self.request.signature
58 end
59
60 def signature_base_string
61 request.signature_base_string
62 end
63
64 def body_hash
65 raise_instantiation_error
66 end
67
68 private
69
70 def token
71 request.token
72 end
73
74 def consumer_key
75 request.consumer_key
76 end
77
78 def nonce
79 request.nonce
80 end
81
82 def secret
83 "#{escape(consumer_secret)}&#{escape(token_secret)}"
84 end
85
86 def digest
87 raise_instantiation_error
88 end
89
90 def raise_instantiation_error
91 raise NotImplementedError, "Cannot instantiate #{self.class.name} class directly."
92 end
93
94 end
95 end
0 require 'oauth/signature/base'
1
2 module OAuth::Signature::HMAC
3 class SHA1 < OAuth::Signature::Base
4 implements 'hmac-sha1'
5
6 def body_hash
7 Base64.encode64(OpenSSL::Digest::SHA1.digest(request.body || '')).chomp.gsub(/\n/,'')
8 end
9
10 private
11
12 def digest
13 OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), secret, signature_base_string)
14 end
15 end
16 end
0 require 'oauth/signature/base'
1
2 module OAuth::Signature
3 class PLAINTEXT < Base
4 implements 'plaintext'
5
6 def signature
7 signature_base_string
8 end
9
10 def ==(cmp_signature)
11 signature.to_s == cmp_signature.to_s
12 end
13
14 def signature_base_string
15 secret
16 end
17
18 def body_hash
19 nil
20 end
21
22 private
23
24 def secret
25 super
26 end
27 end
28 end
0 require 'oauth/signature/base'
1
2 module OAuth::Signature::RSA
3 class SHA1 < OAuth::Signature::Base
4 implements 'rsa-sha1'
5
6 def ==(cmp_signature)
7 public_key.verify(OpenSSL::Digest::SHA1.new, Base64.decode64(cmp_signature.is_a?(Array) ? cmp_signature.first : cmp_signature), signature_base_string)
8 end
9
10 def public_key
11 if consumer_secret.is_a?(String)
12 decode_public_key
13 elsif consumer_secret.is_a?(OpenSSL::X509::Certificate)
14 consumer_secret.public_key
15 else
16 consumer_secret
17 end
18 end
19
20 def body_hash
21 Base64.encode64(OpenSSL::Digest::SHA1.digest(request.body || '')).chomp.gsub(/\n/,'')
22 end
23
24 private
25
26 def decode_public_key
27 case consumer_secret
28 when /-----BEGIN CERTIFICATE-----/
29 OpenSSL::X509::Certificate.new( consumer_secret).public_key
30 else
31 OpenSSL::PKey::RSA.new( consumer_secret)
32 end
33 end
34
35 def digest
36 private_key = OpenSSL::PKey::RSA.new(
37 if options[:private_key_file]
38 IO.read(options[:private_key_file])
39 elsif options[:private_key]
40 options[:private_key]
41 else
42 consumer_secret
43 end
44 )
45
46 private_key.sign(OpenSSL::Digest::SHA1.new, signature_base_string)
47 end
48 end
49 end
0 module OAuth
1 module Signature
2 # Returns a list of available signature methods
3 def self.available_methods
4 @available_methods ||= {}
5 end
6
7 # Build a signature from a +request+.
8 #
9 # Raises UnknownSignatureMethod exception if the signature method is unknown.
10 def self.build(request, options = {}, &block)
11 request = OAuth::RequestProxy.proxy(request, options)
12 klass = available_methods[
13 (request.signature_method ||
14 ((c = request.options[:consumer]) && c.options[:signature_method]) ||
15 "").downcase]
16 raise UnknownSignatureMethod, request.signature_method unless klass
17 klass.new(request, options, &block)
18 end
19
20 # Sign a +request+
21 def self.sign(request, options = {}, &block)
22 self.build(request, options, &block).signature
23 end
24
25 # Verify the signature of +request+
26 def self.verify(request, options = {}, &block)
27 self.build(request, options, &block).verify
28 end
29
30 # Create the signature base string for +request+. This string is the normalized parameter information.
31 #
32 # See Also: {OAuth core spec version 1.0, section 9.1.1}[http://oauth.net/core/1.0#rfc.section.9.1.1]
33 def self.signature_base_string(request, options = {}, &block)
34 self.build(request, options, &block).signature_base_string
35 end
36
37 # Create the body hash for a request
38 def self.body_hash(request, options = {}, &block)
39 self.build(request, options, &block).body_hash
40 end
41
42 class UnknownSignatureMethod < Exception; end
43 end
44 end
0 # this exists for backwards-compatibility
1
2 require 'oauth/tokens/token'
3 require 'oauth/tokens/server_token'
4 require 'oauth/tokens/consumer_token'
5 require 'oauth/tokens/request_token'
6 require 'oauth/tokens/access_token'
0 module OAuth
1 # The Access Token is used for the actual "real" web service calls that you perform against the server
2 class AccessToken < ConsumerToken
3 # The less intrusive way. Otherwise, if we are to do it correctly inside consumer,
4 # we need to restructure and touch more methods: request(), sign!(), etc.
5 def request(http_method, path, *arguments)
6 request_uri = URI.parse(path)
7 site_uri = consumer.uri
8 is_service_uri_different = (request_uri.absolute? && request_uri != site_uri)
9 begin
10 consumer.uri(request_uri) if is_service_uri_different
11 @response = super(http_method, path, *arguments)
12 ensure
13 # NOTE: reset for wholesomeness? meaning that we admit only AccessToken service calls may use different URIs?
14 # so reset in case consumer is still used for other token-management tasks subsequently?
15 consumer.uri(site_uri) if is_service_uri_different
16 end
17 @response
18 end
19
20 # Make a regular GET request using AccessToken
21 #
22 # @response = @token.get('/people')
23 # @response = @token.get('/people', { 'Accept'=>'application/xml' })
24 #
25 def get(path, headers = {})
26 request(:get, path, headers)
27 end
28
29 # Make a regular HEAD request using AccessToken
30 #
31 # @response = @token.head('/people')
32 #
33 def head(path, headers = {})
34 request(:head, path, headers)
35 end
36
37 # Make a regular POST request using AccessToken
38 #
39 # @response = @token.post('/people')
40 # @response = @token.post('/people', { :name => 'Bob', :email => 'bob@mailinator.com' })
41 # @response = @token.post('/people', { :name => 'Bob', :email => 'bob@mailinator.com' }, { 'Accept' => 'application/xml' })
42 # @response = @token.post('/people', nil, {'Accept' => 'application/xml' })
43 # @response = @token.post('/people', @person.to_xml, { 'Accept'=>'application/xml', 'Content-Type' => 'application/xml' })
44 #
45 def post(path, body = '', headers = {})
46 request(:post, path, body, headers)
47 end
48
49 # Make a regular PUT request using AccessToken
50 #
51 # @response = @token.put('/people/123')
52 # @response = @token.put('/people/123', { :name => 'Bob', :email => 'bob@mailinator.com' })
53 # @response = @token.put('/people/123', { :name => 'Bob', :email => 'bob@mailinator.com' }, { 'Accept' => 'application/xml' })
54 # @response = @token.put('/people/123', nil, { 'Accept' => 'application/xml' })
55 # @response = @token.put('/people/123', @person.to_xml, { 'Accept' => 'application/xml', 'Content-Type' => 'application/xml' })
56 #
57 def put(path, body = '', headers = {})
58 request(:put, path, body, headers)
59 end
60
61 # Make a regular PATCH request using AccessToken
62 #
63 # @response = @token.patch('/people/123')
64 # @response = @token.patch('/people/123', { :name => 'Bob', :email => 'bob@mailinator.com' })
65 # @response = @token.patch('/people/123', { :name => 'Bob', :email => 'bob@mailinator.com' }, { 'Accept' => 'application/xml' })
66 # @response = @token.patch('/people/123', nil, { 'Accept' => 'application/xml' })
67 # @response = @token.patch('/people/123', @person.to_xml, { 'Accept' => 'application/xml', 'Content-Type' => 'application/xml' })
68 #
69 def patch(path, body = '', headers = {})
70 request(:patch, path, body, headers)
71 end
72
73 # Make a regular DELETE request using AccessToken
74 #
75 # @response = @token.delete('/people/123')
76 # @response = @token.delete('/people/123', { 'Accept' => 'application/xml' })
77 #
78 def delete(path, headers = {})
79 request(:delete, path, headers)
80 end
81 end
82 end
0 module OAuth
1 # Superclass for tokens used by OAuth Clients
2 class ConsumerToken < Token
3 attr_accessor :consumer, :params
4 attr_reader :response
5
6 def self.from_hash(consumer, hash)
7 token = self.new(consumer, hash[:oauth_token], hash[:oauth_token_secret])
8 token.params = hash
9 token
10 end
11
12 def initialize(consumer, token="", secret="")
13 super(token, secret)
14 @consumer = consumer
15 @params = {}
16 end
17
18 # Make a signed request using given http_method to the path
19 #
20 # @token.request(:get, '/people')
21 # @token.request(:post, '/people', @person.to_xml, { 'Content-Type' => 'application/xml' })
22 #
23 def request(http_method, path, *arguments)
24 @response = consumer.request(http_method, path, self, {}, *arguments)
25 end
26
27 # Sign a request generated elsewhere using Net:HTTP::Post.new or friends
28 def sign!(request, options = {})
29 consumer.sign!(request, self, options)
30 end
31 end
32 end
0 module OAuth
1 # The RequestToken is used for the initial Request.
2 # This is normally created by the Consumer object.
3 class RequestToken < ConsumerToken
4
5 # Generate an authorization URL for user authorization
6 def authorize_url(params = nil)
7 return nil if self.token.nil?
8
9 params = (params || {}).merge(:oauth_token => self.token)
10 build_authorize_url(consumer.authorize_url, params)
11 end
12
13 def callback_confirmed?
14 params[:oauth_callback_confirmed] == "true"
15 end
16
17 # exchange for AccessToken on server
18 def get_access_token(options = {}, *arguments)
19 response = consumer.token_request(consumer.http_method, (consumer.access_token_url? ? consumer.access_token_url : consumer.access_token_path), self, options, *arguments)
20 OAuth::AccessToken.from_hash(consumer, response)
21 end
22
23 protected
24
25 # construct an authorization url
26 def build_authorize_url(base_url, params)
27 uri = URI.parse(base_url.to_s)
28 queries = {}
29 queries = Hash[URI.decode_www_form(uri.query)] if uri.query
30 # TODO doesn't handle array values correctly
31 queries.merge!(params) if params
32 uri.query = URI.encode_www_form(queries) if !queries.empty?
33 uri.to_s
34 end
35 end
36 end
0 module OAuth
1 # Used on the server for generating tokens
2 class ServerToken < Token
3
4 def initialize
5 super(generate_key(16), generate_key)
6 end
7 end
8 end
0 module OAuth
1 # Superclass for the various tokens used by OAuth
2 class Token
3 include OAuth::Helper
4
5 attr_accessor :token, :secret
6
7 def initialize(token, secret)
8 @token = token
9 @secret = secret
10 end
11
12 def to_query
13 "oauth_token=#{escape(token)}&oauth_token_secret=#{escape(secret)}"
14 end
15 end
16 end
0 module OAuth
1 VERSION = "0.5.4"
2 end
0 root = File.dirname(__FILE__)
1 $LOAD_PATH << root unless $LOAD_PATH.include?(root)
2
3 require 'oauth/version'
4
5 require 'oauth/oauth'
6
7 require 'oauth/client/helper'
8 require 'oauth/signature/hmac/sha1'
9 require 'oauth/signature/rsa/sha1'
10 require 'oauth/request_proxy/mock_request'
0 # -*- coding: utf-8 -*-
1 module Pluggaloid
2 class Error < ::StandardError; end
3
4 class ArgumentError < Error; end
5
6 class TypeError < Error; end
7
8 class FilterError < Error; end
9
10 class NoDefaultDelayerError < Error; end
11
12 class DuplicateListenerSlugError < Error; end
13 end
0 # -*- coding: utf-8 -*-
1
2 class Pluggaloid::Event
3 include InstanceStorage
4
5 # オプション。以下のキーを持つHash
6 # :prototype :: 引数の数と型。Arrayで、type_strictが解釈できる条件を設定する
7 # :priority :: Delayerの優先順位
8 attr_accessor :options
9
10 # フィルタを別のスレッドで実行する。偽ならメインスレッドでフィルタを実行する
11 @filter_another_thread = false
12
13 def initialize(*args)
14 super
15 @options = {}
16 @listeners = []
17 @filters = [] end
18
19 def vm
20 self.class.vm end
21
22 # イベントの優先順位を取得する
23 # ==== Return
24 # プラグインの優先順位
25 def priority
26 if @options.has_key? :priority
27 @options[:priority] end end
28
29 # イベントを引数 _args_ で発生させる
30 # ==== Args
31 # [*args] イベントの引数
32 # ==== Return
33 # Delayerか、イベントを待ち受けているリスナがない場合はnil
34 def call(*args)
35 if self.class.filter_another_thread
36 if @filters.empty?
37 vm.Delayer.new(*Array(priority)) do
38 call_all_listeners(args) end
39 else
40 Thread.new do
41 filtered_args = filtering(*args)
42 if filtered_args.is_a? Array
43 vm.Delayer.new(*Array(priority)) do
44 call_all_listeners(filtered_args) end end end end
45 else
46 vm.Delayer.new(*Array(priority)) do
47 args = filtering(*args) if not @filters.empty?
48 call_all_listeners(args) if args.is_a? Array end end end
49
50 # 引数 _args_ をフィルタリングした結果を返す
51 # ==== Args
52 # [*args] 引数
53 # ==== Return
54 # フィルタされた引数の配列
55 def filtering(*args)
56 catch(:filter_exit) {
57 @filters.reduce(args){ |acm, event_filter|
58 event_filter.filtering(*acm) } } end
59
60 def add_listener(listener)
61 unless listener.is_a? Pluggaloid::Listener
62 raise Pluggaloid::ArgumentError, "First argument must be Pluggaloid::Listener, but given #{listener.class}." end
63 if @listeners.map(&:slug).include?(listener.slug)
64 raise Pluggaloid::DuplicateListenerSlugError, "Listener slug #{listener.slug} already exists." end
65 @listeners << listener
66 self end
67
68 def delete_listener(event_filter)
69 @listeners.delete(event_filter)
70 self end
71
72 # イベントフィルタを追加する
73 # ==== Args
74 # [event_filter] イベントフィルタ(Filter)
75 # ==== Return
76 # self
77 def add_filter(event_filter)
78 unless event_filter.is_a? Pluggaloid::Filter
79 raise Pluggaloid::ArgumentError, "First argument must be Pluggaloid::Filter, but given #{event_filter.class}." end
80 @filters << event_filter
81 self end
82
83 # イベントフィルタを削除する
84 # ==== Args
85 # [event_filter] イベントフィルタ(EventFilter)
86 # ==== Return
87 # self
88 def delete_filter(event_filter)
89 @filters.delete(event_filter)
90 self end
91
92 private
93 def call_all_listeners(args)
94 catch(:plugin_exit) do
95 @listeners.each do |listener|
96 listener.call(*args) end end end
97
98 class << self
99 attr_accessor :filter_another_thread, :vm
100
101 alias __clear_aF4e__ clear!
102 def clear!
103 @filter_another_thread = false
104 __clear_aF4e__()
105 end
106 end
107
108 clear!
109 end
0 # -*- coding: utf-8 -*-
1
2 class Pluggaloid::Filter < Pluggaloid::Handler
3 # フィルタ内部で使う。フィルタの実行をキャンセルする。Plugin#filtering はfalseを返し、
4 # イベントのフィルタの場合は、そのイベントの実行自体をキャンセルする。
5 # また、 _result_ が渡された場合、Event#filtering の戻り値は _result_ になる。
6 def self.cancel!(result=false)
7 throw :filter_exit, result end
8
9 # ==== Args
10 # [event] 監視するEventのインスタンス
11 # [name:] 名前(String | nil)
12 # [slug:] フィルタスラッグ(Symbol | nil)
13 # [tags:] Pluggaloid::HandlerTag|Array フィルタのタグ
14 # [&callback] コールバック
15 def initialize(event, **kwrest, &callback)
16 super
17 @callback = Proc.new
18 event.add_filter self end
19
20 # イベントを実行する
21 # ==== Args
22 # [*args] イベントの引数
23 # ==== Return
24 # 加工後の引数の配列
25 def filtering(*args)
26 length = args.size
27 result = @callback.call(*args, &self.class.method(:cancel!))
28 if length != result.size
29 raise Pluggaloid::FilterError, "filter changes arguments length (#{length} to #{result.size})" end
30 result end
31
32 # このリスナを削除する
33 # ==== Return
34 # self
35 def detach
36 @event.delete_filter(self)
37 self end
38
39 end
0 # -*- coding: utf-8 -*-
1
2 =begin rdoc
3 イベントのListenerやFilterのスーパクラス。
4 イベントに関連付けたり、タグを付けたりできる
5 =end
6 class Pluggaloid::Handler < Pluggaloid::Identity
7 Lock = Mutex.new
8 attr_reader :tags
9
10 # ==== Args
11 # [event] 監視するEventのインスタンス
12 # [name:] 名前(String | nil)
13 # [slug:] ハンドラスラッグ(Symbol | nil)
14 # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ
15 # [&callback] コールバック
16 def initialize(event, tags: [], **kwrest)
17 raise Pluggaloid::TypeError, "Argument `event' must be instance of Pluggaloid::Event, but given #{event.class}." unless event.is_a? Pluggaloid::Event
18 super(**kwrest)
19 @event = event
20 _tags = tags.is_a?(Pluggaloid::HandlerTag) ? [tags] : Array(tags)
21 _tags.each{|t| raise "#{t} is not a Pluggaloid::HandlerTag" unless t.is_a?(Pluggaloid::HandlerTag) }
22 @tags = Set.new(_tags).freeze
23 end
24
25 def add_tag(tag)
26 raise Pluggaloid::TypeError, "Argument `tag' must be instance of Pluggaloid::HandlerTag, but given #{tag.class}." unless tag.is_a? Pluggaloid::HandlerTag
27 Lock.synchronize do
28 @tags = Set.new([tag, *@tags]).freeze
29 end
30 self
31 end
32
33 def remove_tag(tag)
34 Lock.synchronize do
35 @tags -= tag
36 @tags.freeze
37 end
38 self
39 end
40
41 def inspect
42 "#<#{self.class} event: #{@event.name.inspect}, slug: #{slug.inspect}, name: #{name.inspect}>"
43 end
44 end
0 # -*- coding: utf-8 -*-
1
2 require 'securerandom'
3
4 =begin rdoc
5 = リスナをまとめて管理するプラグイン
6
7 Pluggaloid::Listener や、 Pluggaloid::Filter をまとめて扱うための仕組み。
8 Pluggaloid::Plugin#add_event などの引数 _tags:_ に、このインスタンスを設定する。
9
10 == インスタンスの作成
11
12 Pluggaloid::Plugin#handler_tag を使って生成する。 Pluggaloid::HandlerTag の
13 _plugin:_ 引数には、レシーバ(Pluggaloid::Plugin)が渡される。
14 Pluggaloid::HandlerTag は、このプラグインの中でだけ使える。複数のプラグインのリスナ
15 をまとめて管理することはできない。
16
17 == リスナにタグをつける
18
19 Pluggaloid::Plugin#add_event または Pluggaloid::Plugin#add_event_filter の
20 _tags:_ 引数にこれのインスタンスを渡す。
21
22 == このタグがついたListenerやFilterを取得する
23
24 Enumerable をincludeしていて、リスナやフィルタを取得することができる。
25 また、
26 - Pluggaloid::HandlerTag#listeners で、 Pluggaloid::Listener だけ
27 - Pluggaloid::HandlerTag#filters で、 Pluggaloid::Filter だけ
28 を対象にした Enumerator を取得することができる
29
30 == このタグがついたリスナを全てdetachする
31
32 Pluggaloid::Plugin#detach の第一引数に Pluggaloid::HandlerTag の
33 インスタンスを渡すことで、そのHandlerTagがついたListener、Filterは全てデタッチ
34 される
35
36 =end
37 class Pluggaloid::HandlerTag < Pluggaloid::Identity
38 include Enumerable
39
40 # ==== Args
41 # [name:] タグの名前(String | nil)
42 def initialize(plugin:, **kwrest)
43 super(**kwrest)
44 @plugin = plugin
45 end
46
47 # このTagがついている Pluggaloid::Listener と Pluggaloid::Filter を全て列挙する
48 # ==== Return
49 # Enumerable
50 def each
51 if block_given?
52 Enumerator.new do |y|
53 listeners{|x| y << x }
54 filters{|x| y << x }
55 end.each(&Proc.new)
56 else
57 Enumerator.new do |y|
58 listeners{|x| y << x }
59 filters{|x| y << x }
60 end
61 end
62 end
63
64 # このTagがついている Pluggaloid::Listener を全て列挙する
65 # ==== Return
66 # Enumerable
67 def listeners
68 if block_given?
69 listeners.each(&Proc.new)
70 else
71 @plugin.to_enum(:listeners).lazy.select{|l| l.tags.include?(self) }
72 end
73 end
74
75 # このTagがついている Pluggaloid::Filter を全て列挙する
76 # ==== Return
77 # Enumerable
78 def filters
79 if block_given?
80 filters.each(&Proc.new)
81 else
82 @plugin.to_enum(:filters).lazy.select{|l| l.tags.include?(self) }
83 end
84 end
85 end
0 # -*- coding: utf-8 -*-
1
2 =begin rdoc
3 slugと名前をもつオブジェクト。
4 これの参照を直接持たずとも、slugで一意に参照したり、表示名を設定することができる
5 =end
6 class Pluggaloid::Identity
7 attr_reader :name, :slug
8
9 # ==== Args
10 # [name:] 名前(String | nil)
11 # [slug:] ハンドラスラッグ(Symbol | nil)
12 def initialize(slug: SecureRandom.uuid, name: slug)
13 @name = name.to_s.freeze
14 @slug = slug.to_sym
15 end
16
17 def inspect
18 "#<#{self.class} slug: #{slug.inspect}, name: #{name.inspect}>"
19 end
20 end
0 # -*- coding: utf-8 -*-
1 require 'securerandom'
2 require 'set'
3
4 class Pluggaloid::Listener < Pluggaloid::Handler
5 # プラグインコールバックをこれ以上実行しない。
6 def self.cancel!
7 throw :plugin_exit, false end
8
9 # ==== Args
10 # [event] 監視するEventのインスタンス
11 # [name:] 名前(String | nil)
12 # [slug:] イベントリスナスラッグ(Symbol | nil)
13 # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ
14 # [&callback] コールバック
15 def initialize(event, **kwrest, &callback)
16 super
17 @callback = Proc.new
18 event.add_listener(self) end
19
20 # イベントを実行する
21 # ==== Args
22 # [*args] イベントの引数
23 def call(*args)
24 @callback.call(*args, &self.class.method(:cancel!)) end
25
26 # このリスナを削除する
27 # ==== Return
28 # self
29 def detach
30 @event.delete_listener(self)
31 self end
32 end
0 # -*- coding: utf-8 -*-
1
2 require 'instance_storage'
3 require 'delayer'
4 require 'securerandom'
5 require 'set'
6
7 # プラグインの本体。
8 # DSLを提供し、イベントやフィルタの管理をする
9 module Pluggaloid
10 class Plugin
11 include InstanceStorage
12
13 class << self
14 attr_writer :vm
15
16 def vm
17 @vm ||= begin
18 raise Pluggaloid::NoDefaultDelayerError, "Default Delayer was not set." unless Delayer.default
19 vm = Pluggaloid::VM.new(
20 Delayer.default,
21 self,
22 Pluggaloid::Event,
23 Pluggaloid::Listener,
24 Pluggaloid::Filter,
25 Pluggaloid::HandlerTag)
26 vm.Event.vm = vm end end
27
28 # プラグインのインスタンスを返す。
29 # ブロックが渡された場合、そのブロックをプラグインのインスタンスのスコープで実行する
30 # ==== Args
31 # [plugin_name] プラグイン名
32 # ==== Return
33 # Plugin
34 def create(plugin_name, &body)
35 self[plugin_name].instance_eval(&body) if body
36 self[plugin_name] end
37
38 # イベントを宣言する。
39 # ==== Args
40 # [event_name] イベント名
41 # [options] 以下のキーを持つHash
42 # :prototype :: 引数の数と型。Arrayで、type_strictが解釈できる条件を設定する
43 # :priority :: Delayerの優先順位
44 def defevent(event_name, options = {})
45 vm.Event[event_name].options = options end
46
47 # イベント _event_name_ を発生させる
48 # ==== Args
49 # [event_name] イベント名
50 # [*args] イベントの引数
51 # ==== Return
52 # Delayer
53 def call(event_name, *args)
54 vm.Event[event_name].call(*args) end
55
56 # 引数 _args_ をフィルタリングした結果を返す
57 # ==== Args
58 # [*args] 引数
59 # ==== Return
60 # フィルタされた引数の配列
61 def filtering(event_name, *args)
62 vm.Event[event_name].filtering(*args) end
63
64 # 互換性のため
65 def uninstall(plugin_name)
66 self[plugin_name].uninstall end
67
68 # 互換性のため
69 def filter_cancel!
70 vm.Filter.cancel! end
71
72 alias plugin_list instances_name
73
74 alias __clear_aF4e__ clear!
75 def clear!
76 if defined?(@vm) and @vm
77 @vm.Event.clear!
78 @vm = nil end
79 __clear_aF4e__() end
80 end
81
82 # プラグインの名前
83 attr_reader :name
84
85 # spec
86 attr_accessor :spec
87
88 # 最初にプラグインがロードされた時刻(uninstallされるとリセットする)
89 attr_reader :defined_time
90
91 # ==== Args
92 # [plugin_name] プラグイン名
93 def initialize(*args)
94 super
95 @defined_time = Time.new.freeze
96 @events = Set.new
97 @filters = Set.new
98 end
99
100 # イベントリスナを新しく登録する
101 # ==== Args
102 # [event] 監視するEventのインスタンス
103 # [name:] 名前(String | nil)
104 # [slug:] イベントリスナスラッグ(Symbol | nil)
105 # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ
106 # [&callback] コールバック
107 # ==== Return
108 # Pluggaloid::Listener
109 def add_event(event_name, **kwrest, &callback)
110 result = vm.Listener.new(vm.Event[event_name], **kwrest, &callback)
111 @events << result
112 result end
113
114 # イベントフィルタを新しく登録する
115 # ==== Args
116 # [event] 監視するEventのインスタンス
117 # [name:] 名前(String | nil)
118 # [slug:] フィルタスラッグ(Symbol | nil)
119 # [tags:] Pluggaloid::HandlerTag|Array フィルタのタグ
120 # [&callback] コールバック
121 # ==== Return
122 # Pluggaloid::Filter
123 def add_event_filter(event_name, **kwrest, &callback)
124 result = vm.Filter.new(vm.Event[event_name], **kwrest, &callback)
125 @filters << result
126 result end
127
128 # このプラグインのHandlerTagを作る。
129 # ブロックが渡された場合は、ブロックの中を実行し、ブロックの中で定義された
130 # Handler全てにTagを付与する。
131 # ==== Args
132 # [slug] スラッグ
133 # [name] タグ名
134 # ==== Return
135 # Pluggaloid::HandlerTag
136 def handler_tag(slug=SecureRandom.uuid, name=slug)
137 tag = case slug
138 when String, Symbol
139 vm.HandlerTag.new(slug: slug.to_sym, name: name.to_s, plugin: self)
140 when vm.HandlerTag
141 slug
142 else
143 raise Pluggaloid::TypeError, "Argument `slug' must be instance of Symbol, String or Pluggaloid::HandlerTag, but given #{slug.class}."
144 end
145 if block_given?
146 handlers = @events + @filters
147 yield tag
148 (@events + @filters - handlers).each do |handler|
149 handler.add_tag(tag)
150 end
151 else
152 tag
153 end
154 end
155
156 # イベントリスナを列挙する
157 # ==== Return
158 # Set of Pluggaloid::Listener
159 def listeners
160 if block_given?
161 @events.each(&Proc.new)
162 else
163 @events.dup
164 end
165 end
166
167 # フィルタを列挙する
168 # ==== Return
169 # Set of Pluggaloid::Filter
170 def filters
171 if block_given?
172 @filters.each(&Proc.new)
173 else
174 @filters.dup
175 end
176 end
177
178
179 # イベントを削除する。
180 # 引数は、Pluggaloid::ListenerかPluggaloid::Filterのみ(on_*やfilter_*の戻り値)。
181 # 互換性のため、二つ引数がある場合は第一引数は無視され、第二引数が使われる。
182 # ==== Args
183 # [*args] 引数
184 # ==== Return
185 # self
186 def detach(*args)
187 listener = args.last
188 case listener
189 when vm.Listener
190 @events.delete(listener)
191 listener.detach
192 when vm.Filter
193 @filters.delete(listener)
194 listener.detach
195 when Enumerable
196 listener.each(&method(:detach))
197 else
198 raise ArgumentError, "Argument must be Pluggaloid::Listener, Pluggaloid::Filter, Pluggaloid::HandlerTag or Enumerable. But given #{listener.class}."
199 end
200 self end
201
202 # このプラグインを破棄する
203 # ==== Return
204 # self
205 def uninstall
206 vm.Event[:unload].call(self.name)
207 vm.Delayer.new do
208 @events.map(&:detach)
209 @filters.map(&:detach)
210 self.class.destroy name
211 end
212 self end
213
214 # イベント _event_name_ を宣言する
215 # ==== Args
216 # [event_name] イベント名
217 # [options] イベントの定義
218 def defevent(event_name, options={})
219 vm.Event[event_name].options.merge!({plugin: self}.merge(options)) end
220
221 # DSLメソッドを新しく追加する。
222 # 追加されたメソッドは呼ぶと &callback が呼ばれ、その戻り値が返される。引数も順番通り全て &callbackに渡される
223 # ==== Args
224 # [dsl_name] 新しく追加するメソッド名
225 # [&callback] 実行されるメソッド
226 # ==== Return
227 # self
228 def defdsl(dsl_name, &callback)
229 self.class.instance_eval {
230 define_method(dsl_name, &callback) }
231 self end
232
233 # プラグインが Plugin.uninstall される時に呼ばれるブロックを登録する。
234 def onunload
235 callback = Proc.new
236 add_event(:unload) do |plugin_slug|
237 if plugin_slug == self.name
238 callback.call
239 end
240 end
241 end
242 alias :on_unload :onunload
243
244 # マジックメソッドを追加する。
245 # on_?name :: add_event(name)
246 # filter_?name :: add_event_filter(name)
247 def method_missing(method, *args, **kwrest, &proc)
248 case method.to_s
249 when /\Aon_?(.+)\Z/
250 add_event($1.to_sym, *args, **kwrest, &proc)
251 when /\Afilter_?(.+)\Z/
252 add_event_filter($1.to_sym, **kwrest, &proc)
253 when /\Ahook_?(.+)\Z/
254 add_event_hook($1.to_sym, &proc)
255 else
256 super end end
257
258 private
259
260 def vm
261 self.class.vm end
262
263 end
264 end
0 module Pluggaloid
1 VERSION = "1.1.1"
2 end
0 require "pluggaloid/version"
1 require "pluggaloid/plugin"
2 require 'pluggaloid/event'
3 require "pluggaloid/identity"
4 require "pluggaloid/handler"
5 require 'pluggaloid/listener'
6 require 'pluggaloid/filter'
7 require "pluggaloid/handler_tag"
8 require 'pluggaloid/error'
9
10 require 'delayer'
11
12 module Pluggaloid
13 VM = Struct.new(*%i<Delayer Plugin Event Listener Filter HandlerTag>)
14
15 def self.new(delayer)
16 vm = VM.new(delayer,
17 Class.new(Plugin),
18 Class.new(Event),
19 Class.new(Listener),
20 Class.new(Filter),
21 Class.new(HandlerTag))
22 vm.Plugin.vm = vm.Event.vm = vm
23 end
24 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 module PublicSuffix
7
8 # Domain represents a domain name, composed by a TLD, SLD and TRD.
9 class Domain
10
11 # Splits a string into the labels, that is the dot-separated parts.
12 #
13 # The input is not validated, but it is assumed to be a valid domain name.
14 #
15 # @example
16 #
17 # name_to_labels('example.com')
18 # # => ['example', 'com']
19 #
20 # name_to_labels('example.co.uk')
21 # # => ['example', 'co', 'uk']
22 #
23 # @param name [String, #to_s] The domain name to split.
24 # @return [Array<String>]
25 def self.name_to_labels(name)
26 name.to_s.split(DOT)
27 end
28
29
30 attr_reader :tld, :sld, :trd
31
32 # Creates and returns a new {PublicSuffix::Domain} instance.
33 #
34 # @overload initialize(tld)
35 # Initializes with a +tld+.
36 # @param [String] tld The TLD (extension)
37 # @overload initialize(tld, sld)
38 # Initializes with a +tld+ and +sld+.
39 # @param [String] tld The TLD (extension)
40 # @param [String] sld The TRD (domain)
41 # @overload initialize(tld, sld, trd)
42 # Initializes with a +tld+, +sld+ and +trd+.
43 # @param [String] tld The TLD (extension)
44 # @param [String] sld The SLD (domain)
45 # @param [String] tld The TRD (subdomain)
46 #
47 # @yield [self] Yields on self.
48 # @yieldparam [PublicSuffix::Domain] self The newly creates instance
49 #
50 # @example Initialize with a TLD
51 # PublicSuffix::Domain.new("com")
52 # # => #<PublicSuffix::Domain @tld="com">
53 #
54 # @example Initialize with a TLD and SLD
55 # PublicSuffix::Domain.new("com", "example")
56 # # => #<PublicSuffix::Domain @tld="com", @trd=nil>
57 #
58 # @example Initialize with a TLD, SLD and TRD
59 # PublicSuffix::Domain.new("com", "example", "wwww")
60 # # => #<PublicSuffix::Domain @tld="com", @trd=nil, @sld="example">
61 #
62 def initialize(*args)
63 @tld, @sld, @trd = args
64 yield(self) if block_given?
65 end
66
67 # Returns a string representation of this object.
68 #
69 # @return [String]
70 def to_s
71 name
72 end
73
74 # Returns an array containing the domain parts.
75 #
76 # @return [Array<String, nil>]
77 #
78 # @example
79 #
80 # PublicSuffix::Domain.new("google.com").to_a
81 # # => [nil, "google", "com"]
82 #
83 # PublicSuffix::Domain.new("www.google.com").to_a
84 # # => [nil, "google", "com"]
85 #
86 def to_a
87 [@trd, @sld, @tld]
88 end
89
90 # Returns the full domain name.
91 #
92 # @return [String]
93 #
94 # @example Gets the domain name of a domain
95 # PublicSuffix::Domain.new("com", "google").name
96 # # => "google.com"
97 #
98 # @example Gets the domain name of a subdomain
99 # PublicSuffix::Domain.new("com", "google", "www").name
100 # # => "www.google.com"
101 #
102 def name
103 [@trd, @sld, @tld].compact.join(DOT)
104 end
105
106 # Returns a domain-like representation of this object
107 # if the object is a {#domain?}, <tt>nil</tt> otherwise.
108 #
109 # PublicSuffix::Domain.new("com").domain
110 # # => nil
111 #
112 # PublicSuffix::Domain.new("com", "google").domain
113 # # => "google.com"
114 #
115 # PublicSuffix::Domain.new("com", "google", "www").domain
116 # # => "www.google.com"
117 #
118 # This method doesn't validate the input. It handles the domain
119 # as a valid domain name and simply applies the necessary transformations.
120 #
121 # This method returns a FQD, not just the domain part.
122 # To get the domain part, use <tt>#sld</tt> (aka second level domain).
123 #
124 # PublicSuffix::Domain.new("com", "google", "www").domain
125 # # => "google.com"
126 #
127 # PublicSuffix::Domain.new("com", "google", "www").sld
128 # # => "google"
129 #
130 # @see #domain?
131 # @see #subdomain
132 #
133 # @return [String]
134 def domain
135 [@sld, @tld].join(DOT) if domain?
136 end
137
138 # Returns a subdomain-like representation of this object
139 # if the object is a {#subdomain?}, <tt>nil</tt> otherwise.
140 #
141 # PublicSuffix::Domain.new("com").subdomain
142 # # => nil
143 #
144 # PublicSuffix::Domain.new("com", "google").subdomain
145 # # => nil
146 #
147 # PublicSuffix::Domain.new("com", "google", "www").subdomain
148 # # => "www.google.com"
149 #
150 # This method doesn't validate the input. It handles the domain
151 # as a valid domain name and simply applies the necessary transformations.
152 #
153 # This method returns a FQD, not just the subdomain part.
154 # To get the subdomain part, use <tt>#trd</tt> (aka third level domain).
155 #
156 # PublicSuffix::Domain.new("com", "google", "www").subdomain
157 # # => "www.google.com"
158 #
159 # PublicSuffix::Domain.new("com", "google", "www").trd
160 # # => "www"
161 #
162 # @see #subdomain?
163 # @see #domain
164 #
165 # @return [String]
166 def subdomain
167 [@trd, @sld, @tld].join(DOT) if subdomain?
168 end
169
170 # Checks whether <tt>self</tt> looks like a domain.
171 #
172 # This method doesn't actually validate the domain.
173 # It only checks whether the instance contains
174 # a value for the {#tld} and {#sld} attributes.
175 #
176 # @example
177 #
178 # PublicSuffix::Domain.new("com").domain?
179 # # => false
180 #
181 # PublicSuffix::Domain.new("com", "google").domain?
182 # # => true
183 #
184 # PublicSuffix::Domain.new("com", "google", "www").domain?
185 # # => true
186 #
187 # # This is an invalid domain, but returns true
188 # # because this method doesn't validate the content.
189 # PublicSuffix::Domain.new("com", nil).domain?
190 # # => true
191 #
192 # @see #subdomain?
193 #
194 # @return [Boolean]
195 def domain?
196 !(@tld.nil? || @sld.nil?)
197 end
198
199 # Checks whether <tt>self</tt> looks like a subdomain.
200 #
201 # This method doesn't actually validate the subdomain.
202 # It only checks whether the instance contains
203 # a value for the {#tld}, {#sld} and {#trd} attributes.
204 # If you also want to validate the domain,
205 # use {#valid_subdomain?} instead.
206 #
207 # @example
208 #
209 # PublicSuffix::Domain.new("com").subdomain?
210 # # => false
211 #
212 # PublicSuffix::Domain.new("com", "google").subdomain?
213 # # => false
214 #
215 # PublicSuffix::Domain.new("com", "google", "www").subdomain?
216 # # => true
217 #
218 # # This is an invalid domain, but returns true
219 # # because this method doesn't validate the content.
220 # PublicSuffix::Domain.new("com", "example", nil).subdomain?
221 # # => true
222 #
223 # @see #domain?
224 #
225 # @return [Boolean]
226 def subdomain?
227 !(@tld.nil? || @sld.nil? || @trd.nil?)
228 end
229
230 end
231
232 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 module PublicSuffix
7
8 class Error < StandardError
9 end
10
11 # Raised when trying to parse an invalid name.
12 # A name is considered invalid when no rule is found in the definition list.
13 #
14 # @example
15 #
16 # PublicSuffix.parse("nic.test")
17 # # => PublicSuffix::DomainInvalid
18 #
19 # PublicSuffix.parse("http://www.nic.it")
20 # # => PublicSuffix::DomainInvalid
21 #
22 class DomainInvalid < Error
23 end
24
25 # Raised when trying to parse a name that matches a suffix.
26 #
27 # @example
28 #
29 # PublicSuffix.parse("nic.do")
30 # # => PublicSuffix::DomainNotAllowed
31 #
32 # PublicSuffix.parse("www.nic.do")
33 # # => PublicSuffix::Domain
34 #
35 class DomainNotAllowed < DomainInvalid
36 end
37
38 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 module PublicSuffix
7
8 # A {PublicSuffix::List} is a collection of one
9 # or more {PublicSuffix::Rule}.
10 #
11 # Given a {PublicSuffix::List},
12 # you can add or remove {PublicSuffix::Rule},
13 # iterate all items in the list or search for the first rule
14 # which matches a specific domain name.
15 #
16 # # Create a new list
17 # list = PublicSuffix::List.new
18 #
19 # # Push two rules to the list
20 # list << PublicSuffix::Rule.factory("it")
21 # list << PublicSuffix::Rule.factory("com")
22 #
23 # # Get the size of the list
24 # list.size
25 # # => 2
26 #
27 # # Search for the rule matching given domain
28 # list.find("example.com")
29 # # => #<PublicSuffix::Rule::Normal>
30 # list.find("example.org")
31 # # => nil
32 #
33 # You can create as many {PublicSuffix::List} you want.
34 # The {PublicSuffix::List.default} rule list is used
35 # to tokenize and validate a domain.
36 #
37 class List
38
39 DEFAULT_LIST_PATH = File.expand_path("../../data/list.txt", __dir__)
40
41 # Gets the default rule list.
42 #
43 # Initializes a new {PublicSuffix::List} parsing the content
44 # of {PublicSuffix::List.default_list_content}, if required.
45 #
46 # @return [PublicSuffix::List]
47 def self.default(**options)
48 @default ||= parse(File.read(DEFAULT_LIST_PATH), options)
49 end
50
51 # Sets the default rule list to +value+.
52 #
53 # @param value [PublicSuffix::List] the new list
54 # @return [PublicSuffix::List]
55 def self.default=(value)
56 @default = value
57 end
58
59 # Parse given +input+ treating the content as Public Suffix List.
60 #
61 # See http://publicsuffix.org/format/ for more details about input format.
62 #
63 # @param string [#each_line] the list to parse
64 # @param private_domains [Boolean] whether to ignore the private domains section
65 # @return [PublicSuffix::List]
66 def self.parse(input, private_domains: true)
67 comment_token = "//".freeze
68 private_token = "===BEGIN PRIVATE DOMAINS===".freeze
69 section = nil # 1 == ICANN, 2 == PRIVATE
70
71 new do |list|
72 input.each_line do |line|
73 line.strip!
74 case # rubocop:disable Style/EmptyCaseCondition
75
76 # skip blank lines
77 when line.empty?
78 next
79
80 # include private domains or stop scanner
81 when line.include?(private_token)
82 break if !private_domains
83 section = 2
84
85 # skip comments
86 when line.start_with?(comment_token)
87 next
88
89 else
90 list.add(Rule.factory(line, private: section == 2))
91
92 end
93 end
94 end
95 end
96
97
98 # Initializes an empty {PublicSuffix::List}.
99 #
100 # @yield [self] Yields on self.
101 # @yieldparam [PublicSuffix::List] self The newly created instance.
102 def initialize
103 @rules = {}
104 yield(self) if block_given?
105 end
106
107
108 # Checks whether two lists are equal.
109 #
110 # List <tt>one</tt> is equal to <tt>two</tt>, if <tt>two</tt> is an instance of
111 # {PublicSuffix::List} and each +PublicSuffix::Rule::*+
112 # in list <tt>one</tt> is available in list <tt>two</tt>, in the same order.
113 #
114 # @param other [PublicSuffix::List] the List to compare
115 # @return [Boolean]
116 def ==(other)
117 return false unless other.is_a?(List)
118 equal?(other) || @rules == other.rules
119 end
120 alias eql? ==
121
122 # Iterates each rule in the list.
123 def each(&block)
124 Enumerator.new do |y|
125 @rules.each do |key, node|
126 y << entry_to_rule(node, key)
127 end
128 end.each(&block)
129 end
130
131
132 # Adds the given object to the list and optionally refreshes the rule index.
133 #
134 # @param rule [PublicSuffix::Rule::*] the rule to add to the list
135 # @return [self]
136 def add(rule)
137 @rules[rule.value] = rule_to_entry(rule)
138 self
139 end
140 alias << add
141
142 # Gets the number of rules in the list.
143 #
144 # @return [Integer]
145 def size
146 @rules.size
147 end
148
149 # Checks whether the list is empty.
150 #
151 # @return [Boolean]
152 def empty?
153 @rules.empty?
154 end
155
156 # Removes all rules.
157 #
158 # @return [self]
159 def clear
160 @rules.clear
161 self
162 end
163
164 # Finds and returns the rule corresponding to the longest public suffix for the hostname.
165 #
166 # @param name [#to_s] the hostname
167 # @param default [PublicSuffix::Rule::*] the default rule to return in case no rule matches
168 # @return [PublicSuffix::Rule::*]
169 def find(name, default: default_rule, **options)
170 rule = select(name, **options).inject do |l, r|
171 return r if r.class == Rule::Exception
172 l.length > r.length ? l : r
173 end
174 rule || default
175 end
176
177 # Selects all the rules matching given hostame.
178 #
179 # If `ignore_private` is set to true, the algorithm will skip the rules that are flagged as
180 # private domain. Note that the rules will still be part of the loop.
181 # If you frequently need to access lists ignoring the private domains,
182 # you should create a list that doesn't include these domains setting the
183 # `private_domains: false` option when calling {.parse}.
184 #
185 # Note that this method is currently private, as you should not rely on it. Instead,
186 # the public interface is {#find}. The current internal algorithm allows to return all
187 # matching rules, but different data structures may not be able to do it, and instead would
188 # return only the match. For this reason, you should rely on {#find}.
189 #
190 # @param name [#to_s] the hostname
191 # @param ignore_private [Boolean]
192 # @return [Array<PublicSuffix::Rule::*>]
193 def select(name, ignore_private: false)
194 name = name.to_s
195
196 parts = name.split(DOT).reverse!
197 index = 0
198 query = parts[index]
199 rules = []
200
201 loop do
202 match = @rules[query]
203 if !match.nil? && (ignore_private == false || match.private == false)
204 rules << entry_to_rule(match, query)
205 end
206
207 index += 1
208 break if index >= parts.size
209 query = parts[index] + DOT + query
210 end
211
212 rules
213 end
214 private :select # rubocop:disable Style/AccessModifierDeclarations
215
216 # Gets the default rule.
217 #
218 # @see PublicSuffix::Rule.default_rule
219 #
220 # @return [PublicSuffix::Rule::*]
221 def default_rule
222 PublicSuffix::Rule.default
223 end
224
225
226 protected
227
228 attr_reader :rules
229
230
231 private
232
233 def entry_to_rule(entry, value)
234 entry.type.new(value: value, length: entry.length, private: entry.private)
235 end
236
237 def rule_to_entry(rule)
238 Rule::Entry.new(rule.class, rule.length, rule.private)
239 end
240
241 end
242 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 module PublicSuffix
7
8 # A Rule is a special object which holds a single definition
9 # of the Public Suffix List.
10 #
11 # There are 3 types of rules, each one represented by a specific
12 # subclass within the +PublicSuffix::Rule+ namespace.
13 #
14 # To create a new Rule, use the {PublicSuffix::Rule#factory} method.
15 #
16 # PublicSuffix::Rule.factory("ar")
17 # # => #<PublicSuffix::Rule::Normal>
18 #
19 module Rule
20
21 # @api internal
22 Entry = Struct.new(:type, :length, :private)
23
24 # = Abstract rule class
25 #
26 # This represent the base class for a Rule definition
27 # in the {Public Suffix List}[https://publicsuffix.org].
28 #
29 # This is intended to be an Abstract class
30 # and you shouldn't create a direct instance. The only purpose
31 # of this class is to expose a common interface
32 # for all the available subclasses.
33 #
34 # * {PublicSuffix::Rule::Normal}
35 # * {PublicSuffix::Rule::Exception}
36 # * {PublicSuffix::Rule::Wildcard}
37 #
38 # ## Properties
39 #
40 # A rule is composed by 4 properties:
41 #
42 # value - A normalized version of the rule name.
43 # The normalization process depends on rule tpe.
44 #
45 # Here's an example
46 #
47 # PublicSuffix::Rule.factory("*.google.com")
48 # #<PublicSuffix::Rule::Wildcard:0x1015c14b0
49 # @value="google.com"
50 # >
51 #
52 # ## Rule Creation
53 #
54 # The best way to create a new rule is passing the rule name
55 # to the <tt>PublicSuffix::Rule.factory</tt> method.
56 #
57 # PublicSuffix::Rule.factory("com")
58 # # => PublicSuffix::Rule::Normal
59 #
60 # PublicSuffix::Rule.factory("*.com")
61 # # => PublicSuffix::Rule::Wildcard
62 #
63 # This method will detect the rule type and create an instance
64 # from the proper rule class.
65 #
66 # ## Rule Usage
67 #
68 # A rule describes the composition of a domain name and explains how to tokenize
69 # the name into tld, sld and trd.
70 #
71 # To use a rule, you first need to be sure the name you want to tokenize
72 # can be handled by the current rule.
73 # You can use the <tt>#match?</tt> method.
74 #
75 # rule = PublicSuffix::Rule.factory("com")
76 #
77 # rule.match?("google.com")
78 # # => true
79 #
80 # rule.match?("google.com")
81 # # => false
82 #
83 # Rule order is significant. A name can match more than one rule.
84 # See the {Public Suffix Documentation}[http://publicsuffix.org/format/]
85 # to learn more about rule priority.
86 #
87 # When you have the right rule, you can use it to tokenize the domain name.
88 #
89 # rule = PublicSuffix::Rule.factory("com")
90 #
91 # rule.decompose("google.com")
92 # # => ["google", "com"]
93 #
94 # rule.decompose("www.google.com")
95 # # => ["www.google", "com"]
96 #
97 # @abstract
98 #
99 class Base
100
101 # @return [String] the rule definition
102 attr_reader :value
103
104 # @return [String] the length of the rule
105 attr_reader :length
106
107 # @return [Boolean] true if the rule is a private domain
108 attr_reader :private
109
110
111 # Initializes a new rule from the content.
112 #
113 # @param content [String] the content of the rule
114 # @param private [Boolean]
115 def self.build(content, private: false)
116 new(value: content, private: private)
117 end
118
119 # Initializes a new rule.
120 #
121 # @param value [String]
122 # @param private [Boolean]
123 def initialize(value:, length: nil, private: false)
124 @value = value.to_s
125 @length = length || @value.count(DOT) + 1
126 @private = private
127 end
128
129 # Checks whether this rule is equal to <tt>other</tt>.
130 #
131 # @param [PublicSuffix::Rule::*] other The rule to compare
132 # @return [Boolean]
133 # Returns true if this rule and other are instances of the same class
134 # and has the same value, false otherwise.
135 def ==(other)
136 equal?(other) || (self.class == other.class && value == other.value)
137 end
138 alias eql? ==
139
140 # Checks if this rule matches +name+.
141 #
142 # A domain name is said to match a rule if and only if
143 # all of the following conditions are met:
144 #
145 # - When the domain and rule are split into corresponding labels,
146 # that the domain contains as many or more labels than the rule.
147 # - Beginning with the right-most labels of both the domain and the rule,
148 # and continuing for all labels in the rule, one finds that for every pair,
149 # either they are identical, or that the label from the rule is "*".
150 #
151 # @see https://publicsuffix.org/list/
152 #
153 # @example
154 # PublicSuffix::Rule.factory("com").match?("example.com")
155 # # => true
156 # PublicSuffix::Rule.factory("com").match?("example.net")
157 # # => false
158 #
159 # @param name [String] the domain name to check
160 # @return [Boolean]
161 def match?(name)
162 # Note: it works because of the assumption there are no
163 # rules like foo.*.com. If the assumption is incorrect,
164 # we need to properly walk the input and skip parts according
165 # to wildcard component.
166 diff = name.chomp(value)
167 diff.empty? || diff.end_with?(DOT)
168 end
169
170 # @abstract
171 def parts
172 raise NotImplementedError
173 end
174
175 # @abstract
176 # @param [String, #to_s] name The domain name to decompose
177 # @return [Array<String, nil>]
178 def decompose(*)
179 raise NotImplementedError
180 end
181
182 end
183
184 # Normal represents a standard rule (e.g. com).
185 class Normal < Base
186
187 # Gets the original rule definition.
188 #
189 # @return [String] The rule definition.
190 def rule
191 value
192 end
193
194 # Decomposes the domain name according to rule properties.
195 #
196 # @param [String, #to_s] name The domain name to decompose
197 # @return [Array<String>] The array with [trd + sld, tld].
198 def decompose(domain)
199 suffix = parts.join('\.')
200 matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
201 matches ? matches[1..2] : [nil, nil]
202 end
203
204 # dot-split rule value and returns all rule parts
205 # in the order they appear in the value.
206 #
207 # @return [Array<String>]
208 def parts
209 @value.split(DOT)
210 end
211
212 end
213
214 # Wildcard represents a wildcard rule (e.g. *.co.uk).
215 class Wildcard < Base
216
217 # Initializes a new rule from the content.
218 #
219 # @param content [String] the content of the rule
220 # @param private [Boolean]
221 def self.build(content, private: false)
222 new(value: content.to_s[2..-1], private: private)
223 end
224
225 # Initializes a new rule.
226 #
227 # @param value [String]
228 # @param private [Boolean]
229 def initialize(value:, length: nil, private: false)
230 super(value: value, length: length, private: private)
231 length or @length += 1 # * counts as 1
232 end
233
234 # Gets the original rule definition.
235 #
236 # @return [String] The rule definition.
237 def rule
238 value == "" ? STAR : STAR + DOT + value
239 end
240
241 # Decomposes the domain name according to rule properties.
242 #
243 # @param [String, #to_s] name The domain name to decompose
244 # @return [Array<String>] The array with [trd + sld, tld].
245 def decompose(domain)
246 suffix = ([".*?"] + parts).join('\.')
247 matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
248 matches ? matches[1..2] : [nil, nil]
249 end
250
251 # dot-split rule value and returns all rule parts
252 # in the order they appear in the value.
253 #
254 # @return [Array<String>]
255 def parts
256 @value.split(DOT)
257 end
258
259 end
260
261 # Exception represents an exception rule (e.g. !parliament.uk).
262 class Exception < Base
263
264 # Initializes a new rule from the content.
265 #
266 # @param content [String] the content of the rule
267 # @param private [Boolean]
268 def self.build(content, private: false)
269 new(value: content.to_s[1..-1], private: private)
270 end
271
272 # Gets the original rule definition.
273 #
274 # @return [String] The rule definition.
275 def rule
276 BANG + value
277 end
278
279 # Decomposes the domain name according to rule properties.
280 #
281 # @param [String, #to_s] name The domain name to decompose
282 # @return [Array<String>] The array with [trd + sld, tld].
283 def decompose(domain)
284 suffix = parts.join('\.')
285 matches = domain.to_s.match(/^(.*)\.(#{suffix})$/)
286 matches ? matches[1..2] : [nil, nil]
287 end
288
289 # dot-split rule value and returns all rule parts
290 # in the order they appear in the value.
291 # The leftmost label is not considered a label.
292 #
293 # See http://publicsuffix.org/format/:
294 # If the prevailing rule is a exception rule,
295 # modify it by removing the leftmost label.
296 #
297 # @return [Array<String>]
298 def parts
299 @value.split(DOT)[1..-1]
300 end
301
302 end
303
304
305 # Takes the +name+ of the rule, detects the specific rule class
306 # and creates a new instance of that class.
307 # The +name+ becomes the rule +value+.
308 #
309 # @example Creates a Normal rule
310 # PublicSuffix::Rule.factory("ar")
311 # # => #<PublicSuffix::Rule::Normal>
312 #
313 # @example Creates a Wildcard rule
314 # PublicSuffix::Rule.factory("*.ar")
315 # # => #<PublicSuffix::Rule::Wildcard>
316 #
317 # @example Creates an Exception rule
318 # PublicSuffix::Rule.factory("!congresodelalengua3.ar")
319 # # => #<PublicSuffix::Rule::Exception>
320 #
321 # @param [String] content The rule content.
322 # @return [PublicSuffix::Rule::*] A rule instance.
323 def self.factory(content, private: false)
324 case content.to_s[0, 1]
325 when STAR
326 Wildcard
327 when BANG
328 Exception
329 else
330 Normal
331 end.build(content, private: private)
332 end
333
334 # The default rule to use if no rule match.
335 #
336 # The default rule is "*". From https://publicsuffix.org/list/:
337 #
338 # > If no rules match, the prevailing rule is "*".
339 #
340 # @return [PublicSuffix::Rule::Wildcard] The default rule.
341 def self.default
342 factory(STAR)
343 end
344
345 end
346
347 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 module PublicSuffix
7 # The current library version.
8 VERSION = "3.0.3".freeze
9 end
0 # = Public Suffix
1 #
2 # Domain name parser based on the Public Suffix List.
3 #
4 # Copyright (c) 2009-2018 Simone Carletti <weppos@weppos.net>
5
6 require_relative "public_suffix/domain"
7 require_relative "public_suffix/version"
8 require_relative "public_suffix/errors"
9 require_relative "public_suffix/rule"
10 require_relative "public_suffix/list"
11
12 # PublicSuffix is a Ruby domain name parser based on the Public Suffix List.
13 #
14 # The [Public Suffix List](https://publicsuffix.org) is a cross-vendor initiative
15 # to provide an accurate list of domain name suffixes.
16 #
17 # The Public Suffix List is an initiative of the Mozilla Project,
18 # but is maintained as a community resource. It is available for use in any software,
19 # but was originally created to meet the needs of browser manufacturers.
20 module PublicSuffix
21
22 DOT = ".".freeze
23 BANG = "!".freeze
24 STAR = "*".freeze
25
26 # Parses +name+ and returns the {PublicSuffix::Domain} instance.
27 #
28 # @example Parse a valid domain
29 # PublicSuffix.parse("google.com")
30 # # => #<PublicSuffix::Domain:0x007fec2e51e588 @sld="google", @tld="com", @trd=nil>
31 #
32 # @example Parse a valid subdomain
33 # PublicSuffix.parse("www.google.com")
34 # # => #<PublicSuffix::Domain:0x007fec276d4cf8 @sld="google", @tld="com", @trd="www">
35 #
36 # @example Parse a fully qualified domain
37 # PublicSuffix.parse("google.com.")
38 # # => #<PublicSuffix::Domain:0x007fec257caf38 @sld="google", @tld="com", @trd=nil>
39 #
40 # @example Parse a fully qualified domain (subdomain)
41 # PublicSuffix.parse("www.google.com.")
42 # # => #<PublicSuffix::Domain:0x007fec27b6bca8 @sld="google", @tld="com", @trd="www">
43 #
44 # @example Parse an invalid (unlisted) domain
45 # PublicSuffix.parse("x.yz")
46 # # => #<PublicSuffix::Domain:0x007fec2f49bec0 @sld="x", @tld="yz", @trd=nil>
47 #
48 # @example Parse an invalid (unlisted) domain with strict checking (without applying the default * rule)
49 # PublicSuffix.parse("x.yz", default_rule: nil)
50 # # => PublicSuffix::DomainInvalid: `x.yz` is not a valid domain
51 #
52 # @example Parse an URL (not supported, only domains)
53 # PublicSuffix.parse("http://www.google.com")
54 # # => PublicSuffix::DomainInvalid: http://www.google.com is not expected to contain a scheme
55 #
56 #
57 # @param [String, #to_s] name The domain name or fully qualified domain name to parse.
58 # @param [PublicSuffix::List] list The rule list to search, defaults to the default {PublicSuffix::List}
59 # @param [Boolean] ignore_private
60 # @return [PublicSuffix::Domain]
61 #
62 # @raise [PublicSuffix::DomainInvalid]
63 # If domain is not a valid domain.
64 # @raise [PublicSuffix::DomainNotAllowed]
65 # If a rule for +domain+ is found, but the rule doesn't allow +domain+.
66 def self.parse(name, list: List.default, default_rule: list.default_rule, ignore_private: false)
67 what = normalize(name)
68 raise what if what.is_a?(DomainInvalid)
69
70 rule = list.find(what, default: default_rule, ignore_private: ignore_private)
71
72 # rubocop:disable Style/IfUnlessModifier
73 if rule.nil?
74 raise DomainInvalid, "`#{what}` is not a valid domain"
75 end
76 if rule.decompose(what).last.nil?
77 raise DomainNotAllowed, "`#{what}` is not allowed according to Registry policy"
78 end
79 # rubocop:enable Style/IfUnlessModifier
80
81 decompose(what, rule)
82 end
83
84 # Checks whether +domain+ is assigned and allowed, without actually parsing it.
85 #
86 # This method doesn't care whether domain is a domain or subdomain.
87 # The validation is performed using the default {PublicSuffix::List}.
88 #
89 # @example Validate a valid domain
90 # PublicSuffix.valid?("example.com")
91 # # => true
92 #
93 # @example Validate a valid subdomain
94 # PublicSuffix.valid?("www.example.com")
95 # # => true
96 #
97 # @example Validate a not-listed domain
98 # PublicSuffix.valid?("example.tldnotlisted")
99 # # => true
100 #
101 # @example Validate a not-listed domain with strict checking (without applying the default * rule)
102 # PublicSuffix.valid?("example.tldnotlisted")
103 # # => true
104 # PublicSuffix.valid?("example.tldnotlisted", default_rule: nil)
105 # # => false
106 #
107 # @example Validate a fully qualified domain
108 # PublicSuffix.valid?("google.com.")
109 # # => true
110 # PublicSuffix.valid?("www.google.com.")
111 # # => true
112 #
113 # @example Check an URL (which is not a valid domain)
114 # PublicSuffix.valid?("http://www.example.com")
115 # # => false
116 #
117 #
118 # @param [String, #to_s] name The domain name or fully qualified domain name to validate.
119 # @param [Boolean] ignore_private
120 # @return [Boolean]
121 def self.valid?(name, list: List.default, default_rule: list.default_rule, ignore_private: false)
122 what = normalize(name)
123 return false if what.is_a?(DomainInvalid)
124
125 rule = list.find(what, default: default_rule, ignore_private: ignore_private)
126
127 !rule.nil? && !rule.decompose(what).last.nil?
128 end
129
130 # Attempt to parse the name and returns the domain, if valid.
131 #
132 # This method doesn't raise. Instead, it returns nil if the domain is not valid for whatever reason.
133 #
134 # @param [String, #to_s] name The domain name or fully qualified domain name to parse.
135 # @param [PublicSuffix::List] list The rule list to search, defaults to the default {PublicSuffix::List}
136 # @param [Boolean] ignore_private
137 # @return [String]
138 def self.domain(name, **options)
139 parse(name, **options).domain
140 rescue PublicSuffix::Error
141 nil
142 end
143
144
145 # private
146
147 def self.decompose(name, rule)
148 left, right = rule.decompose(name)
149
150 parts = left.split(DOT)
151 # If we have 0 parts left, there is just a tld and no domain or subdomain
152 # If we have 1 part left, there is just a tld, domain and not subdomain
153 # If we have 2 parts left, the last part is the domain, the other parts (combined) are the subdomain
154 tld = right
155 sld = parts.empty? ? nil : parts.pop
156 trd = parts.empty? ? nil : parts.join(DOT)
157
158 Domain.new(tld, sld, trd)
159 end
160
161 # Pretend we know how to deal with user input.
162 def self.normalize(name)
163 name = name.to_s.dup
164 name.strip!
165 name.chomp!(DOT)
166 name.downcase!
167
168 return DomainInvalid.new("Name is blank") if name.empty?
169 return DomainInvalid.new("Name starts with a dot") if name.start_with?(DOT)
170 return DomainInvalid.new("%s is not expected to contain a scheme" % name) if name.include?("://")
171 name
172 end
173
174 end
0 # Convenience file to match gem name
1 require 'hmac'
0 # encoding: utf-8
1 #
2 # Ruby implementation of the Double Metaphone algorithm by Lawrence Philips,
3 # originally published in the June 2000 issue of C/C++ Users Journal.
4 #
5 # Based on Stephen Woodbridge's PHP version - http://swoodbridge.com/DoubleMetaPhone/
6 #
7 # Author: Tim Fletcher (mail@tfletcher.com)
8 #
9
10 module Text # :nodoc:
11 module Metaphone
12
13 # Returns the primary and secondary double metaphone tokens
14 # (the secondary will be nil if equal to the primary).
15 def double_metaphone(str)
16 primary, secondary, current = [], [], 0
17 original, length, last = "#{str} ".upcase, str.length, str.length - 1
18 if /^GN|KN|PN|WR|PS$/ =~ original[0, 2]
19 current += 1
20 end
21 if 'X' == original[0, 1]
22 primary << :S
23 secondary << :S
24 current += 1
25 end
26 while primary.length < 4 || secondary.length < 4
27 break if current > str.length
28 a, b, c = double_metaphone_lookup(original, current, length, last)
29 primary << a if a
30 secondary << b if b
31 current += c if c
32 end
33 primary, secondary = primary.join("")[0, 4], secondary.join("")[0, 4]
34 return primary, (primary == secondary ? nil : secondary)
35 end
36
37
38 private
39
40 def slavo_germanic?(str)
41 /W|K|CZ|WITZ/ =~ str
42 end
43
44 def vowel?(str)
45 /^A|E|I|O|U|Y$/ =~ str
46 end
47
48 def double_metaphone_lookup(str, pos, length, last)
49 case str[pos, 1]
50 when /^A|E|I|O|U|Y$/
51 if 0 == pos
52 return :A, :A, 1
53 else
54 return nil, nil, 1
55 end
56 when 'B'
57 return :P, :P, ('B' == str[pos + 1, 1] ? 2 : 1)
58 when 'Ç'
59 return :S, :S, 1
60 when 'C'
61 if pos > 1 &&
62 !vowel?(str[pos - 2, 1]) &&
63 'ACH' == str[pos - 1, 3] &&
64 str[pos + 2, 1] != 'I' && (
65 str[pos + 2, 1] != 'E' ||
66 str[pos - 2, 6] =~ /^(B|M)ACHER$/
67 ) then
68 return :K, :K, 2
69 elsif 0 == pos && 'CAESAR' == str[pos, 6]
70 return :S, :S, 2
71 elsif 'CHIA' == str[pos, 4]
72 return :K, :K, 2
73 elsif 'CH' == str[pos, 2]
74 if pos > 0 && 'CHAE' == str[pos, 4]
75 return :K, :X, 2
76 elsif 0 == pos && (
77 ['HARAC', 'HARIS'].include?(str[pos + 1, 5]) ||
78 ['HOR', 'HYM', 'HIA', 'HEM'].include?(str[pos + 1, 3])
79 ) && str[0, 5] != 'CHORE' then
80 return :K, :K, 2
81 elsif ['VAN ','VON '].include?(str[0, 4]) ||
82 'SCH' == str[0, 3] ||
83 ['ORCHES','ARCHIT','ORCHID'].include?(str[pos - 2, 6]) ||
84 ['T','S'].include?(str[pos + 2, 1]) || (
85 ((0 == pos) || ['A','O','U','E'].include?(str[pos - 1, 1])) &&
86 ['L','R','N','M','B','H','F','V','W',' '].include?(str[pos + 2, 1])
87 ) then
88 return :K, :K, 2
89 elsif pos > 0
90 return ('MC' == str[0, 2] ? 'K' : 'X'), 'K', 2
91 else
92 return :X, :X, 2
93 end
94 elsif 'CZ' == str[pos, 2] && 'WICZ' != str[pos - 2, 4]
95 return :S, :X, 2
96 elsif 'CIA' == str[pos + 1, 3]
97 return :X, :X, 3
98 elsif 'CC' == str[pos, 2] && !(1 == pos && 'M' == str[0, 1])
99 if /^I|E|H$/ =~ str[pos + 2, 1] && 'HU' != str[pos + 2, 2]
100 if (1 == pos && 'A' == str[pos - 1, 1]) ||
101 /^UCCE(E|S)$/ =~ str[pos - 1, 5] then
102 return :KS, :KS, 3
103 else
104 return :X, :X, 3
105 end
106 else
107 return :K, :K, 2
108 end
109 elsif /^C(K|G|Q)$/ =~ str[pos, 2]
110 return :K, :K, 2
111 elsif /^C(I|E|Y)$/ =~ str[pos, 2]
112 return :S, (/^CI(O|E|A)$/ =~ str[pos, 3] ? :X : :S), 2
113 else
114 if /^ (C|Q|G)$/ =~ str[pos + 1, 2]
115 return :K, :K, 3
116 else
117 return :K, :K, (/^C|K|Q$/ =~ str[pos + 1, 1] && !(['CE','CI'].include?(str[pos + 1, 2])) ? 2 : 1)
118 end
119 end
120 when 'D'
121 if 'DG' == str[pos, 2]
122 if /^I|E|Y$/ =~ str[pos + 2, 1]
123 return :J, :J, 3
124 else
125 return :TK, :TK, 2
126 end
127 else
128 return :T, :T, (/^D(T|D)$/ =~ str[pos, 2] ? 2 : 1)
129 end
130 when 'F'
131 return :F, :F, ('F' == str[pos + 1, 1] ? 2 : 1)
132 when 'G'
133 if 'H' == str[pos + 1, 1]
134 if pos > 0 && !vowel?(str[pos - 1, 1])
135 return :K, :K, 2
136 elsif 0 == pos
137 if 'I' == str[pos + 2, 1]
138 return :J, :J, 2
139 else
140 return :K, :K, 2
141 end
142 elsif (pos > 1 && /^B|H|D$/ =~ str[pos - 2, 1]) ||
143 (pos > 2 && /^B|H|D$/ =~ str[pos - 3, 1]) ||
144 (pos > 3 && /^B|H$/ =~ str[pos - 4, 1])
145 return nil, nil, 2
146 else
147 if (pos > 2 && 'U' == str[pos - 1, 1] && /^C|G|L|R|T$/ =~ str[pos - 3, 1])
148 return :F, :F, 2
149 elsif pos > 0 && 'I' != str[pos - 1, 1]
150 return :K, :K, 2
151 else
152 return nil, nil, 2
153 end
154 end
155 elsif 'N' == str[pos + 1, 1]
156 if 1 == pos && vowel?(str[0, 1]) && !slavo_germanic?(str)
157 return :KN, :N, 2
158 else
159 if 'EY' != str[pos + 2, 2] && 'Y' != str[pos + 1, 1] && !slavo_germanic?(str)
160 return :N, :KN, 2
161 else
162 return :KN, :KN, 2
163 end
164 end
165 elsif 'LI' == str[pos + 1, 2] && !slavo_germanic?(str)
166 return :KL, :L, 2
167 elsif 0 == pos && ('Y' == str[pos + 1, 1] || /^(E(S|P|B|L|Y|I|R)|I(B|L|N|E))$/ =~ str[pos + 1, 2])
168 return :K, :J, 2
169 elsif (('ER' == str[pos + 1, 2] || 'Y' == str[pos + 1, 1]) &&
170 /^(D|R|M)ANGER$/ !~ str[0, 6] &&
171 /^E|I$/ !~ str[pos - 1, 1] &&
172 /^(R|O)GY$/ !~ str[pos - 1, 3])
173 return :K, :J, 2
174 elsif /^E|I|Y$/ =~ str[pos + 1, 1] || /^(A|O)GGI$/ =~ str[pos - 1, 4]
175 if (/^V(A|O)N $/ =~ str[0, 4] || 'SCH' == str[0, 3]) || 'ET' == str[pos + 1, 2]
176 return :K, :K, 2
177 else
178 if 'IER ' == str[pos + 1, 4]
179 return :J, :J, 2
180 else
181 return :J, :K, 2
182 end
183 end
184 elsif 'G' == str[pos + 1, 1]
185 return :K, :K, 2
186 else
187 return :K, :K, 1
188 end
189 when 'H'
190 if (0 == pos || vowel?(str[pos - 1, 1])) && vowel?(str[pos + 1, 1])
191 return :H, :H, 2
192 else
193 return nil, nil, 1
194 end
195 when 'J'
196 if 'JOSE' == str[pos, 4] || 'SAN ' == str[0, 4]
197 if (0 == pos && ' ' == str[pos + 4, 1]) || 'SAN ' == str[0, 4]
198 return :H, :H, 1
199 else
200 return :J, :H, 1
201 end
202 else
203 current = ('J' == str[pos + 1, 1] ? 2 : 1)
204
205 if 0 == pos && 'JOSE' != str[pos, 4]
206 return :J, :A, current
207 else
208 if vowel?(str[pos - 1, 1]) && !slavo_germanic?(str) && /^A|O$/ =~ str[pos + 1, 1]
209 return :J, :H, current
210 else
211 if last == pos
212 return :J, nil, current
213 else
214 if /^L|T|K|S|N|M|B|Z$/ !~ str[pos + 1, 1] && /^S|K|L$/ !~ str[pos - 1, 1]
215 return :J, :J, current
216 else
217 return nil, nil, current
218 end
219 end
220 end
221 end
222 end
223 when 'K'
224 return :K, :K, ('K' == str[pos + 1, 1] ? 2 : 1)
225 when 'L'
226 if 'L' == str[pos + 1, 1]
227 if (((length - 3) == pos && /^(ILL(O|A)|ALLE)$/ =~ str[pos - 1, 4]) ||
228 ((/^(A|O)S$/ =~ str[last - 1, 2] || /^A|O$/ =~ str[last, 1]) && 'ALLE' == str[pos - 1, 4]))
229 return :L, nil, 2
230 else
231 return :L, :L, 2
232 end
233 else
234 return :L, :L, 1
235 end
236 when 'M'
237 if ('UMB' == str[pos - 1, 3] &&
238 ((last - 1) == pos || 'ER' == str[pos + 2, 2])) || 'M' == str[pos + 1, 1]
239 return :M, :M, 2
240 else
241 return :M, :M, 1
242 end
243 when 'N'
244 return :N, :N, ('N' == str[pos + 1, 1] ? 2 : 1)
245 when 'Ñ'
246 return :N, :N, 1
247 when 'P'
248 if 'H' == str[pos + 1, 1]
249 return :F, :F, 2
250 else
251 return :P, :P, (/^P|B$/ =~ str[pos + 1, 1] ? 2 : 1)
252 end
253 when 'Q'
254 return :K, :K, ('Q' == str[pos + 1, 1] ? 2 : 1)
255 when 'R'
256 current = ('R' == str[pos + 1, 1] ? 2 : 1)
257
258 if last == pos && !slavo_germanic?(str) && 'IE' == str[pos - 2, 2] && /^M(E|A)$/ !~ str[pos - 4, 2]
259 return nil, :R, current
260 else
261 return :R, :R, current
262 end
263 when 'S'
264 if /^(I|Y)SL$/ =~ str[pos - 1, 3]
265 return nil, nil, 1
266 elsif 0 == pos && 'SUGAR' == str[pos, 5]
267 return :X, :S, 1
268 elsif 'SH' == str[pos, 2]
269 if /^H(EIM|OEK|OLM|OLZ)$/ =~ str[pos + 1, 4]
270 return :S, :S, 2
271 else
272 return :X, :X, 2
273 end
274 elsif /^SI(O|A)$/ =~ str[pos, 3] || 'SIAN' == str[pos, 4]
275 return :S, (slavo_germanic?(str) ? :S : :X), 3
276 elsif (0 == pos && /^M|N|L|W$/ =~ str[pos + 1, 1]) || 'Z' == str[pos + 1, 1]
277 return :S, :X, ('Z' == str[pos + 1, 1] ? 2 : 1)
278 elsif 'SC' == str[pos, 2]
279 if 'H' == str[pos + 2, 1]
280 if /^OO|ER|EN|UY|ED|EM$/ =~ str[pos + 3, 2]
281 return (/^E(R|N)$/ =~ str[pos + 3, 2] ? :X : :SK), :SK, 3
282 else
283 return :X, ((0 == pos && !vowel?(str[3, 1]) && ('W' != str[pos + 3, 1])) ? :S : :X), 3
284 end
285 elsif /^I|E|Y$/ =~ str[pos + 2, 1]
286 return :S, :S, 3
287 else
288 return :SK, :SK, 3
289 end
290 else
291 return (last == pos && /^(A|O)I$/ =~ str[pos - 2, 2] ? nil : 'S'), 'S', (/^S|Z$/ =~ str[pos + 1, 1] ? 2 : 1)
292 end
293 when 'T'
294 if 'TION' == str[pos, 4]
295 return :X, :X, 3
296 elsif /^T(IA|CH)$/ =~ str[pos, 3]
297 return :X, :X, 3
298 elsif 'TH' == str[pos, 2] || 'TTH' == str[pos, 3]
299 if /^(O|A)M$/ =~ str[pos + 2, 2] || /^V(A|O)N $/ =~ str[0, 4] || 'SCH' == str[0, 3]
300 return :T, :T, 2
301 else
302 return 0, :T, 2
303 end
304 else
305 return :T, :T, (/^T|D$/ =~ str[pos + 1, 1] ? 2 : 1)
306 end
307 when 'V'
308 return :F, :F, ('V' == str[pos + 1, 1] ? 2 : 1)
309 when 'W'
310 if 'WR' == str[pos, 2]
311 return :R, :R, 2
312 end
313 pri, sec = nil, nil
314
315 if 0 == pos && (vowel?(str[pos + 1, 1]) || 'WH' == str[pos, 2])
316 pri = :A
317 sec = vowel?(str[pos + 1, 1]) ? :F : :A
318 end
319
320 if (last == pos && vowel?(str[pos - 1, 1])) || 'SCH' == str[0, 3] ||
321 /^EWSKI|EWSKY|OWSKI|OWSKY$/ =~ str[pos - 1, 5]
322 return pri, "#{sec}F".intern, 1
323 elsif /^WI(C|T)Z$/ =~ str[pos, 4]
324 return "#{pri}TS".intern, "#{sec}FX".intern, 4
325 else
326 return pri, sec, 1
327 end
328 when 'X'
329 current = (/^C|X$/ =~ str[pos + 1, 1] ? 2 : 1)
330
331 if !(last == pos && (/^(I|E)AU$/ =~ str[pos - 3, 3] || /^(A|O)U$/ =~ str[pos - 2, 2]))
332 return :KS, :KS, current
333 else
334 return nil, nil, current
335 end
336 when 'Z'
337 if 'H' == str[pos + 1, 1]
338 return :J, :J, 2
339 else
340 current = ('Z' == str[pos + 1, 1] ? 2 : 1)
341
342 if /^Z(O|I|A)$/ =~ str[pos + 1, 2] || (slavo_germanic?(str) && (pos > 0 && 'T' != str[pos - 1, 1]))
343 return :S, :TS, current
344 else
345 return :S, :S, current
346 end
347 end
348 else
349 return nil, nil, 1
350 end
351 end # def double_metaphone_lookup
352
353 extend self
354
355 end # module Metaphone
356 end # module Text
0 #
1 # Levenshtein distance algorithm implementation for Ruby, with UTF-8 support.
2 #
3 # The Levenshtein distance is a measure of how similar two strings s and t are,
4 # calculated as the number of deletions/insertions/substitutions needed to
5 # transform s into t. The greater the distance, the more the strings differ.
6 #
7 # The Levenshtein distance is also sometimes referred to as the
8 # easier-to-pronounce-and-spell 'edit distance'.
9 #
10 # Author: Paul Battley (pbattley@gmail.com)
11 #
12
13 module Text # :nodoc:
14 module Levenshtein
15
16 # Calculate the Levenshtein distance between two strings +str1+ and +str2+.
17 #
18 # The optional argument max_distance can reduce the number of iterations by
19 # stopping if the Levenshtein distance exceeds this value. This increases
20 # performance where it is only necessary to compare the distance with a
21 # reference value instead of calculating the exact distance.
22 #
23 # The distance is calculated in terms of Unicode codepoints. Be aware that
24 # this algorithm does not perform normalisation: if there is a possibility
25 # of different normalised forms being used, normalisation should be performed
26 # beforehand.
27 #
28 def distance(str1, str2, max_distance = nil)
29 if max_distance
30 distance_with_maximum(str1, str2, max_distance)
31 else
32 distance_without_maximum(str1, str2)
33 end
34 end
35
36 private
37 def distance_with_maximum(str1, str2, max_distance) # :nodoc:
38 s = str1.encode(Encoding::UTF_8).unpack("U*")
39 t = str2.encode(Encoding::UTF_8).unpack("U*")
40
41 n = s.length
42 m = t.length
43 big_int = n * m
44
45 # Swap if necessary so that s is always the shorter of the two strings
46 s, t, n, m = t, s, m, n if m < n
47
48 # If the length difference is already greater than the max_distance, then
49 # there is nothing else to check
50 if (n - m).abs >= max_distance
51 return max_distance
52 end
53
54 return 0 if s == t
55 return m if n.zero?
56 return n if m.zero?
57
58 # The values necessary for our threshold are written; the ones after must
59 # be filled with large integers since the tailing member of the threshold
60 # window in the bottom array will run min across them
61 d = (m + 1).times.map { |i|
62 if i < m || i < max_distance + 1
63 i
64 else
65 big_int
66 end
67 }
68 x = nil
69 e = nil
70
71 n.times do |i|
72 # Since we're reusing arrays, we need to be sure to wipe the value left
73 # of the starting index; we don't have to worry about the value above the
74 # ending index as the arrays were initially filled with large integers
75 # and we progress to the right
76 if e.nil?
77 e = i + 1
78 else
79 e = big_int
80 end
81
82 diag_index = t.length - s.length + i
83
84 # If max_distance was specified, we can reduce second loop. So we set
85 # up our threshold window.
86 # See:
87 # Gusfield, Dan (1997). Algorithms on strings, trees, and sequences:
88 # computer science and computational biology.
89 # Cambridge, UK: Cambridge University Press. ISBN 0-521-58519-8.
90 # pp. 263–264.
91 min = i - max_distance - 1
92 min = 0 if min < 0
93 max = i + max_distance
94 max = m - 1 if max > m - 1
95
96 min.upto(max) do |j|
97 # If the diagonal value is already greater than the max_distance
98 # then we can safety return: the diagonal will never go lower again.
99 # See: http://www.levenshtein.net/
100 if j == diag_index && d[j] >= max_distance
101 return max_distance
102 end
103
104 cost = s[i] == t[j] ? 0 : 1
105 insertion = d[j + 1] + 1
106 deletion = e + 1
107 substitution = d[j] + cost
108 x = insertion < deletion ? insertion : deletion
109 x = substitution if substitution < x
110
111 d[j] = e
112 e = x
113 end
114 d[m] = x
115 end
116
117 if x > max_distance
118 return max_distance
119 else
120 return x
121 end
122 end
123
124 def distance_without_maximum(str1, str2) # :nodoc:
125 s = str1.encode(Encoding::UTF_8).unpack("U*")
126 t = str2.encode(Encoding::UTF_8).unpack("U*")
127
128 n = s.length
129 m = t.length
130
131 return m if n.zero?
132 return n if m.zero?
133
134 d = (0..m).to_a
135 x = nil
136
137 n.times do |i|
138 e = i + 1
139 m.times do |j|
140 cost = s[i] == t[j] ? 0 : 1
141 insertion = d[j + 1] + 1
142 deletion = e + 1
143 substitution = d[j] + cost
144 x = insertion < deletion ? insertion : deletion
145 x = substitution if substitution < x
146
147 d[j] = e
148 e = x
149 end
150 d[m] = x
151 end
152
153 return x
154 end
155
156 extend self
157 end
158 end
0 #
1 # An implementation of the Metaphone phonetic coding system in Ruby.
2 #
3 # Metaphone encodes names into a phonetic form such that similar-sounding names
4 # have the same or similar Metaphone encodings.
5 #
6 # The original system was described by Lawrence Philips in Computer Language
7 # Vol. 7 No. 12, December 1990, pp 39-43.
8 #
9 # As there are multiple implementations of Metaphone, each with their own
10 # quirks, I have based this on my interpretation of the algorithm specification.
11 # Even LP's original BASIC implementation appears to contain bugs (specifically
12 # with the handling of CC and MB), when compared to his explanation of the
13 # algorithm.
14 #
15 # I have also compared this implementation with that found in PHP's standard
16 # library, which appears to mimic the behaviour of LP's original BASIC
17 # implementation. For compatibility, these rules can also be used by passing
18 # :buggy=>true to the methods.
19 #
20 # Author: Paul Battley (pbattley@gmail.com)
21 #
22
23 module Text # :nodoc:
24 module Metaphone
25
26 module Rules # :nodoc:all
27
28 # Metaphone rules. These are simply applied in order.
29 #
30 STANDARD = [
31 # Regexp, replacement
32 [ /([bcdfhjklmnpqrstvwxyz])\1+/,
33 '\1' ], # Remove doubled consonants except g.
34 # [PHP] remove c from regexp.
35 [ /^ae/, 'E' ],
36 [ /^[gkp]n/, 'N' ],
37 [ /^wr/, 'R' ],
38 [ /^x/, 'S' ],
39 [ /^wh/, 'W' ],
40 [ /mb$/, 'M' ], # [PHP] remove $ from regexp.
41 [ /(?!^)sch/, 'SK' ],
42 [ /th/, '0' ],
43 [ /t?ch|sh/, 'X' ],
44 [ /c(?=ia)/, 'X' ],
45 [ /[st](?=i[ao])/, 'X' ],
46 [ /s?c(?=[iey])/, 'S' ],
47 [ /(ck?|q)/, 'K' ],
48 [ /dg(?=[iey])/, 'J' ],
49 [ /d/, 'T' ],
50 [ /g(?=h[^aeiou])/, '' ],
51 [ /gn(ed)?/, 'N' ],
52 [ /([^g]|^)g(?=[iey])/,
53 '\1J' ],
54 [ /g+/, 'K' ],
55 [ /ph/, 'F' ],
56 [ /([aeiou])h(?=\b|[^aeiou])/,
57 '\1' ],
58 [ /[wy](?![aeiou])/, '' ],
59 [ /z/, 'S' ],
60 [ /v/, 'F' ],
61 [ /(?!^)[aeiou]+/, '' ],
62 ]
63
64 # The rules for the 'buggy' alternate implementation used by PHP etc.
65 #
66 BUGGY = STANDARD.dup
67 BUGGY[0] = [ /([bdfhjklmnpqrstvwxyz])\1+/, '\1' ]
68 BUGGY[6] = [ /mb/, 'M' ]
69 end
70
71 # Returns the Metaphone representation of a string. If the string contains
72 # multiple words, each word in turn is converted into its Metaphone
73 # representation. Note that only the letters A-Z are supported, so any
74 # language-specific processing should be done beforehand.
75 #
76 # If the :buggy option is set, alternate 'buggy' rules are used.
77 #
78 def metaphone(str, options={})
79 return str.strip.split(/\s+/).map { |w| metaphone_word(w, options) }.join(' ')
80 end
81
82 private
83
84 def metaphone_word(w, options={})
85 # Normalise case and remove non-ASCII
86 s = w.downcase.gsub(/[^a-z]/, '')
87 # Apply the Metaphone rules
88 rules = options[:buggy] ? Rules::BUGGY : Rules::STANDARD
89 rules.each { |rx, rep| s.gsub!(rx, rep) }
90 return s.upcase
91 end
92
93 extend self
94
95 end
96 end
0 #
1 # This is the Porter Stemming algorithm, ported to Ruby from the
2 # version coded up in Perl. It's easy to follow against the rules
3 # in the original paper in:
4 #
5 # Porter, 1980, An algorithm for suffix stripping, Program, Vol. 14,
6 # no. 3, pp 130-137,
7 #
8 # Taken from http://www.tartarus.org/~martin/PorterStemmer (Public Domain)
9 #
10 module Text # :nodoc:
11 module PorterStemming
12
13 STEP_2_LIST = {
14 'ational' => 'ate', 'tional' => 'tion', 'enci' => 'ence', 'anci' => 'ance',
15 'izer' => 'ize', 'bli' => 'ble',
16 'alli' => 'al', 'entli' => 'ent', 'eli' => 'e', 'ousli' => 'ous',
17 'ization' => 'ize', 'ation' => 'ate',
18 'ator' => 'ate', 'alism' => 'al', 'iveness' => 'ive', 'fulness' => 'ful',
19 'ousness' => 'ous', 'aliti' => 'al',
20 'iviti' => 'ive', 'biliti' => 'ble', 'logi' => 'log'
21 }
22
23 STEP_3_LIST = {
24 'icate' => 'ic', 'ative' => '', 'alize' => 'al', 'iciti' => 'ic',
25 'ical' => 'ic', 'ful' => '', 'ness' => ''
26 }
27
28 SUFFIX_1_REGEXP = /(
29 ational |
30 tional |
31 enci |
32 anci |
33 izer |
34 bli |
35 alli |
36 entli |
37 eli |
38 ousli |
39 ization |
40 ation |
41 ator |
42 alism |
43 iveness |
44 fulness |
45 ousness |
46 aliti |
47 iviti |
48 biliti |
49 logi)$/x
50
51 SUFFIX_2_REGEXP = /(
52 al |
53 ance |
54 ence |
55 er |
56 ic |
57 able |
58 ible |
59 ant |
60 ement |
61 ment |
62 ent |
63 ou |
64 ism |
65 ate |
66 iti |
67 ous |
68 ive |
69 ize)$/x
70
71 C = "[^aeiou]" # consonant
72 V = "[aeiouy]" # vowel
73 CC = "#{C}(?>[^aeiouy]*)" # consonant sequence
74 VV = "#{V}(?>[aeiou]*)" # vowel sequence
75
76 MGR0 = /^(#{CC})?#{VV}#{CC}/o # [cc]vvcc... is m>0
77 MEQ1 = /^(#{CC})?#{VV}#{CC}(#{VV})?$/o # [cc]vvcc[vv] is m=1
78 MGR1 = /^(#{CC})?#{VV}#{CC}#{VV}#{CC}/o # [cc]vvccvvcc... is m>1
79 VOWEL_IN_STEM = /^(#{CC})?#{V}/o # vowel in stem
80
81 def self.stem(word)
82
83 # make a copy of the given object and convert it to a string.
84 word = word.dup.to_str
85
86 return word if word.length < 3
87
88 # now map initial y to Y so that the patterns never treat it as vowel
89 word[0] = 'Y' if word[0] == ?y
90
91 # Step 1a
92 if word =~ /(ss|i)es$/
93 word = $` + $1
94 elsif word =~ /([^s])s$/
95 word = $` + $1
96 end
97
98 # Step 1b
99 if word =~ /eed$/
100 word.chop! if $` =~ MGR0
101 elsif word =~ /(ed|ing)$/
102 stem = $`
103 if stem =~ VOWEL_IN_STEM
104 word = stem
105 case word
106 when /(at|bl|iz)$/ then word << "e"
107 when /([^aeiouylsz])\1$/ then word.chop!
108 when /^#{CC}#{V}[^aeiouwxy]$/o then word << "e"
109 end
110 end
111 end
112
113 if word =~ /y$/
114 stem = $`
115 word = stem + "i" if stem =~ VOWEL_IN_STEM
116 end
117
118 # Step 2
119 if word =~ SUFFIX_1_REGEXP
120 stem = $`
121 suffix = $1
122 # print "stem= " + stem + "\n" + "suffix=" + suffix + "\n"
123 if stem =~ MGR0
124 word = stem + STEP_2_LIST[suffix]
125 end
126 end
127
128 # Step 3
129 if word =~ /(icate|ative|alize|iciti|ical|ful|ness)$/
130 stem = $`
131 suffix = $1
132 if stem =~ MGR0
133 word = stem + STEP_3_LIST[suffix]
134 end
135 end
136
137 # Step 4
138 if word =~ SUFFIX_2_REGEXP
139 stem = $`
140 if stem =~ MGR1
141 word = stem
142 end
143 elsif word =~ /(s|t)(ion)$/
144 stem = $` + $1
145 if stem =~ MGR1
146 word = stem
147 end
148 end
149
150 # Step 5
151 if word =~ /e$/
152 stem = $`
153 if (stem =~ MGR1) ||
154 (stem =~ MEQ1 && stem !~ /^#{CC}#{V}[^aeiouwxy]$/o)
155 word = stem
156 end
157 end
158
159 if word =~ /ll$/ && word =~ MGR1
160 word.chop!
161 end
162
163 # and turn initial Y back to y
164 word[0] = 'y' if word[0] == ?Y
165
166 word
167 end
168
169 end
170 end
0 #
1 # Ruby implementation of the Soundex algorithm,
2 # as described by Knuth in volume 3 of The Art of Computer Programming.
3 #
4 # Author: Michael Neumann (neumann@s-direktnet.de)
5 #
6
7 module Text # :nodoc:
8 module Soundex
9
10 def soundex(str_or_arr)
11 case str_or_arr
12 when String
13 soundex_str(str_or_arr)
14 when Array
15 str_or_arr.collect{|ele| soundex_str(ele)}
16 else
17 nil
18 end
19 end
20 module_function :soundex
21
22 private
23
24 #
25 # returns nil if the value couldn't be calculated (empty-string, wrong-character)
26 # do not change the parameter "str"
27 #
28 def soundex_str(str)
29 str = str.upcase.gsub(/[^A-Z]/, "")
30 return nil if str.empty?
31
32 last_code = get_code(str[0,1])
33 soundex_code = str[0,1]
34
35 for index in 1...(str.size) do
36 return soundex_code if soundex_code.size == 4
37
38 code = get_code(str[index,1])
39
40 if code == "0" then
41 last_code = nil
42 elsif code != last_code then
43 soundex_code += code
44 last_code = code
45 end
46 end # for
47
48 return soundex_code.ljust(4, "0")
49 end
50 module_function :soundex_str
51
52 def get_code(char)
53 char.tr! "AEIOUYWHBPFVCSKGJQXZDTLMNR", "00000000111122222222334556"
54 end
55 module_function :get_code
56
57 end # module Soundex
58 end # module Text
0 module Text
1 module VERSION #:nodoc:
2 MAJOR = 1
3 MINOR = 3
4 TINY = 1
5
6 STRING = [MAJOR, MINOR, TINY].join('.')
7 end
8 end
0 # encoding: utf-8
1 # Original author: Wilker Lúcio <wilkerlucio@gmail.com>
2
3 require "set"
4
5 module Text
6
7 # Ruby implementation of the string similarity described by Simon White
8 # at: http://www.catalysoft.com/articles/StrikeAMatch.html
9 #
10 # 2 * |pairs(s1) INTERSECT pairs(s2)|
11 # similarity(s1, s2) = -----------------------------------
12 # |pairs(s1)| + |pairs(s2)|
13 #
14 # e.g.
15 # 2 * |{FR, NC}|
16 # similarity(FRANCE, FRENCH) = ---------------------------------------
17 # |{FR,RA,AN,NC,CE}| + |{FR,RE,EN,NC,CH}|
18 #
19 # = (2 * 2) / (5 + 5)
20 #
21 # = 0.4
22 #
23 # WhiteSimilarity.new.similarity("FRANCE", "FRENCH")
24 #
25 class WhiteSimilarity
26
27 def self.similarity(str1, str2)
28 new.similarity(str1, str2)
29 end
30
31 def initialize
32 @word_letter_pairs = {}
33 end
34
35 def similarity(str1, str2)
36 pairs1 = word_letter_pairs(str1)
37 pairs2 = word_letter_pairs(str2).dup
38
39 union = pairs1.length + pairs2.length
40
41 intersection = 0
42 pairs1.each do |pair1|
43 if index = pairs2.index(pair1)
44 intersection += 1
45 pairs2.delete_at(index)
46 end
47 end
48
49 (2.0 * intersection) / union
50 end
51
52 private
53 def word_letter_pairs(str)
54 @word_letter_pairs[str] ||=
55 str.upcase.split(/\s+/).map{ |word|
56 (0 ... (word.length - 1)).map { |i| word[i, 2] }
57 }.flatten.freeze
58 end
59 end
60 end
0 require 'text/double_metaphone'
1 require 'text/levenshtein'
2 require 'text/metaphone'
3 require 'text/porter_stemming'
4 require 'text/soundex'
5 require 'text/version'
6 require 'text/white_similarity'
0 # encoding: utf-8
1
2 require 'set'
3 require 'twitter-text/hash_helper'
4
5 module Twitter
6 module TwitterText
7 # A module for including Tweet auto-linking in a class. The primary use of this is for helpers/views so they can auto-link
8 # usernames, lists, hashtags and URLs.
9 module Autolink extend self
10 # Default CSS class for auto-linked lists
11 DEFAULT_LIST_CLASS = "tweet-url list-slug".freeze
12 # Default CSS class for auto-linked usernames
13 DEFAULT_USERNAME_CLASS = "tweet-url username".freeze
14 # Default CSS class for auto-linked hashtags
15 DEFAULT_HASHTAG_CLASS = "tweet-url hashtag".freeze
16 # Default CSS class for auto-linked cashtags
17 DEFAULT_CASHTAG_CLASS = "tweet-url cashtag".freeze
18
19 # Default URL base for auto-linked usernames
20 DEFAULT_USERNAME_URL_BASE = "https://twitter.com/".freeze
21 # Default URL base for auto-linked lists
22 DEFAULT_LIST_URL_BASE = "https://twitter.com/".freeze
23 # Default URL base for auto-linked hashtags
24 DEFAULT_HASHTAG_URL_BASE = "https://twitter.com/search?q=%23".freeze
25 # Default URL base for auto-linked cashtags
26 DEFAULT_CASHTAG_URL_BASE = "https://twitter.com/search?q=%24".freeze
27
28 # Default attributes for invisible span tag
29 DEFAULT_INVISIBLE_TAG_ATTRS = "style='position:absolute;left:-9999px;'".freeze
30
31 DEFAULT_OPTIONS = {
32 :list_class => DEFAULT_LIST_CLASS,
33 :username_class => DEFAULT_USERNAME_CLASS,
34 :hashtag_class => DEFAULT_HASHTAG_CLASS,
35 :cashtag_class => DEFAULT_CASHTAG_CLASS,
36
37 :username_url_base => DEFAULT_USERNAME_URL_BASE,
38 :list_url_base => DEFAULT_LIST_URL_BASE,
39 :hashtag_url_base => DEFAULT_HASHTAG_URL_BASE,
40 :cashtag_url_base => DEFAULT_CASHTAG_URL_BASE,
41
42 :invisible_tag_attrs => DEFAULT_INVISIBLE_TAG_ATTRS
43 }.freeze
44
45 def auto_link_with_json(text, json, options = {})
46 # concatenate entities
47 entities = json.values().flatten()
48
49 # map JSON entity to twitter-text entity
50 # be careful not to alter arguments received
51 entities.map! do |entity|
52 entity = HashHelper.symbolize_keys(entity)
53 # hashtag
54 entity[:hashtag] = entity[:text] if entity[:text]
55 entity
56 end
57
58 auto_link_entities(text, entities, options)
59 end
60
61 def auto_link_entities(text, entities, options = {}, &block)
62 return text if entities.empty?
63
64 # NOTE deprecate these attributes not options keys in options hash, then use html_attrs
65 options = DEFAULT_OPTIONS.merge(options)
66 options[:html_attrs] = extract_html_attrs_from_options!(options)
67 options[:html_attrs][:rel] ||= "nofollow" unless options[:suppress_no_follow]
68 options[:html_attrs][:target] = "_blank" if options[:target_blank] == true
69
70 Twitter::TwitterText::Rewriter.rewrite_entities(text.dup, entities) do |entity, chars|
71 if entity[:url]
72 link_to_url(entity, chars, options, &block)
73 elsif entity[:hashtag]
74 link_to_hashtag(entity, chars, options, &block)
75 elsif entity[:screen_name]
76 link_to_screen_name(entity, chars, options, &block)
77 elsif entity[:cashtag]
78 link_to_cashtag(entity, chars, options, &block)
79 end
80 end
81 end
82
83 # Add <tt><a></a></tt> tags around the usernames, lists, hashtags and URLs in the provided <tt>text</tt>.
84 # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash:
85 # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
86 # and place in the <tt><a></tt> tag.
87 #
88 # <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
89 # <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
90 # <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
91 # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
92 # <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
93 # <tt>:username_url_base</tt>:: the value for <tt>href</tt> attribute on username links. The <tt>@username</tt> (minus the <tt>@</tt>) will be appended at the end of this.
94 # <tt>:list_url_base</tt>:: the value for <tt>href</tt> attribute on list links. The <tt>@username/list</tt> (minus the <tt>@</tt>) will be appended at the end of this.
95 # <tt>:hashtag_url_base</tt>:: the value for <tt>href</tt> attribute on hashtag links. The <tt>#hashtag</tt> (minus the <tt>#</tt>) will be appended at the end of this.
96 # <tt>:cashtag_url_base</tt>:: the value for <tt>href</tt> attribute on cashtag links. The <tt>$cashtag</tt> (minus the <tt>$</tt>) will be appended at the end of this.
97 # <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
98 # <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
99 # <tt>:suppress_lists</tt>:: disable auto-linking to lists
100 # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
101 # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
102 # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
103 # <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
104 # <tt>:target_blank</tt>:: adds <tt>target="_blank"</tt> to all auto_linked items username / hashtag / cashtag links / urls
105 # <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
106 # <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
107 def auto_link(text, options = {}, &block)
108 auto_link_entities(text, Extractor.extract_entities_with_indices(text, :extract_url_without_protocol => false), options, &block)
109 end
110
111 # Add <tt><a></a></tt> tags around the usernames and lists in the provided <tt>text</tt>. The
112 # <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
113 # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
114 # and place in the <tt><a></tt> tag.
115 #
116 # <tt>:list_class</tt>:: class to add to list <tt><a></tt> tags
117 # <tt>:username_class</tt>:: class to add to username <tt><a></tt> tags
118 # <tt>:username_url_base</tt>:: the value for <tt>href</tt> attribute on username links. The <tt>@username</tt> (minus the <tt>@</tt>) will be appended at the end of this.
119 # <tt>:list_url_base</tt>:: the value for <tt>href</tt> attribute on list links. The <tt>@username/list</tt> (minus the <tt>@</tt>) will be appended at the end of this.
120 # <tt>:username_include_symbol</tt>:: place the <tt>@</tt> symbol within username and list links
121 # <tt>:suppress_lists</tt>:: disable auto-linking to lists
122 # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
123 # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
124 # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
125 # <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
126 # <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
127 def auto_link_usernames_or_lists(text, options = {}, &block) # :yields: list_or_username
128 auto_link_entities(text, Extractor.extract_mentions_or_lists_with_indices(text), options, &block)
129 end
130
131 # Add <tt><a></a></tt> tags around the hashtags in the provided <tt>text</tt>.
132 # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
133 # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
134 # and place in the <tt><a></tt> tag.
135 #
136 # <tt>:hashtag_class</tt>:: class to add to hashtag <tt><a></tt> tags
137 # <tt>:hashtag_url_base</tt>:: the value for <tt>href</tt> attribute. The hashtag text (minus the <tt>#</tt>) will be appended at the end of this.
138 # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
139 # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
140 # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
141 # <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
142 # <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
143 def auto_link_hashtags(text, options = {}, &block) # :yields: hashtag_text
144 auto_link_entities(text, Extractor.extract_hashtags_with_indices(text), options, &block)
145 end
146
147 # Add <tt><a></a></tt> tags around the cashtags in the provided <tt>text</tt>.
148 # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
149 # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
150 # and place in the <tt><a></tt> tag.
151 #
152 # <tt>:cashtag_class</tt>:: class to add to cashtag <tt><a></tt> tags
153 # <tt>:cashtag_url_base</tt>:: the value for <tt>href</tt> attribute. The cashtag text (minus the <tt>$</tt>) will be appended at the end of this.
154 # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
155 # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
156 # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
157 # <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
158 # <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
159 def auto_link_cashtags(text, options = {}, &block) # :yields: cashtag_text
160 auto_link_entities(text, Extractor.extract_cashtags_with_indices(text), options, &block)
161 end
162
163 # Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
164 # The <tt><a></tt> tags can be controlled with the following entries in the <tt>options</tt> hash.
165 # Also any elements in the <tt>options</tt> hash will be converted to HTML attributes
166 # and place in the <tt><a></tt> tag.
167 #
168 # <tt>:url_class</tt>:: class to add to url <tt><a></tt> tags
169 # <tt>:invisible_tag_attrs</tt>:: HTML attribute to add to invisible span tags
170 # <tt>:suppress_no_follow</tt>:: do not add <tt>rel="nofollow"</tt> to auto-linked items
171 # <tt>:symbol_tag</tt>:: tag to apply around symbol (@, #, $) in username / hashtag / cashtag links
172 # <tt>:text_with_symbol_tag</tt>:: tag to apply around text part in username / hashtag / cashtag links
173 # <tt>:url_target</tt>:: the value for <tt>target</tt> attribute on URL links.
174 # <tt>:link_attribute_block</tt>:: function to modify the attributes of a link based on the entity. called with |entity, attributes| params, and should modify the attributes hash.
175 # <tt>:link_text_block</tt>:: function to modify the text of a link based on the entity. called with |entity, text| params, and should return a modified text.
176 def auto_link_urls(text, options = {}, &block)
177 auto_link_entities(text, Extractor.extract_urls_with_indices(text, :extract_url_without_protocol => false), options, &block)
178 end
179
180 # These methods are deprecated, will be removed in future.
181 extend Deprecation
182
183 # <b>Deprecated</b>: Please use auto_link_urls instead.
184 # Add <tt><a></a></tt> tags around the URLs in the provided <tt>text</tt>.
185 # Any elements in the <tt>href_options</tt> hash will be converted to HTML attributes
186 # and place in the <tt><a></tt> tag.
187 # Unless <tt>href_options</tt> contains <tt>:suppress_no_follow</tt>
188 # the <tt>rel="nofollow"</tt> attribute will be added.
189 alias :auto_link_urls_custom :auto_link_urls
190 deprecate :auto_link_urls_custom, :auto_link_urls
191
192 private
193
194 HTML_ENTITIES = {
195 '&' => '&amp;',
196 '>' => '&gt;',
197 '<' => '&lt;',
198 '"' => '&quot;',
199 "'" => '&#39;'
200 }
201
202 def html_escape(text)
203 text && text.to_s.gsub(/[&"'><]/) do |character|
204 HTML_ENTITIES[character]
205 end
206 end
207
208 # NOTE We will make this private in future.
209 public :html_escape
210
211 # Options which should not be passed as HTML attributes
212 OPTIONS_NOT_ATTRIBUTES = Set.new([
213 :url_class, :list_class, :username_class, :hashtag_class, :cashtag_class,
214 :username_url_base, :list_url_base, :hashtag_url_base, :cashtag_url_base,
215 :username_url_block, :list_url_block, :hashtag_url_block, :cashtag_url_block, :link_url_block,
216 :username_include_symbol, :suppress_lists, :suppress_no_follow, :url_entities,
217 :invisible_tag_attrs, :symbol_tag, :text_with_symbol_tag, :url_target, :target_blank,
218 :link_attribute_block, :link_text_block
219 ]).freeze
220
221 def extract_html_attrs_from_options!(options)
222 html_attrs = {}
223 options.reject! do |key, value|
224 unless OPTIONS_NOT_ATTRIBUTES.include?(key)
225 html_attrs[key] = value
226 true
227 end
228 end
229 html_attrs
230 end
231
232 def url_entities_hash(url_entities)
233 (url_entities || {}).inject({}) do |entities, entity|
234 # be careful not to alter arguments received
235 _entity = HashHelper.symbolize_keys(entity)
236 entities[_entity[:url]] = _entity
237 entities
238 end
239 end
240
241 def link_to_url(entity, chars, options = {})
242 url = entity[:url]
243
244 href = if options[:link_url_block]
245 options[:link_url_block].call(url)
246 else
247 url
248 end
249
250 # NOTE auto link to urls do not use any default values and options
251 # like url_class but use suppress_no_follow.
252 html_attrs = options[:html_attrs].dup
253 html_attrs[:class] = options[:url_class] if options.key?(:url_class)
254
255 # add target attribute only if :url_target is specified
256 html_attrs[:target] = options[:url_target] if options.key?(:url_target)
257
258 url_entities = url_entities_hash(options[:url_entities])
259
260 # use entity from urlEntities if available
261 url_entity = url_entities[url] || entity
262 link_text = if url_entity[:display_url]
263 html_attrs[:title] ||= url_entity[:expanded_url]
264 link_url_with_entity(url_entity, options)
265 else
266 html_escape(url)
267 end
268
269 link_to_text(entity, link_text, href, html_attrs, options)
270 end
271
272 def link_url_with_entity(entity, options)
273 display_url = entity[:display_url]
274 expanded_url = entity[:expanded_url]
275 invisible_tag_attrs = options[:invisible_tag_attrs] || DEFAULT_INVISIBLE_TAG_ATTRS
276
277 # Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
278 # should contain the full original URL (expanded_url), not the display URL.
279 #
280 # Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
281 # font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
282 # Elements with font-size:0 get copied even though they are not visible.
283 # Note that display:none doesn't work here. Elements with display:none don't get copied.
284 #
285 # Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
286 # wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
287 # everything with the tco-ellipsis class.
288 #
289 # Exception: pic.twitter.com images, for which expandedUrl = "https://twitter.com/username/status/1234/photo/1
290 # For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
291 # For a pic.twitter.com URL, the only elided part will be the "https://", so this is fine.
292 display_url_sans_ellipses = display_url.gsub("…", "")
293
294 if expanded_url.include?(display_url_sans_ellipses)
295 before_display_url, after_display_url = expanded_url.split(display_url_sans_ellipses, 2)
296 preceding_ellipsis = /\A…/.match(display_url).to_s
297 following_ellipsis = /…\z/.match(display_url).to_s
298
299 # As an example: The user tweets "hi http://longdomainname.com/foo"
300 # This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
301 # This will get rendered as:
302 # <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
303 # …
304 # <!-- There's a chance the onCopy event handler might not fire. In case that happens,
305 # we include an &nbsp; here so that the … doesn't bump up against the URL and ruin it.
306 # The &nbsp; is inside the tco-ellipsis span so that when the onCopy handler *does*
307 # fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
308 # e.g. "hi http://longdomainname.com/foo".
309 # <span style='font-size:0'>&nbsp;</span>
310 # </span>
311 # <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
312 # http://longdomai
313 # </span>
314 # <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
315 # nname.com/foo
316 # </span>
317 # <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
318 # <span style='font-size:0'>&nbsp;</span>
319 # …
320 # </span>
321 %(<span class="tco-ellipsis">#{preceding_ellipsis}<span #{invisible_tag_attrs}>&nbsp;</span></span>) <<
322 %(<span #{invisible_tag_attrs}>#{html_escape(before_display_url)}</span>) <<
323 %(<span class="js-display-url">#{html_escape(display_url_sans_ellipses)}</span>) <<
324 %(<span #{invisible_tag_attrs}>#{html_escape(after_display_url)}</span>) <<
325 %(<span class="tco-ellipsis"><span #{invisible_tag_attrs}>&nbsp;</span>#{following_ellipsis}</span>)
326 else
327 html_escape(display_url)
328 end
329 end
330
331 def link_to_hashtag(entity, chars, options = {})
332 hash = chars[entity[:indices].first]
333 hashtag = entity[:hashtag]
334 hashtag = yield(hashtag) if block_given?
335 hashtag_class = options[:hashtag_class].to_s
336
337 if hashtag.match Twitter::TwitterText::Regex::REGEXEN[:rtl_chars]
338 hashtag_class += ' rtl'
339 end
340
341 href = if options[:hashtag_url_block]
342 options[:hashtag_url_block].call(hashtag)
343 else
344 "#{options[:hashtag_url_base]}#{hashtag}"
345 end
346
347 html_attrs = {
348 :class => hashtag_class,
349 # FIXME As our conformance test, hash in title should be half-width,
350 # this should be bug of conformance data.
351 :title => "##{hashtag}"
352 }.merge(options[:html_attrs])
353
354 link_to_text_with_symbol(entity, hash, hashtag, href, html_attrs, options)
355 end
356
357 def link_to_cashtag(entity, chars, options = {})
358 dollar = chars[entity[:indices].first]
359 cashtag = entity[:cashtag]
360 cashtag = yield(cashtag) if block_given?
361
362 href = if options[:cashtag_url_block]
363 options[:cashtag_url_block].call(cashtag)
364 else
365 "#{options[:cashtag_url_base]}#{cashtag}"
366 end
367
368 html_attrs = {
369 :class => "#{options[:cashtag_class]}",
370 :title => "$#{cashtag}"
371 }.merge(options[:html_attrs])
372
373 link_to_text_with_symbol(entity, dollar, cashtag, href, html_attrs, options)
374 end
375
376 def link_to_screen_name(entity, chars, options = {})
377 name = "#{entity[:screen_name]}#{entity[:list_slug]}"
378
379 chunk = name.dup
380 chunk = yield(chunk) if block_given?
381
382 at = chars[entity[:indices].first]
383
384 html_attrs = options[:html_attrs].dup
385
386 if entity[:list_slug] && !entity[:list_slug].empty? && !options[:suppress_lists]
387 href = if options[:list_url_block]
388 options[:list_url_block].call(name)
389 else
390 "#{options[:list_url_base]}#{name}"
391 end
392 html_attrs[:class] ||= "#{options[:list_class]}"
393 else
394 href = if options[:username_url_block]
395 options[:username_url_block].call(chunk)
396 else
397 "#{options[:username_url_base]}#{name}"
398 end
399 html_attrs[:class] ||= "#{options[:username_class]}"
400 end
401
402 link_to_text_with_symbol(entity, at, chunk, href, html_attrs, options)
403 end
404
405 def link_to_text_with_symbol(entity, symbol, text, href, attributes = {}, options = {})
406 tagged_symbol = options[:symbol_tag] ? "<#{options[:symbol_tag]}>#{symbol}</#{options[:symbol_tag]}>" : symbol
407 text = html_escape(text)
408 tagged_text = options[:text_with_symbol_tag] ? "<#{options[:text_with_symbol_tag]}>#{text}</#{options[:text_with_symbol_tag]}>" : text
409 if options[:username_include_symbol] || symbol !~ Twitter::TwitterText::Regex::REGEXEN[:at_signs]
410 "#{link_to_text(entity, tagged_symbol + tagged_text, href, attributes, options)}"
411 else
412 "#{tagged_symbol}#{link_to_text(entity, tagged_text, href, attributes, options)}"
413 end
414 end
415
416 def link_to_text(entity, text, href, attributes = {}, options = {})
417 attributes[:href] = href
418 options[:link_attribute_block].call(entity, attributes) if options[:link_attribute_block]
419 text = options[:link_text_block].call(entity, text) if options[:link_text_block]
420 %(<a#{tag_attrs(attributes)}>#{text}</a>)
421 end
422
423 BOOLEAN_ATTRIBUTES = Set.new([:disabled, :readonly, :multiple, :checked]).freeze
424
425 def tag_attrs(attributes)
426 attributes.keys.sort_by{|k| k.to_s}.inject("") do |attrs, key|
427 value = attributes[key]
428
429 if BOOLEAN_ATTRIBUTES.include?(key)
430 value = value ? key : nil
431 end
432
433 unless value.nil?
434 value = case value
435 when Array
436 value.compact.join(" ")
437 else
438 value
439 end
440 attrs << %( #{html_escape(key)}="#{html_escape(value)}")
441 end
442
443 attrs
444 end
445 end
446 end
447 end
448 end
0 # encoding: UTF-8
1
2 module Twitter
3 module TwitterText
4 class Configuration
5 require 'json'
6
7 PARSER_VERSION_CLASSIC = "v1"
8 PARSER_VERSION_DEFAULT = "v2"
9
10 class << self
11 attr_accessor :default_configuration
12 end
13
14 attr_reader :version, :max_weighted_tweet_length, :scale
15 attr_reader :default_weight, :transformed_url_length, :ranges
16
17 CONFIG_V1 = File.join(
18 File.expand_path('../../../config', __FILE__), # project root
19 "#{PARSER_VERSION_CLASSIC}.json"
20 )
21
22 CONFIG_V2 = File.join(
23 File.expand_path('../../../config', __FILE__), # project root
24 "#{PARSER_VERSION_DEFAULT}.json"
25 )
26
27 def self.parse_string(string, options = {})
28 JSON.parse(string, options.merge(symbolize_names: true))
29 end
30
31 def self.parse_file(filename)
32 string = File.open(filename, 'rb') { |f| f.read }
33 parse_string(string)
34 end
35
36 def self.configuration_from_file(filename)
37 config = parse_file(filename)
38 config ? self.new(config) : nil
39 end
40
41 def initialize(config = {})
42 @version = config[:version]
43 @max_weighted_tweet_length = config[:maxWeightedTweetLength]
44 @scale = config[:scale]
45 @default_weight = config[:defaultWeight]
46 @transformed_url_length = config[:transformedURLLength]
47 @ranges = config[:ranges].map { |range| Twitter::TwitterText::WeightedRange.new(range) } if config.key?(:ranges) && config[:ranges].is_a?(Array)
48 end
49
50 self.default_configuration = self.configuration_from_file(CONFIG_V2)
51 end
52 end
53 end
0 module Twitter
1 module TwitterText
2 module Deprecation
3 def deprecate(method, new_method = nil)
4 deprecated_method = :"deprecated_#{method}"
5 message = "Deprecation: `#{method}` is deprecated."
6 message << " Please use `#{new_method}` instead." if new_method
7
8 alias_method(deprecated_method, method)
9 define_method method do |*args, &block|
10 warn message unless $TESTING
11 send(deprecated_method, *args, &block)
12 end
13 end
14 end
15 end
16 end
0 # encoding: utf-8
1 require 'idn'
2
3 class String
4 # Helper function to count the character length by first converting to an
5 # array. This is needed because with unicode strings, the return value
6 # of length may be incorrect
7 def char_length
8 if respond_to? :codepoints
9 length
10 else
11 chars.kind_of?(Enumerable) ? chars.to_a.size : chars.size
12 end
13 end
14
15 # Helper function to convert this string into an array of unicode characters.
16 def to_char_a
17 @to_char_a ||= if chars.kind_of?(Enumerable)
18 chars.to_a
19 else
20 char_array = []
21 0.upto(char_length - 1) { |i| char_array << [chars.slice(i)].pack('U') }
22 char_array
23 end
24 end
25 end
26
27 # Helper functions to return character offsets instead of byte offsets.
28 class MatchData
29 def char_begin(n)
30 if string.respond_to? :codepoints
31 self.begin(n)
32 else
33 string[0, self.begin(n)].char_length
34 end
35 end
36
37 def char_end(n)
38 if string.respond_to? :codepoints
39 self.end(n)
40 else
41 string[0, self.end(n)].char_length
42 end
43 end
44 end
45
46 module Twitter
47 module TwitterText
48 # A module for including Tweet parsing in a class. This module provides function for the extraction and processing
49 # of usernames, lists, URLs and hashtags.
50 module Extractor extend self
51
52 # Maximum URL length as defined by Twitter's backend.
53 MAX_URL_LENGTH = 4096
54
55 # The maximum t.co path length that the Twitter backend supports.
56 MAX_TCO_SLUG_LENGTH = 40
57
58 URL_PROTOCOL_LENGTH = "https://".length
59
60 # Remove overlapping entities.
61 # This returns a new array with no overlapping entities.
62 def remove_overlapping_entities(entities)
63 # sort by start index
64 entities = entities.sort_by{|entity| entity[:indices].first}
65
66 # remove duplicates
67 prev = nil
68 entities.reject!{|entity| (prev && prev[:indices].last > entity[:indices].first) || (prev = entity) && false}
69 entities
70 end
71
72 # Extracts all usernames, lists, hashtags and URLs in the Tweet <tt>text</tt>
73 # along with the indices for where the entity ocurred
74 # If the <tt>text</tt> is <tt>nil</tt> or contains no entity an empty array
75 # will be returned.
76 #
77 # If a block is given then it will be called for each entity.
78 def extract_entities_with_indices(text, options = {}, &block)
79 # extract all entities
80 entities = extract_urls_with_indices(text, options) +
81 extract_hashtags_with_indices(text, :check_url_overlap => false) +
82 extract_mentions_or_lists_with_indices(text) +
83 extract_cashtags_with_indices(text)
84
85 return [] if entities.empty?
86
87 entities = remove_overlapping_entities(entities)
88
89 entities.each(&block) if block_given?
90 entities
91 end
92
93 # Extracts a list of all usernames mentioned in the Tweet <tt>text</tt>. If the
94 # <tt>text</tt> is <tt>nil</tt> or contains no username mentions an empty array
95 # will be returned.
96 #
97 # If a block is given then it will be called for each username.
98 def extract_mentioned_screen_names(text, &block) # :yields: username
99 screen_names = extract_mentioned_screen_names_with_indices(text).map{|m| m[:screen_name]}
100 screen_names.each(&block) if block_given?
101 screen_names
102 end
103
104 # Extracts a list of all usernames mentioned in the Tweet <tt>text</tt>
105 # along with the indices for where the mention ocurred. If the
106 # <tt>text</tt> is nil or contains no username mentions, an empty array
107 # will be returned.
108 #
109 # If a block is given, then it will be called with each username, the start
110 # index, and the end index in the <tt>text</tt>.
111 def extract_mentioned_screen_names_with_indices(text) # :yields: username, start, end
112 return [] unless text
113
114 possible_screen_names = []
115 extract_mentions_or_lists_with_indices(text) do |screen_name, list_slug, start_position, end_position|
116 next unless list_slug.empty?
117 possible_screen_names << {
118 :screen_name => screen_name,
119 :indices => [start_position, end_position]
120 }
121 end
122
123 if block_given?
124 possible_screen_names.each do |mention|
125 yield mention[:screen_name], mention[:indices].first, mention[:indices].last
126 end
127 end
128
129 possible_screen_names
130 end
131
132 # Extracts a list of all usernames or lists mentioned in the Tweet <tt>text</tt>
133 # along with the indices for where the mention ocurred. If the
134 # <tt>text</tt> is nil or contains no username or list mentions, an empty array
135 # will be returned.
136 #
137 # If a block is given, then it will be called with each username, list slug, the start
138 # index, and the end index in the <tt>text</tt>. The list_slug will be an empty stirng
139 # if this is a username mention.
140 def extract_mentions_or_lists_with_indices(text) # :yields: username, list_slug, start, end
141 return [] unless text =~ /[@@]/
142
143 possible_entries = []
144 text.to_s.scan(Twitter::TwitterText::Regex[:valid_mention_or_list]) do |before, at, screen_name, list_slug|
145 match_data = $~
146 after = $'
147 unless after =~ Twitter::TwitterText::Regex[:end_mention_match]
148 start_position = match_data.char_begin(3) - 1
149 end_position = match_data.char_end(list_slug.nil? ? 3 : 4)
150 possible_entries << {
151 :screen_name => screen_name,
152 :list_slug => list_slug || "",
153 :indices => [start_position, end_position]
154 }
155 end
156 end
157
158 if block_given?
159 possible_entries.each do |mention|
160 yield mention[:screen_name], mention[:list_slug], mention[:indices].first, mention[:indices].last
161 end
162 end
163
164 possible_entries
165 end
166
167 # Extracts the username username replied to in the Tweet <tt>text</tt>. If the
168 # <tt>text</tt> is <tt>nil</tt> or is not a reply nil will be returned.
169 #
170 # If a block is given then it will be called with the username replied to (if any)
171 def extract_reply_screen_name(text) # :yields: username
172 return nil unless text
173
174 possible_screen_name = text.match(Twitter::TwitterText::Regex[:valid_reply])
175 return unless possible_screen_name.respond_to?(:captures)
176 return if $' =~ Twitter::TwitterText::Regex[:end_mention_match]
177 screen_name = possible_screen_name.captures.first
178 yield screen_name if block_given?
179 screen_name
180 end
181
182 # Extracts a list of all URLs included in the Tweet <tt>text</tt>. If the
183 # <tt>text</tt> is <tt>nil</tt> or contains no URLs an empty array
184 # will be returned.
185 #
186 # If a block is given then it will be called for each URL.
187 def extract_urls(text, &block) # :yields: url
188 urls = extract_urls_with_indices(text).map{|u| u[:url]}
189 urls.each(&block) if block_given?
190 urls
191 end
192
193 # Extracts a list of all URLs included in the Tweet <tt>text</tt> along
194 # with the indices. If the <tt>text</tt> is <tt>nil</tt> or contains no
195 # URLs an empty array will be returned.
196 #
197 # If a block is given then it will be called for each URL.
198 def extract_urls_with_indices(text, options = {:extract_url_without_protocol => true}) # :yields: url, start, end
199 return [] unless text && (options[:extract_url_without_protocol] ? text.index(".") : text.index(":"))
200 urls = []
201
202 text.to_s.scan(Twitter::TwitterText::Regex[:valid_url]) do |all, before, url, protocol, domain, port, path, query|
203 valid_url_match_data = $~
204
205 start_position = valid_url_match_data.char_begin(3)
206 end_position = valid_url_match_data.char_end(3)
207
208 # If protocol is missing and domain contains non-ASCII characters,
209 # extract ASCII-only domains.
210 if !protocol
211 next if !options[:extract_url_without_protocol] || before =~ Twitter::TwitterText::Regex[:invalid_url_without_protocol_preceding_chars]
212 last_url = nil
213 domain.scan(Twitter::TwitterText::Regex[:valid_ascii_domain]) do |ascii_domain|
214 next unless is_valid_domain(url.length, ascii_domain, protocol)
215 last_url = {
216 :url => ascii_domain,
217 :indices => [start_position + $~.char_begin(0),
218 start_position + $~.char_end(0)]
219 }
220 if path ||
221 ascii_domain =~ Twitter::TwitterText::Regex[:valid_special_short_domain] ||
222 ascii_domain !~ Twitter::TwitterText::Regex[:invalid_short_domain]
223 urls << last_url
224 end
225 end
226
227 # no ASCII-only domain found. Skip the entire URL
228 next unless last_url
229
230 # last_url only contains domain. Need to add path and query if they exist.
231 if path
232 # last_url was not added. Add it to urls here.
233 last_url[:url] = url.sub(domain, last_url[:url])
234 last_url[:indices][1] = end_position
235 end
236 else
237 # In the case of t.co URLs, don't allow additional path characters
238 if url =~ Twitter::TwitterText::Regex[:valid_tco_url]
239 next if $1 && $1.length > MAX_TCO_SLUG_LENGTH
240 url = $&
241 end_position = start_position + url.char_length
242 end
243
244 next unless is_valid_domain(url.length, domain, protocol)
245
246 urls << {
247 :url => url,
248 :indices => [start_position, end_position]
249 }
250 end
251 end
252 urls.each{|url| yield url[:url], url[:indices].first, url[:indices].last} if block_given?
253 urls
254 end
255
256 # Extracts a list of all hashtags included in the Tweet <tt>text</tt>. If the
257 # <tt>text</tt> is <tt>nil</tt> or contains no hashtags an empty array
258 # will be returned. The array returned will not include the leading <tt>#</tt>
259 # character.
260 #
261 # If a block is given then it will be called for each hashtag.
262 def extract_hashtags(text, &block) # :yields: hashtag_text
263 hashtags = extract_hashtags_with_indices(text).map{|h| h[:hashtag]}
264 hashtags.each(&block) if block_given?
265 hashtags
266 end
267
268 # Extracts a list of all hashtags included in the Tweet <tt>text</tt>. If the
269 # <tt>text</tt> is <tt>nil</tt> or contains no hashtags an empty array
270 # will be returned. The array returned will not include the leading <tt>#</tt>
271 # character.
272 #
273 # If a block is given then it will be called for each hashtag.
274 def extract_hashtags_with_indices(text, options = {:check_url_overlap => true}) # :yields: hashtag_text, start, end
275 return [] unless text =~ /[##]/
276
277 tags = []
278 text.scan(Twitter::TwitterText::Regex[:valid_hashtag]) do |before, hash, hash_text|
279 match_data = $~
280 start_position = match_data.char_begin(2)
281 end_position = match_data.char_end(3)
282 after = $'
283 unless after =~ Twitter::TwitterText::Regex[:end_hashtag_match]
284 tags << {
285 :hashtag => hash_text,
286 :indices => [start_position, end_position]
287 }
288 end
289 end
290
291 if options[:check_url_overlap]
292 # extract URLs
293 urls = extract_urls_with_indices(text)
294 unless urls.empty?
295 tags.concat(urls)
296 # remove duplicates
297 tags = remove_overlapping_entities(tags)
298 # remove URL entities
299 tags.reject!{|entity| !entity[:hashtag] }
300 end
301 end
302
303 tags.each{|tag| yield tag[:hashtag], tag[:indices].first, tag[:indices].last} if block_given?
304 tags
305 end
306
307 # Extracts a list of all cashtags included in the Tweet <tt>text</tt>. If the
308 # <tt>text</tt> is <tt>nil</tt> or contains no cashtags an empty array
309 # will be returned. The array returned will not include the leading <tt>$</tt>
310 # character.
311 #
312 # If a block is given then it will be called for each cashtag.
313 def extract_cashtags(text, &block) # :yields: cashtag_text
314 cashtags = extract_cashtags_with_indices(text).map{|h| h[:cashtag]}
315 cashtags.each(&block) if block_given?
316 cashtags
317 end
318
319 # Extracts a list of all cashtags included in the Tweet <tt>text</tt>. If the
320 # <tt>text</tt> is <tt>nil</tt> or contains no cashtags an empty array
321 # will be returned. The array returned will not include the leading <tt>$</tt>
322 # character.
323 #
324 # If a block is given then it will be called for each cashtag.
325 def extract_cashtags_with_indices(text) # :yields: cashtag_text, start, end
326 return [] unless text =~ /\$/
327
328 tags = []
329 text.scan(Twitter::TwitterText::Regex[:valid_cashtag]) do |before, dollar, cash_text|
330 match_data = $~
331 start_position = match_data.char_begin(2)
332 end_position = match_data.char_end(3)
333 tags << {
334 :cashtag => cash_text,
335 :indices => [start_position, end_position]
336 }
337 end
338
339 tags.each{|tag| yield tag[:cashtag], tag[:indices].first, tag[:indices].last} if block_given?
340 tags
341 end
342
343 def is_valid_domain(url_length, domain, protocol)
344 begin
345 raise ArgumentError.new("invalid empty domain") unless domain
346 original_domain_length = domain.length
347 encoded_domain = IDN::Idna.toASCII(domain)
348 updated_domain_length = encoded_domain.length
349 url_length += (updated_domain_length - original_domain_length) if (updated_domain_length > original_domain_length)
350 url_length += URL_PROTOCOL_LENGTH unless protocol
351 url_length <= MAX_URL_LENGTH
352 rescue Exception
353 # On error don't consider this a valid domain.
354 return false
355 end
356 end
357 end
358 end
359 end
0 module Twitter
1 module TwitterText
2 module HashHelper
3 # Return a new hash with all keys converted to symbols, as long as
4 # they respond to +to_sym+.
5 #
6 # { 'name' => 'Rob', 'years' => '28' }.symbolize_keys
7 # #=> { :name => "Rob", :years => "28" }
8 def self.symbolize_keys(hash)
9 symbolize_keys!(hash.dup)
10 end
11
12 # Destructively convert all keys to symbols, as long as they respond
13 # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
14 def self.symbolize_keys!(hash)
15 hash.keys.each do |key|
16 hash[(key.to_sym rescue key) || key] = hash.delete(key)
17 end
18 hash
19 end
20 end
21 end
22 end
0 module Twitter
1 module TwitterText
2 # Module for doing "hit highlighting" on tweets that have been auto-linked already.
3 # Useful with the results returned from the Search API.
4 module HitHighlighter extend self
5 # Default Tag used for hit highlighting
6 DEFAULT_HIGHLIGHT_TAG = "em"
7
8 # Add <tt><em></em></tt> tags around the <tt>hits</tt> provided in the <tt>text</tt>. The
9 # <tt>hits</tt> should be an array of (start, end) index pairs, relative to the original
10 # text, before auto-linking (but the <tt>text</tt> may already be auto-linked if desired)
11 #
12 # The <tt><em></em></tt> tags can be overridden using the <tt>:tag</tt> option. For example:
13 #
14 # irb> hit_highlight("test hit here", [[5, 8]], :tag => 'strong')
15 # => "test <strong>hit</strong> here"
16 def hit_highlight(text, hits = [], options = {})
17 if hits.empty?
18 return text
19 end
20
21 tag_name = options[:tag] || DEFAULT_HIGHLIGHT_TAG
22 tags = ["<" + tag_name + ">", "</" + tag_name + ">"]
23
24 chunks = text.split(/[<>]/)
25
26 result = []
27 chunk_index, chunk = 0, chunks[0]
28 chunk_chars = chunk.to_s.to_char_a
29 prev_chunks_len = 0
30 chunk_cursor = 0
31 start_in_chunk = false
32 for hit, index in hits.flatten.each_with_index do
33 tag = tags[index % 2]
34
35 placed = false
36 until chunk.nil? || hit < prev_chunks_len + chunk.length do
37 result << chunk_chars[chunk_cursor..-1]
38 if start_in_chunk && hit == prev_chunks_len + chunk_chars.length
39 result << tag
40 placed = true
41 end
42
43 # correctly handle highlights that end on the final character.
44 if tag_text = chunks[chunk_index+1]
45 result << "<#{tag_text}>"
46 end
47
48 prev_chunks_len += chunk_chars.length
49 chunk_cursor = 0
50 chunk_index += 2
51 chunk = chunks[chunk_index]
52 chunk_chars = chunk.to_s.to_char_a
53 start_in_chunk = false
54 end
55
56 if !placed && !chunk.nil?
57 hit_spot = hit - prev_chunks_len
58 result << chunk_chars[chunk_cursor...hit_spot] << tag
59 chunk_cursor = hit_spot
60 if index % 2 == 0
61 start_in_chunk = true
62 else
63 start_in_chunk = false
64 end
65 placed = true
66 end
67
68 # ultimate fallback, hits that run off the end get a closing tag
69 if !placed
70 result << tag
71 end
72 end
73
74 if chunk
75 if chunk_cursor < chunk_chars.length
76 result << chunk_chars[chunk_cursor..-1]
77 end
78 (chunk_index+1).upto(chunks.length-1).each do |i|
79 result << (i.even? ? chunks[i] : "<#{chunks[i]}>")
80 end
81 end
82
83 result.flatten.join
84 end
85 end
86 end
87 end
0 # encoding: utf-8
1
2 module Twitter
3 module TwitterText
4 # A collection of regular expressions for parsing Tweet text. The regular expression
5 # list is frozen at load time to ensure immutability. These regular expressions are
6 # used throughout the <tt>TwitterText</tt> classes. Special care has been taken to make
7 # sure these reular expressions work with Tweets in all languages.
8 class Regex
9 require 'yaml'
10
11 REGEXEN = {} # :nodoc:
12
13 def self.regex_range(from, to = nil) # :nodoc:
14 if $RUBY_1_9
15 if to
16 "\\u{#{from.to_s(16).rjust(4, '0')}}-\\u{#{to.to_s(16).rjust(4, '0')}}"
17 else
18 "\\u{#{from.to_s(16).rjust(4, '0')}}"
19 end
20 else
21 if to
22 [from].pack('U') + '-' + [to].pack('U')
23 else
24 [from].pack('U')
25 end
26 end
27 end
28
29 TLDS = YAML.load_file(
30 File.join(
31 __dir__,
32 '..', 'assets', 'tld_lib.yml'
33 )
34 )
35
36 # Space is more than %20, U+3000 for example is the full-width space used with Kanji. Provide a short-hand
37 # to access both the list of characters and a pattern suitible for use with String#split
38 # Taken from: ActiveSupport::Multibyte::Handlers::UTF8Handler::UNICODE_WHITESPACE
39 UNICODE_SPACES = [
40 (0x0009..0x000D).to_a, # White_Space # Cc [5] <control-0009>..<control-000D>
41 0x0020, # White_Space # Zs SPACE
42 0x0085, # White_Space # Cc <control-0085>
43 0x00A0, # White_Space # Zs NO-BREAK SPACE
44 0x1680, # White_Space # Zs OGHAM SPACE MARK
45 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
46 (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
47 0x2028, # White_Space # Zl LINE SEPARATOR
48 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
49 0x202F, # White_Space # Zs NARROW NO-BREAK SPACE
50 0x205F, # White_Space # Zs MEDIUM MATHEMATICAL SPACE
51 0x3000, # White_Space # Zs IDEOGRAPHIC SPACE
52 ].flatten.map{|c| [c].pack('U*')}.freeze
53 REGEXEN[:spaces] = /[#{UNICODE_SPACES.join('')}]/o
54
55 # Character not allowed in Tweets
56 INVALID_CHARACTERS = [
57 0xFFFE, 0xFEFF, # BOM
58 0xFFFF, # Special
59 0x202A, 0x202B, 0x202C, 0x202D, 0x202E # Directional change
60 ].map{|cp| [cp].pack('U') }.freeze
61 REGEXEN[:invalid_control_characters] = /[#{INVALID_CHARACTERS.join('')}]/o
62
63 major, minor, _patch = RUBY_VERSION.split('.')
64 if major.to_i >= 2 || major.to_i == 1 && minor.to_i >= 9 || (defined?(RUBY_ENGINE) && ["jruby", "rbx"].include?(RUBY_ENGINE))
65 REGEXEN[:list_name] = /[a-z][a-z0-9_\-\u0080-\u00ff]{0,24}/i
66 else
67 # This line barfs at compile time in Ruby 1.9, JRuby, or Rubinius.
68 REGEXEN[:list_name] = eval("/[a-z][a-z0-9_\\-\x80-\xff]{0,24}/i")
69 end
70
71 # Latin accented characters
72 # Excludes 0xd7 from the range (the multiplication sign, confusable with "x").
73 # Also excludes 0xf7, the division sign
74 LATIN_ACCENTS = [
75 regex_range(0xc0, 0xd6),
76 regex_range(0xd8, 0xf6),
77 regex_range(0xf8, 0xff),
78 regex_range(0x0100, 0x024f),
79 regex_range(0x0253, 0x0254),
80 regex_range(0x0256, 0x0257),
81 regex_range(0x0259),
82 regex_range(0x025b),
83 regex_range(0x0263),
84 regex_range(0x0268),
85 regex_range(0x026f),
86 regex_range(0x0272),
87 regex_range(0x0289),
88 regex_range(0x028b),
89 regex_range(0x02bb),
90 regex_range(0x0300, 0x036f),
91 regex_range(0x1e00, 0x1eff)
92 ].join('').freeze
93 REGEXEN[:latin_accents] = /[#{LATIN_ACCENTS}]+/o
94
95 RTL_CHARACTERS = [
96 regex_range(0x0600,0x06FF),
97 regex_range(0x0750,0x077F),
98 regex_range(0x0590,0x05FF),
99 regex_range(0xFE70,0xFEFF)
100 ].join('').freeze
101
102 PUNCTUATION_CHARS = '!"#$%&\'()*+,-./:;<=>?@\[\]^_\`{|}~'
103 SPACE_CHARS = " \t\n\x0B\f\r"
104 CTRL_CHARS = "\x00-\x1F\x7F"
105
106 # Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Ruby's \p{L}\p{M}
107 HASHTAG_LETTERS_AND_MARKS = "\\p{L}\\p{M}" +
108 "\u037f\u0528-\u052f\u08a0-\u08b2\u08e4-\u08ff\u0978\u0980\u0c00\u0c34\u0c81\u0d01\u0ede\u0edf" +
109 "\u10c7\u10cd\u10fd-\u10ff\u16f1-\u16f8\u17b4\u17b5\u191d\u191e\u1ab0-\u1abe\u1bab-\u1bad\u1bba-" +
110 "\u1bbf\u1cf3-\u1cf6\u1cf8\u1cf9\u1de7-\u1df5\u2cf2\u2cf3\u2d27\u2d2d\u2d66\u2d67\u9fcc\ua674-" +
111 "\ua67b\ua698-\ua69d\ua69f\ua792-\ua79f\ua7aa-\ua7ad\ua7b0\ua7b1\ua7f7-\ua7f9\ua9e0-\ua9ef\ua9fa-" +
112 "\ua9fe\uaa7c-\uaa7f\uaae0-\uaaef\uaaf2-\uaaf6\uab30-\uab5a\uab5c-\uab5f\uab64\uab65\uf870-\uf87f" +
113 "\uf882\uf884-\uf89f\uf8b8\uf8c1-\uf8d6\ufa2e\ufa2f\ufe27-\ufe2d\u{102e0}\u{1031f}\u{10350}-\u{1037a}" +
114 "\u{10500}-\u{10527}\u{10530}-\u{10563}\u{10600}-\u{10736}\u{10740}-\u{10755}\u{10760}-\u{10767}" +
115 "\u{10860}-\u{10876}\u{10880}-\u{1089e}\u{10980}-\u{109b7}\u{109be}\u{109bf}\u{10a80}-\u{10a9c}" +
116 "\u{10ac0}-\u{10ac7}\u{10ac9}-\u{10ae6}\u{10b80}-\u{10b91}\u{1107f}\u{110d0}-\u{110e8}\u{11100}-" +
117 "\u{11134}\u{11150}-\u{11173}\u{11176}\u{11180}-\u{111c4}\u{111da}\u{11200}-\u{11211}\u{11213}-" +
118 "\u{11237}\u{112b0}-\u{112ea}\u{11301}-\u{11303}\u{11305}-\u{1130c}\u{1130f}\u{11310}\u{11313}-" +
119 "\u{11328}\u{1132a}-\u{11330}\u{11332}\u{11333}\u{11335}-\u{11339}\u{1133c}-\u{11344}\u{11347}" +
120 "\u{11348}\u{1134b}-\u{1134d}\u{11357}\u{1135d}-\u{11363}\u{11366}-\u{1136c}\u{11370}-\u{11374}" +
121 "\u{11480}-\u{114c5}\u{114c7}\u{11580}-\u{115b5}\u{115b8}-\u{115c0}\u{11600}-\u{11640}\u{11644}" +
122 "\u{11680}-\u{116b7}\u{118a0}-\u{118df}\u{118ff}\u{11ac0}-\u{11af8}\u{1236f}-\u{12398}\u{16a40}-" +
123 "\u{16a5e}\u{16ad0}-\u{16aed}\u{16af0}-\u{16af4}\u{16b00}-\u{16b36}\u{16b40}-\u{16b43}\u{16b63}-" +
124 "\u{16b77}\u{16b7d}-\u{16b8f}\u{16f00}-\u{16f44}\u{16f50}-\u{16f7e}\u{16f8f}-\u{16f9f}\u{1bc00}-" +
125 "\u{1bc6a}\u{1bc70}-\u{1bc7c}\u{1bc80}-\u{1bc88}\u{1bc90}-\u{1bc99}\u{1bc9d}\u{1bc9e}\u{1e800}-" +
126 "\u{1e8c4}\u{1e8d0}-\u{1e8d6}\u{1ee00}-\u{1ee03}\u{1ee05}-\u{1ee1f}\u{1ee21}\u{1ee22}\u{1ee24}" +
127 "\u{1ee27}\u{1ee29}-\u{1ee32}\u{1ee34}-\u{1ee37}\u{1ee39}\u{1ee3b}\u{1ee42}\u{1ee47}\u{1ee49}" +
128 "\u{1ee4b}\u{1ee4d}-\u{1ee4f}\u{1ee51}\u{1ee52}\u{1ee54}\u{1ee57}\u{1ee59}\u{1ee5b}\u{1ee5d}\u{1ee5f}" +
129 "\u{1ee61}\u{1ee62}\u{1ee64}\u{1ee67}-\u{1ee6a}\u{1ee6c}-\u{1ee72}\u{1ee74}-\u{1ee77}\u{1ee79}-" +
130 "\u{1ee7c}\u{1ee7e}\u{1ee80}-\u{1ee89}\u{1ee8b}-\u{1ee9b}\u{1eea1}-\u{1eea3}\u{1eea5}-\u{1eea9}" +
131 "\u{1eeab}-\u{1eebb}"
132
133 # Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Ruby's \p{Nd}
134 HASHTAG_NUMERALS = "\\p{Nd}" +
135 "\u0de6-\u0def\ua9f0-\ua9f9\u{110f0}-\u{110f9}\u{11136}-\u{1113f}\u{111d0}-\u{111d9}\u{112f0}-" +
136 "\u{112f9}\u{114d0}-\u{114d9}\u{11650}-\u{11659}\u{116c0}-\u{116c9}\u{118e0}-\u{118e9}\u{16a60}-" +
137 "\u{16a69}\u{16b50}-\u{16b59}"
138
139 HASHTAG_SPECIAL_CHARS = "_\u200c\u200d\ua67e\u05be\u05f3\u05f4\uff5e\u301c\u309b\u309c\u30a0\u30fb\u3003\u0f0b\u0f0c\u00b7"
140
141 HASHTAG_LETTERS_NUMERALS = "#{HASHTAG_LETTERS_AND_MARKS}#{HASHTAG_NUMERALS}#{HASHTAG_SPECIAL_CHARS}"
142 HASHTAG_LETTERS_NUMERALS_SET = "[#{HASHTAG_LETTERS_NUMERALS}]"
143 HASHTAG_LETTERS_SET = "[#{HASHTAG_LETTERS_AND_MARKS}]"
144
145 HASHTAG = /(\A|\ufe0e|\ufe0f|[^&#{HASHTAG_LETTERS_NUMERALS}])(#|#)(?!\ufe0f|\u20e3)(#{HASHTAG_LETTERS_NUMERALS_SET}*#{HASHTAG_LETTERS_SET}#{HASHTAG_LETTERS_NUMERALS_SET}*)/io
146
147 REGEXEN[:valid_hashtag] = /#{HASHTAG}/io
148 # Used in Extractor for final filtering
149 REGEXEN[:end_hashtag_match] = /\A(?:[##]|:\/\/)/o
150
151 REGEXEN[:valid_mention_preceding_chars] = /(?:[^a-z0-9_!#\$%&*@@]|^|(?:^|[^a-z0-9_+~.-])[rR][tT]:?)/io
152 REGEXEN[:at_signs] = /[@@]/
153 REGEXEN[:valid_mention_or_list] = /
154 (#{REGEXEN[:valid_mention_preceding_chars]}) # $1: Preceeding character
155 (#{REGEXEN[:at_signs]}) # $2: At mark
156 ([a-z0-9_]{1,20}) # $3: Screen name
157 (\/[a-z][a-zA-Z0-9_\-]{0,24})? # $4: List (optional)
158 /iox
159 REGEXEN[:valid_reply] = /^(?:#{REGEXEN[:spaces]})*#{REGEXEN[:at_signs]}([a-z0-9_]{1,20})/io
160 # Used in Extractor for final filtering
161 REGEXEN[:end_mention_match] = /\A(?:#{REGEXEN[:at_signs]}|#{REGEXEN[:latin_accents]}|:\/\/)/io
162
163 # URL related hash regex collection
164 REGEXEN[:valid_url_preceding_chars] = /(?:[^A-Z0-9@@$###{INVALID_CHARACTERS.join('')}]|^)/io
165 REGEXEN[:invalid_url_without_protocol_preceding_chars] = /[-_.\/]$/
166 DOMAIN_VALID_CHARS = "[^#{PUNCTUATION_CHARS}#{SPACE_CHARS}#{CTRL_CHARS}#{INVALID_CHARACTERS.join('')}#{UNICODE_SPACES.join('')}]"
167 REGEXEN[:valid_subdomain] = /(?:(?:#{DOMAIN_VALID_CHARS}(?:[_-]|#{DOMAIN_VALID_CHARS})*)?#{DOMAIN_VALID_CHARS}\.)/io
168 REGEXEN[:valid_domain_name] = /(?:(?:#{DOMAIN_VALID_CHARS}(?:[-]|#{DOMAIN_VALID_CHARS})*)?#{DOMAIN_VALID_CHARS}\.)/io
169
170 REGEXEN[:valid_gTLD] = %r{
171 (?:
172 (?:#{TLDS['generic'].join('|')})
173 (?=[^0-9a-z@]|$)
174 )
175 }ix
176
177 REGEXEN[:valid_ccTLD] = %r{
178 (?:
179 (?:#{TLDS['country'].join('|')})
180 (?=[^0-9a-z@]|$)
181 )
182 }ix
183 REGEXEN[:valid_punycode] = /(?:xn--[0-9a-z]+)/i
184
185 REGEXEN[:valid_special_cctld] = %r{
186 (?:
187 (?:co|tv)
188 (?=[^0-9a-z@]|$)
189 )
190 }ix
191
192 REGEXEN[:valid_domain] = /(?:
193 #{REGEXEN[:valid_subdomain]}*#{REGEXEN[:valid_domain_name]}
194 (?:#{REGEXEN[:valid_gTLD]}|#{REGEXEN[:valid_ccTLD]}|#{REGEXEN[:valid_punycode]})
195 )/iox
196
197 # This is used in Extractor
198 REGEXEN[:valid_ascii_domain] = /
199 (?:(?:[a-z0-9\-_]|#{REGEXEN[:latin_accents]})+\.)+
200 (?:#{REGEXEN[:valid_gTLD]}|#{REGEXEN[:valid_ccTLD]}|#{REGEXEN[:valid_punycode]})
201 /iox
202
203 # This is used in Extractor for stricter t.co URL extraction
204 REGEXEN[:valid_tco_url] = /^https?:\/\/t\.co\/([a-z0-9]+)/i
205
206 # This is used in Extractor to filter out unwanted URLs.
207 REGEXEN[:invalid_short_domain] = /\A#{REGEXEN[:valid_domain_name]}#{REGEXEN[:valid_ccTLD]}\Z/io
208 REGEXEN[:valid_special_short_domain] = /\A#{REGEXEN[:valid_domain_name]}#{REGEXEN[:valid_special_cctld]}\Z/io
209
210 REGEXEN[:valid_port_number] = /[0-9]+/
211
212 REGEXEN[:valid_general_url_path_chars] = /[a-z\p{Cyrillic}0-9!\*';:=\+\,\.\$\/%#\[\]\p{Pd}_~&\|@#{LATIN_ACCENTS}]/io
213 # Allow URL paths to contain up to two nested levels of balanced parens
214 # 1. Used in Wikipedia URLs like /Primer_(film)
215 # 2. Used in IIS sessions like /S(dfd346)/
216 # 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/
217 REGEXEN[:valid_url_balanced_parens] = /
218 \(
219 (?:
220 #{REGEXEN[:valid_general_url_path_chars]}+
221 |
222 # allow one nested level of balanced parentheses
223 (?:
224 #{REGEXEN[:valid_general_url_path_chars]}*
225 \(
226 #{REGEXEN[:valid_general_url_path_chars]}+
227 \)
228 #{REGEXEN[:valid_general_url_path_chars]}*
229 )
230 )
231 \)
232 /iox
233 # Valid end-of-path chracters (so /foo. does not gobble the period).
234 # 1. Allow =&# for empty URL parameters and other URL-join artifacts
235 REGEXEN[:valid_url_path_ending_chars] = /[a-z\p{Cyrillic}0-9=_#\/\+\-#{LATIN_ACCENTS}]|(?:#{REGEXEN[:valid_url_balanced_parens]})/io
236 REGEXEN[:valid_url_path] = /(?:
237 (?:
238 #{REGEXEN[:valid_general_url_path_chars]}*
239 (?:#{REGEXEN[:valid_url_balanced_parens]} #{REGEXEN[:valid_general_url_path_chars]}*)*
240 #{REGEXEN[:valid_url_path_ending_chars]}
241 )|(?:#{REGEXEN[:valid_general_url_path_chars]}+\/)
242 )/iox
243
244 REGEXEN[:valid_url_query_chars] = /[a-z0-9!?\*'\(\);:&=\+\$\/%#\[\]\-_\.,~|@]/i
245 REGEXEN[:valid_url_query_ending_chars] = /[a-z0-9_&=#\/\-]/i
246 REGEXEN[:valid_url] = %r{
247 ( # $1 total match
248 (#{REGEXEN[:valid_url_preceding_chars]}) # $2 Preceeding chracter
249 ( # $3 URL
250 (https?:\/\/)? # $4 Protocol (optional)
251 (#{REGEXEN[:valid_domain]}) # $5 Domain(s)
252 (?::(#{REGEXEN[:valid_port_number]}))? # $6 Port number (optional)
253 (/#{REGEXEN[:valid_url_path]}*)? # $7 URL Path and anchor
254 (\?#{REGEXEN[:valid_url_query_chars]}*#{REGEXEN[:valid_url_query_ending_chars]})? # $8 Query String
255 )
256 )
257 }iox
258
259 REGEXEN[:cashtag] = /[a-z]{1,6}(?:[._][a-z]{1,2})?/i
260 REGEXEN[:valid_cashtag] = /(^|#{REGEXEN[:spaces]})(\$)(#{REGEXEN[:cashtag]})(?=$|\s|[#{PUNCTUATION_CHARS}])/i
261
262 # These URL validation pattern strings are based on the ABNF from RFC 3986
263 REGEXEN[:validate_url_unreserved] = /[a-z\p{Cyrillic}0-9\p{Pd}._~]/i
264 REGEXEN[:validate_url_pct_encoded] = /(?:%[0-9a-f]{2})/i
265 REGEXEN[:validate_url_sub_delims] = /[!$&'()*+,;=]/i
266 REGEXEN[:validate_url_pchar] = /(?:
267 #{REGEXEN[:validate_url_unreserved]}|
268 #{REGEXEN[:validate_url_pct_encoded]}|
269 #{REGEXEN[:validate_url_sub_delims]}|
270 [:\|@]
271 )/iox
272
273 REGEXEN[:validate_url_scheme] = /(?:[a-z][a-z0-9+\-.]*)/i
274 REGEXEN[:validate_url_userinfo] = /(?:
275 #{REGEXEN[:validate_url_unreserved]}|
276 #{REGEXEN[:validate_url_pct_encoded]}|
277 #{REGEXEN[:validate_url_sub_delims]}|
278 :
279 )*/iox
280
281 REGEXEN[:validate_url_dec_octet] = /(?:[0-9]|(?:[1-9][0-9])|(?:1[0-9]{2})|(?:2[0-4][0-9])|(?:25[0-5]))/i
282 REGEXEN[:validate_url_ipv4] =
283 /(?:#{REGEXEN[:validate_url_dec_octet]}(?:\.#{REGEXEN[:validate_url_dec_octet]}){3})/iox
284
285 # Punting on real IPv6 validation for now
286 REGEXEN[:validate_url_ipv6] = /(?:\[[a-f0-9:\.]+\])/i
287
288 # Also punting on IPvFuture for now
289 REGEXEN[:validate_url_ip] = /(?:
290 #{REGEXEN[:validate_url_ipv4]}|
291 #{REGEXEN[:validate_url_ipv6]}
292 )/iox
293
294 # This is more strict than the rfc specifies
295 REGEXEN[:validate_url_subdomain_segment] = /(?:[a-z0-9](?:[a-z0-9_\-]*[a-z0-9])?)/i
296 REGEXEN[:validate_url_domain_segment] = /(?:[a-z0-9](?:[a-z0-9\-]*[a-z0-9])?)/i
297 REGEXEN[:validate_url_domain_tld] = /(?:[a-z](?:[a-z0-9\-]*[a-z0-9])?)/i
298 REGEXEN[:validate_url_domain] = /(?:(?:#{REGEXEN[:validate_url_subdomain_segment]}\.)*
299 (?:#{REGEXEN[:validate_url_domain_segment]}\.)
300 #{REGEXEN[:validate_url_domain_tld]})/iox
301
302 REGEXEN[:validate_url_host] = /(?:
303 #{REGEXEN[:validate_url_ip]}|
304 #{REGEXEN[:validate_url_domain]}
305 )/iox
306
307 # Unencoded internationalized domains - this doesn't check for invalid UTF-8 sequences
308 REGEXEN[:validate_url_unicode_subdomain_segment] =
309 /(?:(?:[a-z0-9]|[^\x00-\x7f])(?:(?:[a-z0-9_\-]|[^\x00-\x7f])*(?:[a-z0-9]|[^\x00-\x7f]))?)/ix
310 REGEXEN[:validate_url_unicode_domain_segment] =
311 /(?:(?:[a-z0-9]|[^\x00-\x7f])(?:(?:[a-z0-9\-]|[^\x00-\x7f])*(?:[a-z0-9]|[^\x00-\x7f]))?)/ix
312 REGEXEN[:validate_url_unicode_domain_tld] =
313 /(?:(?:[a-z]|[^\x00-\x7f])(?:(?:[a-z0-9\-]|[^\x00-\x7f])*(?:[a-z0-9]|[^\x00-\x7f]))?)/ix
314 REGEXEN[:validate_url_unicode_domain] = /(?:(?:#{REGEXEN[:validate_url_unicode_subdomain_segment]}\.)*
315 (?:#{REGEXEN[:validate_url_unicode_domain_segment]}\.)
316 #{REGEXEN[:validate_url_unicode_domain_tld]})/iox
317
318 REGEXEN[:validate_url_unicode_host] = /(?:
319 #{REGEXEN[:validate_url_ip]}|
320 #{REGEXEN[:validate_url_unicode_domain]}
321 )/iox
322
323 REGEXEN[:validate_url_port] = /[0-9]{1,5}/
324
325 REGEXEN[:validate_url_unicode_authority] = %r{
326 (?:(#{REGEXEN[:validate_url_userinfo]})@)? # $1 userinfo
327 (#{REGEXEN[:validate_url_unicode_host]}) # $2 host
328 (?::(#{REGEXEN[:validate_url_port]}))? # $3 port
329 }iox
330
331 REGEXEN[:validate_url_authority] = %r{
332 (?:(#{REGEXEN[:validate_url_userinfo]})@)? # $1 userinfo
333 (#{REGEXEN[:validate_url_host]}) # $2 host
334 (?::(#{REGEXEN[:validate_url_port]}))? # $3 port
335 }iox
336
337 REGEXEN[:validate_url_path] = %r{(/#{REGEXEN[:validate_url_pchar]}*)*}i
338 REGEXEN[:validate_url_query] = %r{(#{REGEXEN[:validate_url_pchar]}|/|\?)*}i
339 REGEXEN[:validate_url_fragment] = %r{(#{REGEXEN[:validate_url_pchar]}|/|\?)*}i
340
341 # Modified version of RFC 3986 Appendix B
342 REGEXEN[:validate_url_unencoded] = %r{
343 \A # Full URL
344 (?:
345 ([^:/?#]+):// # $1 Scheme
346 )?
347 ([^/?#]*) # $2 Authority
348 ([^?#]*) # $3 Path
349 (?:
350 \?([^#]*) # $4 Query
351 )?
352 (?:
353 \#(.*) # $5 Fragment
354 )?\Z
355 }ix
356
357 REGEXEN[:rtl_chars] = /[#{RTL_CHARACTERS}]/io
358
359 REGEXEN.each_pair{|k,v| v.freeze }
360
361 # Return the regular expression for a given <tt>key</tt>. If the <tt>key</tt>
362 # is not a known symbol a <tt>nil</tt> will be returned.
363 def self.[](key)
364 REGEXEN[key]
365 end
366 end
367 end
368 end
0 module Twitter
1 module TwitterText
2 # A module provides base methods to rewrite usernames, lists, hashtags and URLs.
3 module Rewriter extend self
4 def rewrite_entities(text, entities)
5 chars = text.to_s.to_char_a
6
7 # sort by start index
8 entities = entities.sort_by do |entity|
9 indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
10 indices.first
11 end
12
13 result = []
14 last_index = entities.inject(0) do |index, entity|
15 indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
16 result << chars[index...indices.first]
17 result << yield(entity, chars)
18 indices.last
19 end
20 result << chars[last_index..-1]
21
22 result.flatten.join
23 end
24
25 # These methods are deprecated, will be removed in future.
26 extend Deprecation
27
28 def rewrite(text, options = {})
29 [:hashtags, :urls, :usernames_or_lists].inject(text) do |key|
30 options[key] ? send(:"rewrite_#{key}", text, &options[key]) : text
31 end
32 end
33 deprecate :rewrite, :rewrite_entities
34
35 def rewrite_usernames_or_lists(text)
36 entities = Extractor.extract_mentions_or_lists_with_indices(text)
37 rewrite_entities(text, entities) do |entity, chars|
38 at = chars[entity[:indices].first]
39 list_slug = entity[:list_slug]
40 list_slug = nil if list_slug.empty?
41 yield(at, entity[:screen_name], list_slug)
42 end
43 end
44 deprecate :rewrite_usernames_or_lists, :rewrite_entities
45
46 def rewrite_hashtags(text)
47 entities = Extractor.extract_hashtags_with_indices(text)
48 rewrite_entities(text, entities) do |entity, chars|
49 hash = chars[entity[:indices].first]
50 yield(hash, entity[:hashtag])
51 end
52 end
53 deprecate :rewrite_hashtags, :rewrite_entities
54
55 def rewrite_urls(text)
56 entities = Extractor.extract_urls_with_indices(text, :extract_url_without_protocol => false)
57 rewrite_entities(text, entities) do |entity, chars|
58 yield(entity[:url])
59 end
60 end
61 deprecate :rewrite_urls, :rewrite_entities
62 end
63 end
64 end
0 module Twitter
1 module TwitterText
2 # This module lazily defines constants of the form Uxxxx for all Unicode
3 # codepoints from U0000 to U10FFFF. The value of each constant is the
4 # UTF-8 string for the codepoint.
5 # Examples:
6 # copyright = Unicode::U00A9
7 # euro = Unicode::U20AC
8 # infinity = Unicode::U221E
9 #
10 module Unicode
11 CODEPOINT_REGEX = /^U_?([0-9a-fA-F]{4,5}|10[0-9a-fA-F]{4})$/
12
13 def self.const_missing(name)
14 # Check that the constant name is of the right form: U0000 to U10FFFF
15 if name.to_s =~ CODEPOINT_REGEX
16 # Convert the codepoint to an immutable UTF-8 string,
17 # define a real constant for that value and return the value
18 #p name, name.class
19 const_set(name, [$1.to_i(16)].pack("U").freeze)
20 else # Raise an error for constants that are not Unicode.
21 raise NameError, "Uninitialized constant: Unicode::#{name}"
22 end
23 end
24 end
25 end
26 end
0 require 'unf'
1
2 module Twitter
3 module TwitterText
4 module Validation extend self
5 DEFAULT_TCO_URL_LENGTHS = {
6 :short_url_length => 23,
7 }
8
9 # :weighted_length the weighted length of tweet based on weights specified in the config
10 # :valid If tweet is valid
11 # :permillage permillage of the tweet over the max length specified in config
12 # :valid_range_start beginning of valid text
13 # :valid_range_end End index of valid part of the tweet text (inclusive)
14 # :display_range_start beginning index of display text
15 # :display_range_end end index of display text (inclusive)
16 class ParseResults < Hash
17
18 RESULT_PARAMS = [:weighted_length, :valid, :permillage, :valid_range_start, :valid_range_end, :display_range_start, :display_range_end]
19
20 def self.empty
21 return ParseResults.new(weighted_length: 0, permillage: 0, valid: true, display_range_start: 0, display_range_end: 0, valid_range_start: 0, valid_range_end: 0)
22 end
23
24 def initialize(params = {})
25 RESULT_PARAMS.each do |key|
26 super[key] = params[key] if params.key?(key)
27 end
28 end
29 end
30
31 # Parse input text and return hash with descriptive parameters populated.
32 def parse_tweet(text, options = {})
33 options = DEFAULT_TCO_URL_LENGTHS.merge(options)
34 config = options[:config] || Twitter::TwitterText::Configuration.default_configuration
35 normalized_text = text.to_nfc
36 normalized_text_length = normalized_text.char_length
37 unless (normalized_text_length > 0)
38 ParseResults.empty()
39 end
40
41 scale = config.scale
42 max_weighted_tweet_length = config.max_weighted_tweet_length
43 scaled_max_weighted_tweet_length = max_weighted_tweet_length * scale
44 transformed_url_length = config.transformed_url_length * scale
45 ranges = config.ranges
46
47 url_entities = Twitter::TwitterText::Extractor.extract_urls_with_indices(normalized_text)
48
49 has_invalid_chars = false
50 weighted_count = 0
51 offset = 0
52 display_offset = 0
53 valid_offset = 0
54
55 while offset < normalized_text_length
56 # Reset the default char weight each pass through the loop
57 char_weight = config.default_weight
58 url_entities.each do |url_entity|
59 if url_entity[:indices].first == offset
60 url_length = url_entity[:indices].last - url_entity[:indices].first
61 weighted_count += transformed_url_length
62 offset += url_length
63 display_offset += url_length
64 if weighted_count <= scaled_max_weighted_tweet_length
65 valid_offset += url_length
66 end
67 # Finding a match breaks the loop; order of ranges matters.
68 break
69 end
70 end
71
72 if offset < normalized_text_length
73 code_point = normalized_text[offset]
74
75 ranges.each do |range|
76 if range.contains?(code_point.unpack("U").first)
77 char_weight = range.weight
78 break
79 end
80 end
81
82 weighted_count += char_weight
83
84 has_invalid_chars = contains_invalid?(normalized_text[offset]) unless has_invalid_chars
85 char_count = code_point.char_length
86 offset += char_count
87 display_offset += char_count
88
89 if !has_invalid_chars && (weighted_count <= scaled_max_weighted_tweet_length)
90 valid_offset += char_count
91 end
92 end
93 end
94 normalized_text_offset = text.char_length - normalized_text.char_length
95 scaled_weighted_length = weighted_count / scale
96 is_valid = !has_invalid_chars && (scaled_weighted_length <= max_weighted_tweet_length)
97 permillage = scaled_weighted_length * 1000 / max_weighted_tweet_length
98
99 return ParseResults.new(weighted_length: scaled_weighted_length, permillage: permillage, valid: is_valid, display_range_start: 0, display_range_end: (display_offset + normalized_text_offset - 1), valid_range_start: 0, valid_range_end: (valid_offset + normalized_text_offset - 1))
100 end
101
102 def contains_invalid?(text)
103 return false if !text || text.empty?
104 begin
105 return true if Twitter::TwitterText::Regex::INVALID_CHARACTERS.any?{|invalid_char| text.include?(invalid_char) }
106 rescue ArgumentError
107 # non-Unicode value.
108 return true
109 end
110 return false
111 end
112
113 def valid_username?(username)
114 return false if !username || username.empty?
115
116 extracted = Twitter::TwitterText::Extractor.extract_mentioned_screen_names(username)
117 # Should extract the username minus the @ sign, hence the [1..-1]
118 extracted.size == 1 && extracted.first == username[1..-1]
119 end
120
121 VALID_LIST_RE = /\A#{Twitter::TwitterText::Regex[:valid_mention_or_list]}\z/o
122 def valid_list?(username_list)
123 match = username_list.match(VALID_LIST_RE)
124 # Must have matched and had nothing before or after
125 !!(match && match[1] == "" && match[4] && !match[4].empty?)
126 end
127
128 def valid_hashtag?(hashtag)
129 return false if !hashtag || hashtag.empty?
130
131 extracted = Twitter::TwitterText::Extractor.extract_hashtags(hashtag)
132 # Should extract the hashtag minus the # sign, hence the [1..-1]
133 extracted.size == 1 && extracted.first == hashtag[1..-1]
134 end
135
136 def valid_url?(url, unicode_domains=true, require_protocol=true)
137 return false if !url || url.empty?
138
139 url_parts = url.match(Twitter::TwitterText::Regex[:validate_url_unencoded])
140 return false unless (url_parts && url_parts.to_s == url)
141
142 scheme, authority, path, query, fragment = url_parts.captures
143
144 return false unless ((!require_protocol ||
145 (valid_match?(scheme, Twitter::TwitterText::Regex[:validate_url_scheme]) && scheme.match(/\Ahttps?\Z/i))) &&
146 valid_match?(path, Twitter::TwitterText::Regex[:validate_url_path]) &&
147 valid_match?(query, Twitter::TwitterText::Regex[:validate_url_query], true) &&
148 valid_match?(fragment, Twitter::TwitterText::Regex[:validate_url_fragment], true))
149
150 return (unicode_domains && valid_match?(authority, Twitter::TwitterText::Regex[:validate_url_unicode_authority])) ||
151 (!unicode_domains && valid_match?(authority, Twitter::TwitterText::Regex[:validate_url_authority]))
152 end
153
154 # These methods are deprecated, will be removed in future.
155 extend Deprecation
156
157 MAX_LENGTH_LEGACY = 140
158
159 # DEPRECATED: Please use parse_text instead.
160 #
161 # Returns the length of the string as it would be displayed. This is equivilent to the length of the Unicode NFC
162 # (See: http://www.unicode.org/reports/tr15). This is needed in order to consistently calculate the length of a
163 # string no matter which actual form was transmitted. For example:
164 #
165 # U+0065 Latin Small Letter E
166 # + U+0301 Combining Acute Accent
167 # ----------
168 # = 2 bytes, 2 characters, displayed as é (1 visual glyph)
169 # … The NFC of {U+0065, U+0301} is {U+00E9}, which is a single chracter and a +display_length+ of 1
170 #
171 # The string could also contain U+00E9 already, in which case the canonicalization will not change the value.
172 #
173 def tweet_length(text, options = {})
174 options = DEFAULT_TCO_URL_LENGTHS.merge(options)
175
176 length = text.to_nfc.unpack("U*").length
177
178 Twitter::TwitterText::Extractor.extract_urls_with_indices(text) do |url, start_position, end_position|
179 length += start_position - end_position
180 length += options[:short_url_length] if url.length > 0
181 end
182
183 length
184 end
185 deprecate :tweet_length, :parse_tweet
186
187 # DEPRECATED: Please use parse_text instead.
188 #
189 # Check the <tt>text</tt> for any reason that it may not be valid as a Tweet. This is meant as a pre-validation
190 # before posting to api.twitter.com. There are several server-side reasons for Tweets to fail but this pre-validation
191 # will allow quicker feedback.
192 #
193 # Returns <tt>false</tt> if this <tt>text</tt> is valid. Otherwise one of the following Symbols will be returned:
194 #
195 # <tt>:too_long</tt>:: if the <tt>text</tt> is too long
196 # <tt>:empty</tt>:: if the <tt>text</tt> is nil or empty
197 # <tt>:invalid_characters</tt>:: if the <tt>text</tt> contains non-Unicode or any of the disallowed Unicode characters
198 def tweet_invalid?(text)
199 return :empty if !text || text.empty?
200 begin
201 return :too_long if tweet_length(text) > MAX_LENGTH_LEGACY
202 return :invalid_characters if Twitter::TwitterText::Regex::INVALID_CHARACTERS.any?{|invalid_char| text.include?(invalid_char) }
203 rescue ArgumentError
204 # non-Unicode value.
205 return :invalid_characters
206 end
207
208 return false
209 end
210 deprecate :tweet_invalid?, :parse_tweet
211
212 def valid_tweet_text?(text)
213 !tweet_invalid?(text)
214 end
215 deprecate :valid_tweet_text?, :parse_tweet
216
217 private
218
219 def valid_match?(string, regex, optional=false)
220 return (string && string.match(regex) && $~.to_s == string) unless optional
221
222 !(string && (!string.match(regex) || $~.to_s != string))
223 end
224 end
225 end
226 end
0 # encoding: UTF-8
1
2 module Twitter
3 module TwitterText
4 class WeightedRange
5 attr_reader :start, :end, :weight
6
7 def initialize(range = {})
8 raise ArgumentError.new("Invalid range") unless [:start, :end, :weight].all? { |key| range.key?(key) && range[key].is_a?(Integer) }
9 @start = range[:start]
10 @end = range[:end]
11 @weight = range[:weight]
12 end
13
14 def contains?(code_point)
15 code_point >= @start && code_point <= @end
16 end
17 end
18 end
19 end
0 major, minor, _patch = RUBY_VERSION.split('.')
1
2 $RUBY_1_9 = if major.to_i == 1 && minor.to_i < 9
3 # Ruby 1.8 KCODE check. Not needed on 1.9 and later.
4 raise("twitter-text requires the $KCODE variable be set to 'UTF8' or 'u'") unless $KCODE[0].chr =~ /u/i
5 false
6 else
7 true
8 end
9
10 %w(
11 deprecation
12 regex
13 rewriter
14 autolink
15 extractor
16 unicode
17 weighted_range
18 configuration
19 validation
20 hit_highlighter
21 ).each do |name|
22 require "twitter-text/#{name}"
23 end
0 # Provides the validation functions that get included into a TypedArray
1
2 # Namespace TypedArray
3 module TypedArray
4
5 # The functions that get included into TypedArray
6 module Functions
7 # Validates outcome. See Array#initialize
8 def initialize *args, &block
9 ary = Array.new *args, &block
10 self.replace ary
11 end
12
13 # Validates outcome. See Array#replace
14 def replace other_ary
15 _ensure_all_items_in_array_are_allowed other_ary
16 super
17 end
18
19 # Validates outcome. See Array#&
20 def & ary
21 self.class.new super
22 end
23
24 # Validates outcome. See Array#*
25 def * int
26 self.class.new super
27 end
28
29 # Validates outcome. See Array#+
30 def + ary
31 self.class.new super
32 end
33
34 # Validates outcome. See Array#<<
35 def << item
36 _ensure_item_is_allowed item
37 super
38 end
39
40 # Validates outcome. See Array#[]
41 def [] idx
42 self.class.new super
43 end
44
45 # Validates outcome. See Array#slice
46 def slice *args
47 self.class.new super
48 end
49
50 # Validates outcome. See Array#[]=
51 def []= idx, item
52 _ensure_item_is_allowed item
53 super
54 end
55
56 # Validates outcome. See Array#concat
57 def concat other_ary
58 _ensure_all_items_in_array_are_allowed other_ary
59 super
60 end
61
62 # Validates outcome. See Array#eql?
63 def eql? other_ary
64 _ensure_all_items_in_array_are_allowed other_ary
65 super
66 end
67
68 # Validates outcome. See Array#fill
69 def fill *args, &block
70 ary = self.to_a
71 ary.fill *args, &block
72 self.replace ary
73 end
74
75 # Validates outcome. See Array#push
76 def push *items
77 _ensure_all_items_in_array_are_allowed items
78 super
79 end
80
81 # Validates outcome. See Array#unshift
82 def unshift *items
83 _ensure_all_items_in_array_are_allowed items
84 super
85 end
86
87 # Validates outcome. See Array#map!
88 def map! &block
89 self.replace( self.map &block )
90 end
91
92 protected
93
94 # Ensure that all items in the passed Array are allowed
95 def _ensure_all_items_in_array_are_allowed ary
96 # If we're getting an instance of self, accept
97 return true if ary.is_a? self.class
98 _ensure_item_is_allowed( ary, [Array] )
99 ary.each do |item|
100 _ensure_item_is_allowed(item)
101 end
102 end
103
104 # Ensure that the specific item passed is allowed
105 def _ensure_item_is_allowed item, expected=nil
106 return true if item.nil? #allow nil entries
107 expected = self.class.restricted_types if expected.nil?
108 expected.each do |allowed|
109 return true if item.class <= allowed
110 end
111 raise TypedArray::UnexpectedTypeException.new expected, item.class
112 end
113 end
114 end
0 # :include: ../README.rdoc
1
2 require "typed-array/functions"
3
4 # Provides TypedArray functionality to a subclass of Array
5 # when extended in the class's definiton
6 module TypedArray
7
8 # Hook the extension process in order to include the necessary functions
9 # and do some basic sanity checks.
10 def self.extended( mod )
11 unless mod <= Array
12 raise UnexpectedTypeException.new( [Array], mod.class )
13 end
14 mod.module_exec(self::Functions) do |functions_module|
15 include functions_module
16 end
17 end
18
19 # when a class inherits from this one, make sure that it also inherits
20 # the types that are being enforced
21 def inherited( subclass )
22 self._subclasses << subclass
23 subclass.restricted_types *restricted_types
24 end
25
26 # A getter/setter for types to add. If no arguments are passed, it simply
27 # returns the current array of accepted types.
28 def restricted_types(*types)
29 @_restricted_types ||= []
30 types.each do |type|
31 raise UnexpectedTypeException.new([Class],type.class) unless type.is_a? Class
32 @_restricted_types << type unless @_restricted_types.include? type
33 _subclasses.each do |subclass|
34 subclass.restricted_types type
35 end
36 end
37 @_restricted_types
38 end; alias :restricted_type :restricted_types
39
40 # The exception that is raised when an Unexpected Type is reached during validation
41 class UnexpectedTypeException < Exception
42 # Provide access to the types of objects expected and the class of the object received
43 attr_reader :expected, :received
44
45 def initialize expected_one_of, received
46 @expected = expected_one_of
47 @received = received
48 end
49
50 def to_s
51 %{Expected one of #{@expected.inspect} but received a(n) #{@received}}
52 end
53 end
54
55 protected
56
57 # a store of subclasses
58 def _subclasses
59 @_subclasses ||= []
60 end
61
62 end
63
64 # Provide a factory method. Takes any number of types to accept as arguments
65 # and returns a class that behaves as a type-enforced array.
66 def TypedArray *types_allowed
67 klass = Class.new( Array )
68 klass.class_exec(types_allowed) do |types_allowed|
69 extend TypedArray
70 restricted_types *types_allowed
71 restricted_types
72 end
73 klass.restricted_types
74 klass
75 end
0 require 'singleton'
1 if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
2 require 'unf/normalizer_jruby'
3 else
4 require 'unf/normalizer_cruby'
5 end
6
7 # UTF-8 string normalizer class. Implementations may vary depending
8 # on the platform.
9 class UNF::Normalizer
10 include Singleton
11
12 class << self
13 # :singleton-method: instance
14 #
15 # Returns a singleton normalizer instance.
16
17 # :singleton-method: new
18 #
19 # Returns a new normalizer instance. Use +singleton+ instead.
20 public :new
21
22 # A shortcut for instance.normalize(string, form).
23 def normalize(string, form)
24 instance.normalize(string, form)
25 end
26 end
27
28 # :method: normalize
29 # :call-seq:
30 # normalize(string, form)
31 #
32 # Normalizes a UTF-8 string into a given form (:nfc, :nfd, :nfkc or
33 # :nfkd).
34 end
0 require 'java'
1
2 module UNF # :nodoc: all
3 class Normalizer
4 def initialize()
5 @normalizer = java.text.Normalizer
6 end
7
8 def normalize(string, normalization_form)
9 @normalizer.normalize(string, form(normalization_form))
10 end
11
12 private
13
14 def form(symbol)
15 case symbol
16 when :nfc
17 @normalizer::Form::NFC
18 when :nfd
19 @normalizer::Form::NFD
20 when :nfkc
21 @normalizer::Form::NFKC
22 when :nfkd
23 @normalizer::Form::NFKD
24 else
25 raise ArgumentError, "unknown normalization form: #{symbol.inspect}"
26 end
27 end
28 end
29 end
0 module UNF
1 VERSION = '0.1.4'
2 end
0 require 'unf/version'
1
2 module UNF
3 autoload :Normalizer, 'unf/normalizer'
4 end
5
6 class String
7 ascii_only =
8 if method_defined?(:ascii_only?)
9 'ascii_only?'
10 else
11 '/[^\x00-\x7f]/ !~ self'
12 end
13
14 # :method: to_nfc
15 # Converts the string to NFC.
16
17 # :method: to_nfd
18 # Converts the string to NFD.
19
20 # :method: to_nfkc
21 # Converts the string to NFKC.
22
23 # :method: to_nfkd
24 # Converts the string to NFKD.
25
26 [:nfc, :nfd, :nfkc, :nfkd].each { |form|
27 eval %{
28 def to_#{form.to_s}
29 if #{ascii_only}
30 self
31 else
32 UNF::Normalizer.normalize(self, #{form.inspect})
33 end
34 end
35 }
36 }
37 end
0 module UNF
1 class Normalizer
2 VERSION = "0.0.7.5"
3 end
4 end
0 begin
1 require "#{RUBY_VERSION[/\A[0-9]+\.[0-9]+/]}/unf_ext.so"
2 rescue LoadError
3 require "unf_ext.so"
4 end
Binary diff not shown