fastreader 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +6 -0
- data/Manifest.txt +7 -0
- data/README.txt +91 -0
- data/bin/fastreader +55 -0
- data/db/default.sqlite3 +0 -0
- data/lib/autodiscovery.rb +24 -0
- data/lib/character_cleaner.rb +17 -0
- data/lib/command_parser.rb +17 -0
- data/lib/command_window.rb +112 -0
- data/lib/curses_color.rb +1391 -0
- data/lib/curses_controller.rb +176 -0
- data/lib/curses_extensions.rb +161 -0
- data/lib/display.rb +232 -0
- data/lib/entries_controller.rb +270 -0
- data/lib/entry.rb +21 -0
- data/lib/entry_controller.rb +131 -0
- data/lib/entry_window.rb +150 -0
- data/lib/fastreader.rb +283 -0
- data/lib/feed.rb +222 -0
- data/lib/feeds_controller.rb +240 -0
- data/lib/htmlentities.rb +165 -0
- data/lib/htmlentities/html4.rb +257 -0
- data/lib/htmlentities/legacy.rb +27 -0
- data/lib/htmlentities/string.rb +26 -0
- data/lib/htmlentities/xhtml1.rb +258 -0
- data/lib/menu_pager.rb +71 -0
- data/lib/menu_window.rb +419 -0
- data/lib/opml.rb +16 -0
- data/lib/virtual_feed.rb +39 -0
- data/test/test_fastreader.rb +0 -0
- metadata +162 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
require 'curses'
|
|
2
|
+
require 'iconv'
|
|
3
|
+
include Curses
|
|
4
|
+
|
|
5
|
+
class CursesController
|
|
6
|
+
include EntriesController
|
|
7
|
+
include FeedsController
|
|
8
|
+
include EntryController
|
|
9
|
+
|
|
10
|
+
def log(statement)
|
|
11
|
+
LOGGER.debug(statement)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show_help(text)
|
|
15
|
+
@command_window.escape_help_prompt
|
|
16
|
+
@help_window = Curses::Window.new(@scr.maxy - 4, @scr.maxx, 0, 0)
|
|
17
|
+
@text_area = Curses::Window.new(@scr.maxy - 4, @scr.maxx - 4, 2, 5)
|
|
18
|
+
@help_window.clear
|
|
19
|
+
@text_area.addstr(PREAMBLE)
|
|
20
|
+
@text_area.addstr(text)
|
|
21
|
+
@text_area.addstr(FOOTER)
|
|
22
|
+
@help_window.refresh
|
|
23
|
+
@text_area.refresh
|
|
24
|
+
@scr.getch
|
|
25
|
+
@text_area.close
|
|
26
|
+
@help_window.close
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
PREAMBLE = <<END
|
|
30
|
+
FastReader Terminal Feed Reader Command Reference
|
|
31
|
+
|
|
32
|
+
END
|
|
33
|
+
|
|
34
|
+
FOOTER = <<END
|
|
35
|
+
|
|
36
|
+
by Daniel Choi, June 2008
|
|
37
|
+
Cambridge, Massachusetts, USA
|
|
38
|
+
dhchoi@gmail.com
|
|
39
|
+
|
|
40
|
+
License: GPL
|
|
41
|
+
END
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
FEEDS_MENU_COMMANDS = <<-END
|
|
45
|
+
Feed Menu Navigation
|
|
46
|
+
|
|
47
|
+
The standard Vi bindings h, j, k, l move left, down, up, and right. You can
|
|
48
|
+
also use the arrow keys. Right in this case drills down the hierachy
|
|
49
|
+
(goes from the feed list to the entry list for a feed to an entry), while
|
|
50
|
+
left goes back up the hierarchy.
|
|
51
|
+
|
|
52
|
+
You can prefix a up or down movement command with a number to go that many
|
|
53
|
+
steps in that direction.
|
|
54
|
+
|
|
55
|
+
CTRL-f Page down
|
|
56
|
+
CTRL-b Page up
|
|
57
|
+
H Move to top of screen
|
|
58
|
+
M Move to top middle of screen
|
|
59
|
+
L Move to bottom of screen
|
|
60
|
+
G Move to last item
|
|
61
|
+
1G Move to first item
|
|
62
|
+
l, RIGHT ARROW, RETURN Select a feed and display its entries
|
|
63
|
+
|
|
64
|
+
You can also type the feed's number and press RETURN or RIGHT ARROW to select
|
|
65
|
+
it.
|
|
66
|
+
|
|
67
|
+
Actions
|
|
68
|
+
|
|
69
|
+
q Quit the program
|
|
70
|
+
u Updates the feed; won't update with last update was within the last hour
|
|
71
|
+
U Forces an update of the feed
|
|
72
|
+
CTRL-u Updates all feeds
|
|
73
|
+
* It's better to update the feeds from the command line because the
|
|
74
|
+
feedback if easier to follow. Use the command 'fastreader update'.
|
|
75
|
+
d Deletes the feed. This will also delete any flagged items from this feed.
|
|
76
|
+
a Adds a feed; enter the url of the webpage with a feed autodiscovery link
|
|
77
|
+
or the URL of the feed itself
|
|
78
|
+
/ Start a global search; asks for a search string and will find all entry
|
|
79
|
+
items in the database that match
|
|
80
|
+
|
|
81
|
+
END
|
|
82
|
+
|
|
83
|
+
ENTRIES_MENU_COMMANDS = <<-END
|
|
84
|
+
Feed Items Navigation
|
|
85
|
+
|
|
86
|
+
The standard Vi bindings h, j, k, l move left, down, up, and right. You can
|
|
87
|
+
also use the arrow keys. Right in this case drills down the hierachy
|
|
88
|
+
(goes from the feed list to the item list for a feed to an item), while
|
|
89
|
+
left goes back up the hierarchy.
|
|
90
|
+
|
|
91
|
+
You can prefix a up or down movement command with a number to go that many
|
|
92
|
+
steps in that direction.
|
|
93
|
+
|
|
94
|
+
CTRL-f Page down
|
|
95
|
+
CTRL-b Page up
|
|
96
|
+
H Move to top of screen
|
|
97
|
+
M Move to top middle of screen
|
|
98
|
+
L Move to bottom of screen
|
|
99
|
+
G Move to last item
|
|
100
|
+
1G Move to first item
|
|
101
|
+
l, RIGHT ARROW, RETURN Select an item and display its content
|
|
102
|
+
|
|
103
|
+
Actions
|
|
104
|
+
|
|
105
|
+
q Quit the program
|
|
106
|
+
/ Start a local search of the item titles. This lets you jump to a
|
|
107
|
+
particular item quickly.
|
|
108
|
+
s, * Flag an item. Flagged items appears in a virtual feed called
|
|
109
|
+
"Flagged Entries."
|
|
110
|
+
A Flag all items in the list
|
|
111
|
+
Z Unflag all items in the list
|
|
112
|
+
D Delete all the items from the cursor position downward. Flagged items will
|
|
113
|
+
be preserved.
|
|
114
|
+
u Updates the feed; won't update with last update was within the last hour
|
|
115
|
+
U Forces an update of the feed
|
|
116
|
+
END
|
|
117
|
+
|
|
118
|
+
ENTRY_PAGE_COMMANDS = <<-END
|
|
119
|
+
Feed Item Content Navigation
|
|
120
|
+
|
|
121
|
+
j, DOWN ARROW, CTRL-f Page down (if there is more than one page)
|
|
122
|
+
k, UP ARROW, CTRL-b Page up
|
|
123
|
+
h, LEFT ARROW Go back to entries list
|
|
124
|
+
l, RIGHT ARROW, RETURN Go to the web page linked to this entry.
|
|
125
|
+
|
|
126
|
+
By default, the command to open the web browser is "open {url}." This works out
|
|
127
|
+
of the box on Mac OS X. You can change this command by setting your shell's
|
|
128
|
+
FASTREADER_WEB environmental variable. Currently, in-terminal text browsers like
|
|
129
|
+
elinks do not work with Fastreader. Hopefully this will change.
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
>, . Go to next entry in the list
|
|
133
|
+
<, , Go to previous entry in the list
|
|
134
|
+
|
|
135
|
+
Actions
|
|
136
|
+
|
|
137
|
+
q Quit the program
|
|
138
|
+
s, * Flag an entry. Flagged entries appears in a virtual feed called
|
|
139
|
+
"Flagged Entries."
|
|
140
|
+
\\ Show raw entry content markup
|
|
141
|
+
|
|
142
|
+
END
|
|
143
|
+
|
|
144
|
+
def initialize(formatter)
|
|
145
|
+
# Formatter is the Display class. Should change the variable names to match the class
|
|
146
|
+
# names or vice versa.
|
|
147
|
+
@formatter = formatter
|
|
148
|
+
@scr = init_screen
|
|
149
|
+
noecho
|
|
150
|
+
cbreak
|
|
151
|
+
Color.init
|
|
152
|
+
curs_set(0)
|
|
153
|
+
@scr.keypad = true
|
|
154
|
+
log "Screen width: #{@scr.maxx} | Screen height: #{@scr.maxy}."
|
|
155
|
+
log "Command window initilized."
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def key_buffer(initial_char)
|
|
159
|
+
c = initial_char
|
|
160
|
+
buffer = []
|
|
161
|
+
printable = (33..126).to_a
|
|
162
|
+
while printable.include?(c) && c.chr =~ /\d/
|
|
163
|
+
LOGGER.debug("detecting a multiplier: #{c.chr}")
|
|
164
|
+
# TODO This code doesn't work right
|
|
165
|
+
buffer << c.chr.to_i
|
|
166
|
+
c = @scr.getch
|
|
167
|
+
end
|
|
168
|
+
return c, buffer
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def control_key(character)
|
|
172
|
+
character[0] - 64
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
end
|
|
176
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# This code is taken from Takashi Nakamoto's GPLType example program
|
|
2
|
+
|
|
3
|
+
#################################################################
|
|
4
|
+
# Curses::Key
|
|
5
|
+
#################################################################
|
|
6
|
+
module Curses::Key
|
|
7
|
+
BS = 8
|
|
8
|
+
HT = 9
|
|
9
|
+
LF = 10
|
|
10
|
+
CR = 13
|
|
11
|
+
ESC = 27
|
|
12
|
+
SP = 32
|
|
13
|
+
DEL = 127
|
|
14
|
+
|
|
15
|
+
KEY_GROUP = {
|
|
16
|
+
:backspace => [BS, BACKSPACE, DEL],
|
|
17
|
+
:escape => [ESC, BREAK],
|
|
18
|
+
:space => [SP, HT],
|
|
19
|
+
:enter => [ENTER, LF, CR],
|
|
20
|
+
:printable => (33..126).to_a,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def match?(key_type, key_code)
|
|
24
|
+
if key_type.is_a?(Array)
|
|
25
|
+
key_type.map{ |type| KEY_GROUP[type].include?(key_code)}.include?(true)
|
|
26
|
+
elsif key_type.is_a?(Symbol)
|
|
27
|
+
KEY_GROUP[key_type].include?(key_code)
|
|
28
|
+
else
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
module_function :match?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
#################################################################
|
|
36
|
+
# Curses::Color
|
|
37
|
+
#################################################################
|
|
38
|
+
module Curses::Color
|
|
39
|
+
RED = Curses::COLOR_RED
|
|
40
|
+
BLUE = Curses::COLOR_BLUE
|
|
41
|
+
GREEN = Curses::COLOR_GREEN
|
|
42
|
+
YELLOW = Curses::COLOR_YELLOW
|
|
43
|
+
MAGENTA = Curses::COLOR_MAGENTA
|
|
44
|
+
CYAN = Curses::COLOR_CYAN
|
|
45
|
+
WHITE = Curses::COLOR_WHITE
|
|
46
|
+
BLACK = Curses::COLOR_BLACK
|
|
47
|
+
|
|
48
|
+
COLOR_STYLES = {
|
|
49
|
+
# :name => [foreground, background],
|
|
50
|
+
:default => [WHITE, BLACK],
|
|
51
|
+
:red => [RED, BLACK],
|
|
52
|
+
:blue => [BLUE, BLACK],
|
|
53
|
+
:green => [GREEN, BLACK],
|
|
54
|
+
:yellow => [YELLOW, BLACK],
|
|
55
|
+
:magenta => [MAGENTA, BLACK],
|
|
56
|
+
:cyan => [CYAN, BLACK],
|
|
57
|
+
:wrong => [RED, BLACK],
|
|
58
|
+
:wrong_space => [RED, RED],
|
|
59
|
+
:correct => [BLUE, BLACK],
|
|
60
|
+
:ok => [CYAN, BLACK],
|
|
61
|
+
:highscore => [GREEN, BLACK],
|
|
62
|
+
:search_string => [WHITE, BLUE],
|
|
63
|
+
:mistype => [RED, BLACK],
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@@color_pair = {}
|
|
67
|
+
@@use_color = false
|
|
68
|
+
|
|
69
|
+
def init
|
|
70
|
+
@@use_color = Curses.has_colors?
|
|
71
|
+
return if @@use_color == false
|
|
72
|
+
|
|
73
|
+
Curses::start_color # background becomes black
|
|
74
|
+
|
|
75
|
+
n = 1
|
|
76
|
+
defined_pair = {}
|
|
77
|
+
|
|
78
|
+
COLOR_STYLES.each do |name, pair|
|
|
79
|
+
if defined_pair.include?(pair)
|
|
80
|
+
@@color_pair[name] = Curses::color_pair(defined_pair[pair])
|
|
81
|
+
else
|
|
82
|
+
Curses::init_pair(n, pair[0], pair[1])
|
|
83
|
+
@@color_pair[name] = Curses::color_pair(n)
|
|
84
|
+
defined_pair[pair] = n
|
|
85
|
+
n += 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
module_function :init
|
|
90
|
+
|
|
91
|
+
# Return the result of Curses::color_pair, or nil if the specified
|
|
92
|
+
# name is not defined or the terminal doesn't support color
|
|
93
|
+
def pair(name)
|
|
94
|
+
@@use_color ? @@color_pair[name] : nil
|
|
95
|
+
end
|
|
96
|
+
module_function :pair
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#################################################################
|
|
100
|
+
# Curses::Window
|
|
101
|
+
#################################################################
|
|
102
|
+
class Curses::Window
|
|
103
|
+
alias :width :maxx
|
|
104
|
+
alias :height :maxy
|
|
105
|
+
|
|
106
|
+
alias :_orig_addstr :addstr
|
|
107
|
+
def addstr(str, color_name = nil, align = nil, ref = false)
|
|
108
|
+
# color style on
|
|
109
|
+
if color_name && Curses::Color.pair(color_name)
|
|
110
|
+
attron(Curses::Color.pair(color_name))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# set alignment
|
|
114
|
+
case(align)
|
|
115
|
+
when :left
|
|
116
|
+
setpos(cury, 0)
|
|
117
|
+
when :right
|
|
118
|
+
setpos(cury, width - str.size)
|
|
119
|
+
when :center
|
|
120
|
+
setpos(cury, (width - str.size) / 2)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# draw
|
|
124
|
+
_orig_addstr(str)
|
|
125
|
+
|
|
126
|
+
# color style off
|
|
127
|
+
if color_name && Curses::Color.pair(color_name)
|
|
128
|
+
attroff(Curses::Color.pair(color_name))
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# refresh is required
|
|
132
|
+
refresh if ref
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def next_line
|
|
136
|
+
setpos(cury + 1, 0)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def set_title(title, align = :center)
|
|
140
|
+
return if width <= 6 # too small to show title
|
|
141
|
+
|
|
142
|
+
orig_cur = {:x => curx, :y => cury}
|
|
143
|
+
|
|
144
|
+
# shrink title
|
|
145
|
+
title = title[0...(width-6)] if (title.size + 6) > width
|
|
146
|
+
|
|
147
|
+
case(align)
|
|
148
|
+
when :left
|
|
149
|
+
setpos(0, 2)
|
|
150
|
+
when :right
|
|
151
|
+
setpos(0, width - title.size - 4)
|
|
152
|
+
else # :center
|
|
153
|
+
setpos(0, (width - title.size) / 2 - 1)
|
|
154
|
+
end
|
|
155
|
+
addstr(" #{title} ")
|
|
156
|
+
|
|
157
|
+
setpos(orig_cur[:y], orig_cur[:x])
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
|
data/lib/display.rb
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/htmlentities'
|
|
2
|
+
require 'hpricot'
|
|
3
|
+
require 'highline/import'
|
|
4
|
+
require 'active_support/core_ext/string/unicode'
|
|
5
|
+
# This class formats stuff for human consumption
|
|
6
|
+
|
|
7
|
+
class Display
|
|
8
|
+
include CharacterCleaner
|
|
9
|
+
|
|
10
|
+
attr_reader :width
|
|
11
|
+
# In the future, may allow configuration options here, so that you can
|
|
12
|
+
# customize the Display object.
|
|
13
|
+
def initialize(options={})
|
|
14
|
+
# could opt for :no_links
|
|
15
|
+
@options = options
|
|
16
|
+
@width = options[:width] || 80
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list_feeds(feeds)
|
|
20
|
+
if @options[:curses]
|
|
21
|
+
CursesController.new(self).show_feeds(feeds)
|
|
22
|
+
else
|
|
23
|
+
feeds.each {|f| display_feed(f)}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def display_entries(entries)
|
|
29
|
+
if @options[:curses]
|
|
30
|
+
CursesController.new(self).show_entries(entries)
|
|
31
|
+
else
|
|
32
|
+
entries.each do |e|
|
|
33
|
+
puts '-' * @width
|
|
34
|
+
puts display_entry(e)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def display_entry(entry, show_title_and_feed = true)
|
|
40
|
+
out = []
|
|
41
|
+
if show_title_and_feed
|
|
42
|
+
out << "#{entry.feed.title} | #{entry.last_updated.strftime('%A, %B %d %Y')}"
|
|
43
|
+
out << wrap_text(html_to_text(entry.title.strip))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# description is the summary, so mark it as such
|
|
47
|
+
if entry.description && entry.content && (entry.description.strip.length < entry.content.strip.length)
|
|
48
|
+
out << "Entry Summary:\n\n" + html_to_text(entry.description).strip
|
|
49
|
+
out << divider.strip
|
|
50
|
+
out << "Entry Content:"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# If there is no content, just print the description
|
|
54
|
+
if entry.content.nil? || entry.content.strip == ''
|
|
55
|
+
out << html_to_text(entry.description || '').strip
|
|
56
|
+
else
|
|
57
|
+
out << html_to_text(entry.content || '').strip
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless @options[:simple]
|
|
61
|
+
out << entry.url
|
|
62
|
+
end
|
|
63
|
+
unless entry.categories.empty?
|
|
64
|
+
out << "Categories: #{entry.categories.join(", ")}"
|
|
65
|
+
end
|
|
66
|
+
out.join("\n\n")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def display_title_and_feed(entry)
|
|
70
|
+
out = []
|
|
71
|
+
out << "#{entry.feed.title} | #{entry.last_updated.strftime('%A, %B %d %Y')}"
|
|
72
|
+
out << wrap_text(html_to_text(entry.title.strip))
|
|
73
|
+
out.join("\n\n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def display_title(entry)
|
|
77
|
+
wrap_text(html_to_text(entry.title.strip))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def display_feed(entry)
|
|
81
|
+
"#{entry.feed.title} | #{entry.date_published.strftime('%A, %B %d %Y')}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def display_raw_entry_content(entry)
|
|
85
|
+
out = []
|
|
86
|
+
if entry.description
|
|
87
|
+
out << divider + "Entry Summary\n\n" + entry.description
|
|
88
|
+
end
|
|
89
|
+
if entry.content
|
|
90
|
+
out << "Entry Content\n\n" + entry.content
|
|
91
|
+
end
|
|
92
|
+
wrap_text(out.join(divider)).strip
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def html_to_text(html)
|
|
96
|
+
LOGGER.debug("html_to_text:\n #{html}")
|
|
97
|
+
html.strip!
|
|
98
|
+
# convert utf-8 to ascii
|
|
99
|
+
|
|
100
|
+
html = process_entities_and_utf(html)
|
|
101
|
+
|
|
102
|
+
# if there are no tags, and this is a body, wrap what looks like paragraph
|
|
103
|
+
# in paragarph tags, so it can be processed with the html paragraph rule
|
|
104
|
+
# below.
|
|
105
|
+
if html =~ /\n/ && html !~ /<[^>]+>/ && html !~ /<\/[^>]+>/
|
|
106
|
+
html = html.split("\n\n").collect {|x| "<p>#{x}</p>"}.join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
html, *links = links_to_footnotes(html)
|
|
110
|
+
html = tags_to_text(html)
|
|
111
|
+
html = normalize_blank_lines(html)
|
|
112
|
+
html = wrap_text(html)
|
|
113
|
+
html = [html, links.join("\n")].join("\n\n")
|
|
114
|
+
#puts out
|
|
115
|
+
#out = normalize_blank_lines(out)
|
|
116
|
+
html.strip
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Make sure there is no more than one blank line anywhere
|
|
120
|
+
def normalize_blank_lines(text)
|
|
121
|
+
# get rid of ms line feeds
|
|
122
|
+
text.gsub(/\r\n/, "\n").
|
|
123
|
+
# compress 3 or more blank lines to one
|
|
124
|
+
gsub(/^\s*$/, "\n").
|
|
125
|
+
split(/\n\n\n*/).join( "\n\n")
|
|
126
|
+
# hflush everything left to begin with
|
|
127
|
+
#gsub(/^\s+(\w)/, '\1')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def tags_to_text(html)
|
|
131
|
+
doc = Hpricot(html)
|
|
132
|
+
|
|
133
|
+
doc.search('//comment()').remove
|
|
134
|
+
|
|
135
|
+
doc.search('p, div') do |p|
|
|
136
|
+
p.swap( "\n\n" + p.inner_text.gsub("\n", ' ').squeeze(' ').strip + "\n\n" )
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
doc.search('//blockquote') do |x|
|
|
140
|
+
# compress extra spaces
|
|
141
|
+
text = x.inner_text.squeeze(' ').strip
|
|
142
|
+
# collapse the spacing in the text
|
|
143
|
+
text.gsub!(/\s{2,}/, ' ')
|
|
144
|
+
text = wrap_text(text, @width - 4).gsub(/^/, ' ') # indent 4 spaces
|
|
145
|
+
x.swap("\n\n" + text + "\n\n")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
doc.search('h1,h2,h3,h4') do |p|
|
|
149
|
+
p.swap( "\n\n= #{p.inner_text}\n\n" )
|
|
150
|
+
end
|
|
151
|
+
doc.search('//img') do |img|
|
|
152
|
+
img.swap( "(img)" )
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
doc.search('object').remove
|
|
156
|
+
doc.search('table').remove
|
|
157
|
+
doc.search('script').remove
|
|
158
|
+
|
|
159
|
+
doc.search('//br') do |p|
|
|
160
|
+
p.swap( "\n" )
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
doc.search('i, b') do |p|
|
|
164
|
+
p.swap( "*#{p.inner_text}*" )
|
|
165
|
+
end
|
|
166
|
+
# anchor tags are processed after real links
|
|
167
|
+
doc.search('a') do |p|
|
|
168
|
+
p.swap( "#{p.inner_text}" )
|
|
169
|
+
end
|
|
170
|
+
doc.search('dt') do |x|
|
|
171
|
+
x.swap( "#{x.inner_html}:\n" )
|
|
172
|
+
end
|
|
173
|
+
# This could be improved to insure an indentation even if there are nested
|
|
174
|
+
# tag elements
|
|
175
|
+
doc.search('dd') do |x|
|
|
176
|
+
x.swap( "#{x.inner_text}\n" )
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
doc.search('//span') do |s|
|
|
181
|
+
s.swap( s.inner_text )
|
|
182
|
+
end
|
|
183
|
+
doc.search('hr') do |s|
|
|
184
|
+
s.swap( '-' * @width)
|
|
185
|
+
end
|
|
186
|
+
# Do this before erasing the enclosing <ul> or <ol> tags
|
|
187
|
+
doc.search('li') do |s|
|
|
188
|
+
text = s.inner_text.strip
|
|
189
|
+
# wrap the text and indent it 2 spaces
|
|
190
|
+
text = wrap_text(text, @width - 2).gsub(/^/, ' ')
|
|
191
|
+
# don't indent 1st line
|
|
192
|
+
text.lstrip!
|
|
193
|
+
s.swap( "* " + text + "\n" )
|
|
194
|
+
end
|
|
195
|
+
doc.search('ul, ol, dl') do |s|
|
|
196
|
+
s.swap( s.inner_text.strip )
|
|
197
|
+
end
|
|
198
|
+
doc.to_s
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def links_to_footnotes(html)
|
|
202
|
+
doc = Hpricot(html)
|
|
203
|
+
footnotes = []
|
|
204
|
+
doc.search('//a[@href]') do |link|
|
|
205
|
+
if @options[:no_links]
|
|
206
|
+
link.swap(
|
|
207
|
+
if link.inner_text == ''
|
|
208
|
+
''
|
|
209
|
+
else
|
|
210
|
+
"[#{link.inner_text}]"
|
|
211
|
+
end
|
|
212
|
+
)
|
|
213
|
+
else
|
|
214
|
+
href = link.attributes['href']
|
|
215
|
+
footnotes << " [#{footnotes.size + 1}] #{href}"
|
|
216
|
+
link.swap(link.inner_text + "[#{footnotes.size}]")
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
[doc.to_s, *footnotes]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# From
|
|
223
|
+
# http://blog.macromates.com/2006/wrapping-text-with-regular-expressions/
|
|
224
|
+
def wrap_text(txt, col = @width)
|
|
225
|
+
txt.chars.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/, "\\1\\3\n")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def divider
|
|
230
|
+
"\n" + '-' * @width + "\n"
|
|
231
|
+
end
|
|
232
|
+
end
|