sup 0.0.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sup might be problematic. Click here for more details.

Files changed (53) hide show
  1. data/History.txt +5 -0
  2. data/LICENSE +280 -0
  3. data/Manifest.txt +52 -0
  4. data/README.txt +119 -0
  5. data/Rakefile +45 -0
  6. data/bin/sup +229 -0
  7. data/bin/sup-import +162 -0
  8. data/doc/FAQ.txt +38 -0
  9. data/doc/Philosophy.txt +59 -0
  10. data/doc/TODO +31 -0
  11. data/lib/sup.rb +141 -0
  12. data/lib/sup/account.rb +53 -0
  13. data/lib/sup/buffer.rb +391 -0
  14. data/lib/sup/colormap.rb +118 -0
  15. data/lib/sup/contact.rb +40 -0
  16. data/lib/sup/draft.rb +105 -0
  17. data/lib/sup/index.rb +353 -0
  18. data/lib/sup/keymap.rb +89 -0
  19. data/lib/sup/label.rb +41 -0
  20. data/lib/sup/logger.rb +42 -0
  21. data/lib/sup/mbox.rb +51 -0
  22. data/lib/sup/mbox/loader.rb +116 -0
  23. data/lib/sup/message.rb +302 -0
  24. data/lib/sup/mode.rb +79 -0
  25. data/lib/sup/modes/buffer-list-mode.rb +37 -0
  26. data/lib/sup/modes/compose-mode.rb +33 -0
  27. data/lib/sup/modes/contact-list-mode.rb +121 -0
  28. data/lib/sup/modes/edit-message-mode.rb +162 -0
  29. data/lib/sup/modes/forward-mode.rb +38 -0
  30. data/lib/sup/modes/help-mode.rb +19 -0
  31. data/lib/sup/modes/inbox-mode.rb +45 -0
  32. data/lib/sup/modes/label-list-mode.rb +89 -0
  33. data/lib/sup/modes/label-search-results-mode.rb +29 -0
  34. data/lib/sup/modes/line-cursor-mode.rb +133 -0
  35. data/lib/sup/modes/log-mode.rb +44 -0
  36. data/lib/sup/modes/person-search-results-mode.rb +29 -0
  37. data/lib/sup/modes/poll-mode.rb +24 -0
  38. data/lib/sup/modes/reply-mode.rb +136 -0
  39. data/lib/sup/modes/resume-mode.rb +18 -0
  40. data/lib/sup/modes/scroll-mode.rb +106 -0
  41. data/lib/sup/modes/search-results-mode.rb +31 -0
  42. data/lib/sup/modes/text-mode.rb +51 -0
  43. data/lib/sup/modes/thread-index-mode.rb +389 -0
  44. data/lib/sup/modes/thread-view-mode.rb +338 -0
  45. data/lib/sup/person.rb +120 -0
  46. data/lib/sup/poll.rb +80 -0
  47. data/lib/sup/sent.rb +46 -0
  48. data/lib/sup/tagger.rb +40 -0
  49. data/lib/sup/textfield.rb +83 -0
  50. data/lib/sup/thread.rb +358 -0
  51. data/lib/sup/update.rb +21 -0
  52. data/lib/sup/util.rb +260 -0
  53. metadata +123 -0
@@ -0,0 +1,89 @@
1
+ require "curses"
2
+
3
+ module Redwood
4
+
5
+ class Keymap
6
+ def initialize
7
+ @map = {}
8
+ @order = []
9
+ yield self if block_given?
10
+ end
11
+
12
+ def keysym_to_keycode k
13
+ case k
14
+ when :down: Curses::KEY_DOWN
15
+ when :up: Curses::KEY_UP
16
+ when :left: Curses::KEY_LEFT
17
+ when :right: Curses::KEY_RIGHT
18
+ when :page_down: Curses::KEY_NPAGE
19
+ when :page_up: Curses::KEY_PPAGE
20
+ when :backspace: Curses::KEY_BACKSPACE
21
+ when :home: Curses::KEY_HOME
22
+ when :end: Curses::KEY_END
23
+ when :ctrl_l: "\f"[0]
24
+ when :ctrl_g: "\a"[0]
25
+ when :tab: "\t"[0]
26
+ when :enter, :return: 10 #Curses::KEY_ENTER
27
+ else
28
+ if k.is_a?(String) && k.length == 1
29
+ k[0]
30
+ else
31
+ raise ArgumentError, "unknown key name '#{k}'"
32
+ end
33
+ end
34
+ end
35
+
36
+ def keysym_to_string k
37
+ case k
38
+ when :down: "<down arrow>"
39
+ when :up: "<up arrow>"
40
+ when :left: "<left arrow>"
41
+ when :right: "<right arrow>"
42
+ when :page_down: "<page down>"
43
+ when :page_up: "<page up>"
44
+ when :backspace: "<backspace>"
45
+ when :home: "<home>"
46
+ when :end: "<end>"
47
+ when :enter, :return: "<enter>"
48
+ when :ctrl_l: "ctrl-l"
49
+ when :ctrl_l: "ctrl-g"
50
+ when :tab: "tab"
51
+ when " ": "<space>"
52
+ else
53
+ if k.is_a?(String) && k.length == 1
54
+ k
55
+ else
56
+ raise ArgumentError, "unknown key name \"#{k}\""
57
+ end
58
+ end
59
+ end
60
+
61
+ def add action, help, *keys
62
+ entry = [action, help, keys]
63
+ @order << entry
64
+ keys.each do |k|
65
+ raise ArgumentError, "key #{k} already defined (action #{action})" if @map.include? k
66
+ kc = keysym_to_keycode k
67
+ @map[kc] = entry
68
+ end
69
+ end
70
+
71
+ def action_for kc
72
+ action, help, keys = @map[kc]
73
+ action
74
+ end
75
+
76
+ def keysyms; @map.values.map { |action, help, keys| keys }.flatten; end
77
+
78
+ def help_text except_for={}
79
+ lines = @order.map do |action, help, keys|
80
+ valid_keys = keys.select { |k| !except_for[k] }
81
+ next if valid_keys.empty?
82
+ [valid_keys.map { |k| keysym_to_string k }.join(", "), help]
83
+ end.compact
84
+ llen = lines.map { |a, b| a.length }.max
85
+ lines.map { |a, b| sprintf " %#{llen}s : %s", a, b }.join("\n")
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,41 @@
1
+ module Redwood
2
+
3
+ class LabelManager
4
+ include Singleton
5
+
6
+ ## all labels that have special meaning. user will be unable to
7
+ ## add/remove these via normal label mechanisms.
8
+ RESERVED_LABELS = [ :starred, :spam, :draft, :unread, :killed, :sent ]
9
+
10
+ ## labels which it nonetheless makes sense to search for by
11
+ LISTABLE_LABELS = [ :starred, :spam, :draft, :sent ]
12
+
13
+ ## labels that will never be displayed to the user
14
+ HIDDEN_LABELS = [ :starred, :unread ]
15
+
16
+ def initialize fn
17
+ @fn = fn
18
+ labels =
19
+ if File.exists? fn
20
+ IO.readlines(fn).map { |x| x.chomp.intern }
21
+ else
22
+ []
23
+ end
24
+ @labels = {}
25
+ labels.each { |t| @labels[t] = true }
26
+
27
+ self.class.i_am_the_instance self
28
+ end
29
+
30
+ def user_labels; @labels.keys; end
31
+
32
+ def << t; @labels[t] = true unless @labels.member?(t) || RESERVED_LABELS.member?(t); end
33
+
34
+ def delete t; @labels.delete t; end
35
+
36
+ def save
37
+ File.open(@fn, "w") { |f| f.puts @labels.keys }
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,42 @@
1
+ module Redwood
2
+
3
+ class Logger
4
+ @@instance = nil
5
+
6
+ attr_reader :buf
7
+
8
+ def initialize
9
+ raise "only one Log can be defined" if @@instance
10
+ @@instance = self
11
+ @mode = LogMode.new
12
+ @respawn = true
13
+ @spawning = false # to prevent infinite loops!
14
+ end
15
+
16
+ ## must be called if you want to see anything!
17
+ ## once called, will respawn if killed...
18
+ def make_buf
19
+ return if @mode.buffer || !BufferManager.instantiated? || !@respawn || @spawning
20
+ @spawning = true
21
+ @mode.text = ""
22
+ @mode.buffer = BufferManager.instance.spawn "<log>", @mode, :hidden => true
23
+ @spawning = false
24
+ end
25
+
26
+ def log s
27
+ # $stderr.puts s
28
+ @mode << "#{Time.now}: #{s}\n"
29
+ make_buf
30
+ end
31
+
32
+ def self.method_missing m, *a
33
+ @@instance = Logger.new unless @@instance
34
+ @@instance.send m, *a
35
+ end
36
+
37
+ def self.buffer
38
+ @@instance.buf
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,51 @@
1
+ require "sup/mbox/loader"
2
+
3
+ module Redwood
4
+
5
+ ## some utility functions
6
+ module MBox
7
+ BREAK_RE = /^From \S+@\S+/
8
+
9
+ def read_header f
10
+ header = {}
11
+ last = nil
12
+
13
+ ## i do it in this weird way because i am trying to speed things up
14
+ ## at load-message time.
15
+ while(line = f.gets)
16
+ case line
17
+ when /^From:\s+(.*)$/i: header[last = "From"] = $1
18
+ when /^To:\s+(.*)$/i: header[last = "To"] = $1
19
+ when /^Cc:\s+(.*)$/i: header[last = "Cc"] = $1
20
+ when /^Bcc:\s+(.*)$/i: header[last = "Bcc"] = $1
21
+ when /^Subject:\s+(.*)$/i: header[last = "Subject"] = $1
22
+ when /^Date:\s+(.*)$/i: header[last = "Date"] = $1
23
+ when /^Message-Id:\s+<(.*)>$/i: header[last = "Message-Id"] = $1
24
+ when /^References:\s+(.*)$/i: header[last = "References"] = $1
25
+ when /^In-Reply-To:\s+(.*)$/i: header[last = "In-Reply-To"] = $1
26
+ when /^List-Post:\s+(.*)$/i: header[last = "List-Post"] = $1
27
+ when /^Reply-To:\s+(.*)$/i: header[last = "Reply-To"] = $1
28
+ when /^Status:\s+(.*)$/i: header[last = "Status"] = $1
29
+ when /^Delivered-To:\s+(.*)$/i
30
+ header[last = "Delivered-To"] = $1 unless header["Delivered-To"]
31
+ when /^$/: break
32
+ when /:/: last = nil
33
+ else
34
+ header[last] += line.gsub(/^\s+/, "") if last
35
+ end
36
+ end
37
+ header
38
+ end
39
+
40
+ def read_body f
41
+ body = []
42
+ f.each_line do |l|
43
+ break if l =~ BREAK_RE
44
+ body << l.chomp
45
+ end
46
+ body
47
+ end
48
+
49
+ module_function :read_header, :read_body
50
+ end
51
+ end
@@ -0,0 +1,116 @@
1
+ require 'thread'
2
+ require 'rmail'
3
+
4
+ module Redwood
5
+ module MBox
6
+
7
+ class Error < StandardError; end
8
+
9
+ class Loader
10
+ attr_reader :filename
11
+ bool_reader :usual, :archived, :read, :dirty
12
+ attr_accessor :id, :labels
13
+
14
+ ## end_offset is the last offsets within the file which we've read.
15
+ ## everything after that is considered new messages that haven't
16
+ ## been indexed.
17
+ def initialize filename, end_offset=0, usual=true, archived=false, id=nil
18
+ @filename = filename.gsub(%r(^mbox://), "")
19
+ @end_offset = end_offset
20
+ @dirty = false
21
+ @usual = usual
22
+ @archived = archived
23
+ @id = id
24
+ @mutex = Mutex.new
25
+ @f = File.open @filename
26
+ @labels = ([
27
+ :unread,
28
+ archived ? nil : :inbox,
29
+ ] +
30
+ if File.dirname(filename) =~ /\b(var|usr|spool)\b/
31
+ []
32
+ else
33
+ [File.basename(filename).intern]
34
+ end).compact
35
+ end
36
+
37
+ def reset!; @end_offset = 0; @dirty = true; end
38
+ def == o; o.is_a?(Loader) && o.filename == filename; end
39
+ def to_s; "mbox://#{@filename}"; end
40
+
41
+ def is_source_for? s
42
+ @filename == s || self.to_s == s
43
+ end
44
+
45
+ def load_header offset=nil
46
+ header = nil
47
+ @mutex.synchronize do
48
+ @f.seek offset if offset
49
+ header = MBox::read_header @f
50
+ end
51
+ header
52
+ end
53
+
54
+ def load_message offset
55
+ ret = nil
56
+ @mutex.synchronize do
57
+ @f.seek offset
58
+ RMail::Mailbox::MBoxReader.new(@f).each_message do |input|
59
+ return RMail::Parser.read(input)
60
+ end
61
+ end
62
+ end
63
+
64
+ ## load the full header text
65
+ def load_header_text offset
66
+ ret = ""
67
+ @mutex.synchronize do
68
+ @f.seek offset
69
+ until @f.eof? || (l = @f.gets) =~ /^$/
70
+ ret += l
71
+ end
72
+ end
73
+ ret
74
+ end
75
+
76
+ def next
77
+ return nil if done?
78
+ @dirty = true
79
+ next_end_offset = @end_offset
80
+
81
+ @mutex.synchronize do
82
+ @f.seek @end_offset
83
+
84
+ @f.gets # skip the From separator
85
+ next_end_offset = @f.tell
86
+ while(line = @f.gets)
87
+ break if line =~ BREAK_RE
88
+ next_end_offset = @f.tell + 1
89
+ end
90
+ end
91
+
92
+ start_offset = @end_offset
93
+ @end_offset = next_end_offset
94
+
95
+ start_offset
96
+ end
97
+
98
+ def each
99
+ until @end_offset >= File.size(@f)
100
+ n = self.next
101
+ yield(n, labels) if n
102
+ end
103
+ end
104
+
105
+ def each_header
106
+ each { |offset, labels| yield offset, labels, load_header(offset) }
107
+ end
108
+
109
+ def done?; @end_offset >= File.size(@f); end
110
+ def total; File.size @f; end
111
+ end
112
+
113
+ Redwood::register_yaml(Loader, %w(filename end_offset usual archived id))
114
+
115
+ end
116
+ end
@@ -0,0 +1,302 @@
1
+ require 'tempfile'
2
+ require 'time'
3
+
4
+ module Redwood
5
+
6
+ class MessageFormatError < StandardError; end
7
+
8
+ ## a Message is what's threaded.
9
+ ##
10
+ ## it is also where the parsing for quotes and signatures is done, but
11
+ ## that should be moved out to a separate class at some point (because
12
+ ## i would like, for example, to be able to add in a ruby-talk
13
+ ## specific module that would detect and link to /ruby-talk:\d+/
14
+ ## sequences in the text of an email. (how sweet would that be?)
15
+ ##
16
+ ## TODO: integrate with user's addressbook to render names
17
+ ## appropriately.
18
+ class Message
19
+ SNIPPET_LEN = 80
20
+ RE_PATTERN = /^((re|re[\[\(]\d[\]\)]):\s*)+/i
21
+
22
+ ## some utility methods
23
+ class << self
24
+ def normalize_subj s; s.gsub(RE_PATTERN, ""); end
25
+ def subj_is_reply? s; s =~ RE_PATTERN; end
26
+ def reify_subj s; subj_is_reply?(s) ? s : "Re: " + s; end
27
+ end
28
+
29
+ class Attachment
30
+ attr_reader :content_type, :desc
31
+ def initialize content_type, desc, part
32
+ @content_type = content_type
33
+ @desc = desc
34
+ @part = part
35
+ @file = nil
36
+ end
37
+
38
+ def view!
39
+ unless @file
40
+ @file = Tempfile.new "redwood.attachment"
41
+ @file.print @part.decode
42
+ @file.close
43
+ end
44
+
45
+ ## TODO: handle unknown mime-types
46
+ system "/usr/bin/run-mailcap --action=view #{@content_type}:#{@file.path}"
47
+ end
48
+ end
49
+
50
+ class Text
51
+ attr_reader :lines
52
+ def initialize lines
53
+ ## do some wrapping
54
+ @lines = lines.map { |l| l.wrap 80 }.flatten
55
+ end
56
+ end
57
+
58
+ class Quote
59
+ attr_reader :lines
60
+ def initialize lines
61
+ @lines = lines
62
+ end
63
+ end
64
+
65
+ class Signature
66
+ attr_reader :lines
67
+ def initialize lines
68
+ @lines = lines
69
+ end
70
+ end
71
+
72
+ QUOTE_PATTERN = /^\s{0,4}[>|\}]/
73
+ BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
74
+ QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
75
+ SIG_PATTERN = /(^-- ?$)|(^\s*----------+\s*$)|(^\s*_________+\s*$)/
76
+ SIG_DISTANCE = 15 # lines from the end
77
+ DEFAULT_SUBJECT = "(missing subject)"
78
+ DEFAULT_SENDER = "(missing sender)"
79
+
80
+ attr_reader :id, :date, :from, :subj, :refs, :replytos, :to, :source,
81
+ :cc, :bcc, :labels, :list_address, :recipient_email, :replyto,
82
+ :source_info, :mbox_status
83
+
84
+ bool_reader :dirty
85
+
86
+ def initialize source, source_info, labels, snippet=nil
87
+ @source = source
88
+ @source_info = source_info
89
+ @dirty = false
90
+ @snippet = snippet
91
+ @labels = labels
92
+
93
+ header = @source.load_header @source_info
94
+ header.each { |k, v| header[k.downcase] = v }
95
+
96
+ %w(message-id date).each do |f|
97
+ raise MessageFormatError, "no #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header.include? f
98
+ raise MessageFormatError, "nil #{f} field in header #{header.inspect} (source #@source offset #@source_info)" unless header[f]
99
+ end
100
+
101
+ begin
102
+ @date = Time.parse header["date"]
103
+ rescue ArgumentError => e
104
+ raise MessageFormatError, "unparsable date #{header['date']}: #{e.message}"
105
+ end
106
+
107
+ if(@subj = header["subject"])
108
+ @subj = @subj.gsub(/\s+/, " ").gsub(/\s+$/, "")
109
+ else
110
+ @subj = DEFAULT_SUBJECT
111
+ end
112
+ @from = Person.for header["from"]
113
+ @to = Person.for_several header["to"]
114
+ @cc = Person.for_several header["cc"]
115
+ @bcc = Person.for_several header["bcc"]
116
+ @id = header["message-id"]
117
+ @refs = (header["references"] || "").scan(/<(.*?)>/).flatten
118
+ @replytos = (header["in-reply-to"] || "").scan(/<(.*?)>/).flatten
119
+ @replyto = Person.for header["reply-to"]
120
+ @list_address =
121
+ if header["list-post"]
122
+ @list_address = Person.for header["list-post"].gsub(/^<mailto:|>$/, "")
123
+ else
124
+ nil
125
+ end
126
+
127
+ @recipient_email = header["delivered-to"]
128
+ @mbox_status = header["status"]
129
+ end
130
+
131
+ def snippet
132
+ to_chunks unless @snippet
133
+ @snippet
134
+ end
135
+
136
+ def is_list_message?; !@list_address.nil?; end
137
+ def is_draft?; DraftLoader === @source; end
138
+ def draft_filename
139
+ raise "not a draft" unless is_draft?
140
+ @source.fn_for_offset @source_info
141
+ end
142
+
143
+ def save index
144
+ index.update_message self if @dirty
145
+ @dirty = false
146
+ end
147
+
148
+ def has_label? t; @labels.member? t; end
149
+ def add_label t
150
+ return if @labels.member? t
151
+ @labels.push t
152
+ @dirty = true
153
+ end
154
+ def remove_label t
155
+ return unless @labels.member? t
156
+ @labels.delete t
157
+ @dirty = true
158
+ end
159
+
160
+ def recipients
161
+ @to + @cc + @bcc
162
+ end
163
+
164
+ def labels= l
165
+ @labels = l
166
+ @dirty = true
167
+ end
168
+
169
+ def to_chunks
170
+ m = @source.load_message @source_info
171
+ message_to_chunks m
172
+ end
173
+
174
+ def header_text
175
+ @source.load_header_text @source_info
176
+ end
177
+
178
+ def content
179
+ [
180
+ from && from.longname,
181
+ to.map { |p| p.longname },
182
+ cc.map { |p| p.longname },
183
+ bcc.map { |p| p.longname },
184
+ to_chunks.select { |c| c.is_a? Text }.map { |c| c.lines },
185
+ subj,
186
+ ].flatten.compact.join " "
187
+ end
188
+
189
+ def basic_body_lines
190
+ to_chunks.find_all { |c| c.is_a?(Text) || c.is_a?(Quote) }.map { |c| c.lines }.flatten
191
+ end
192
+
193
+ def basic_header_lines
194
+ ["From: #{@from.full_address}"] +
195
+ (@to.empty? ? [] : ["To: " + @to.map { |p| p.full_address }.join(", ")]) +
196
+ (@cc.empty? ? [] : ["Cc: " + @cc.map { |p| p.full_address }.join(", ")]) +
197
+ (@bcc.empty? ? [] : ["Bcc: " + @bcc.map { |p| p.full_address }.join(", ")]) +
198
+ ["Date: #{@date.rfc822}",
199
+ "Subject: #{@subj}"]
200
+ end
201
+
202
+ private
203
+
204
+ ## everything RubyMail-specific goes here.
205
+ def message_to_chunks m
206
+ ret = [] <<
207
+ case m.header.content_type
208
+ when "text/plain", nil
209
+ raise MessageFormatError, "no message body before decode" unless
210
+ m.body
211
+ body = m.decode or raise MessageFormatError, "no message body"
212
+ text_to_chunks body.gsub(/\t/, " ").gsub(/\r/, "").split("\n")
213
+ when "multipart/alternative", "multipart/mixed"
214
+ nil
215
+ else
216
+ disp = m.header["Content-Disposition"] || ""
217
+ Attachment.new m.header.content_type, disp.gsub(/[\s\n]+/, " "), m
218
+ end
219
+
220
+ m.each_part { |p| ret << message_to_chunks(p) } if m.multipart?
221
+ ret.compact.flatten
222
+ end
223
+
224
+ ## parse the lines of text into chunk objects. the heuristics here
225
+ ## need tweaking in some nice manner. TODO: move these heuristics
226
+ ## into the classes themselves.
227
+
228
+ def text_to_chunks lines
229
+ state = :text # one of :text, :quote, or :sig
230
+ chunks = []
231
+ chunk_lines = []
232
+
233
+ lines.each_with_index do |line, i|
234
+ nextline = lines[(i + 1) ... lines.length].find { |l| l !~ /^\s*$/ } # skip blank lines
235
+ case state
236
+ when :text
237
+ newstate = nil
238
+ if line =~ QUOTE_PATTERN || (line =~ QUOTE_START_PATTERN && (nextline =~ QUOTE_PATTERN || nextline =~ QUOTE_START_PATTERN))
239
+ newstate = :quote
240
+ elsif line =~ SIG_PATTERN && (lines.length - i) < SIG_DISTANCE
241
+ newstate = :sig
242
+ elsif line =~ BLOCK_QUOTE_PATTERN
243
+ newstate = :block_quote
244
+ end
245
+ if newstate
246
+ chunks << Text.new(chunk_lines) unless chunk_lines.empty?
247
+ chunk_lines = [line]
248
+ state = newstate
249
+ else
250
+ chunk_lines << line
251
+ end
252
+ when :quote
253
+ newstate = nil
254
+ if line =~ QUOTE_PATTERN || line =~ QUOTE_START_PATTERN || line =~ /^\s*$/
255
+ chunk_lines << line
256
+ elsif line =~ SIG_PATTERN && (lines.length - i) < SIG_DISTANCE
257
+ newstate = :sig
258
+ else
259
+ newstate = :text
260
+ end
261
+ if newstate
262
+ if chunk_lines.empty?
263
+ # nothing
264
+ elsif chunk_lines.size == 1
265
+ chunks << Text.new(chunk_lines) # forget about one-line quotes
266
+ else
267
+ chunks << Quote.new(chunk_lines)
268
+ end
269
+ chunk_lines = [line]
270
+ state = newstate
271
+ end
272
+ when :block_quote
273
+ chunk_lines << line
274
+ when :sig
275
+ chunk_lines << line
276
+ end
277
+
278
+ if state == :text && (@snippet.nil? || @snippet.length < SNIPPET_LEN) &&
279
+ line !~ /[=\*#_-]{3,}/ && line !~ /^\s*$/
280
+ @snippet = (@snippet ? @snippet + " " : "") + line.gsub(/^\s+/, "").gsub(/[\r\n]/, "").gsub(/\s+/, " ")
281
+ @snippet = @snippet[0 ... SNIPPET_LEN]
282
+ end
283
+ # if @snippet.nil? && state == :text && (line.length > 40 ||
284
+ # line =~ /\S+.*[^,!:]\s*$/)
285
+ # @snippet = line.gsub(/^\s+/, "").gsub(/[\r\n]/, "")[0 .. 80]
286
+ # end
287
+ end
288
+
289
+ ## final object
290
+ case state
291
+ when :quote, :block_quote
292
+ chunks << Quote.new(chunk_lines) unless chunk_lines.empty?
293
+ when :text
294
+ chunks << Text.new(chunk_lines) unless chunk_lines.empty?
295
+ when :sig
296
+ chunks << Signature.new(chunk_lines) unless chunk_lines.empty?
297
+ end
298
+ chunks
299
+ end
300
+ end
301
+
302
+ end