epubforge 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +26 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +26 -0
- data/Rakefile +71 -0
- data/VERSION +1 -0
- data/bin/epubforge +10 -0
- data/config/actions/book_to_epub.rb +20 -0
- data/config/actions/generate.rb +24 -0
- data/config/actions/generate_chapter.rb +26 -0
- data/config/actions/git_backup.rb +23 -0
- data/config/actions/gitify.rb +72 -0
- data/config/actions/globals.rb +77 -0
- data/config/actions/help.rb +21 -0
- data/config/actions/init.rb +137 -0
- data/config/actions/kindle.rb +68 -0
- data/config/actions/notes_to_epub.rb +20 -0
- data/config/actions/notes_to_kindle.rb +17 -0
- data/config/actions/word_count.rb +126 -0
- data/config/actions/wrap_scene_notes_in_hidden_div.rb +118 -0
- data/config/htmlizers.rb +62 -0
- data/lib/action/actions_lookup.rb +41 -0
- data/lib/action/cli_command.rb +72 -0
- data/lib/action/cli_sequence.rb +55 -0
- data/lib/action/file_transformer.rb +59 -0
- data/lib/action/run_description.rb +24 -0
- data/lib/action/runner.rb +122 -0
- data/lib/action/thor_action.rb +149 -0
- data/lib/core_extensions/array.rb +5 -0
- data/lib/core_extensions/kernel.rb +42 -0
- data/lib/core_extensions/nil_class.rb +5 -0
- data/lib/core_extensions/object.rb +5 -0
- data/lib/core_extensions/string.rb +37 -0
- data/lib/custom_helpers.rb +60 -0
- data/lib/epub/assets/asset.rb +11 -0
- data/lib/epub/assets/html.rb +8 -0
- data/lib/epub/assets/image.rb +18 -0
- data/lib/epub/assets/markdown.rb +8 -0
- data/lib/epub/assets/page.rb +32 -0
- data/lib/epub/assets/stylesheet.rb +22 -0
- data/lib/epub/assets/textile.rb +8 -0
- data/lib/epub/builder.rb +270 -0
- data/lib/epub/packager.rb +16 -0
- data/lib/epubforge.rb +97 -0
- data/lib/errors.rb +8 -0
- data/lib/project/project.rb +65 -0
- data/lib/utils/action_loader.rb +7 -0
- data/lib/utils/class_loader.rb +83 -0
- data/lib/utils/directory_builder.rb +181 -0
- data/lib/utils/downloader.rb +58 -0
- data/lib/utils/file_orderer.rb +45 -0
- data/lib/utils/file_path.rb +152 -0
- data/lib/utils/html_translator.rb +99 -0
- data/lib/utils/html_translator_queue.rb +70 -0
- data/lib/utils/htmlizer.rb +92 -0
- data/lib/utils/misc.rb +20 -0
- data/lib/utils/root_path.rb +20 -0
- data/lib/utils/settings.rb +146 -0
- data/lib/utils/template_evaluator.rb +20 -0
- data/templates/default/book/afterword.markdown.template +4 -0
- data/templates/default/book/chapter-%i%.markdown.sequence +4 -0
- data/templates/default/book/foreword.markdown.template +6 -0
- data/templates/default/book/images/cover.png +0 -0
- data/templates/default/book/stylesheets/stylesheet.css.template +2 -0
- data/templates/default/book/title_page.markdown.template +4 -0
- data/templates/default/notes/character.named.markdown.template +4 -0
- data/templates/default/notes/stylesheets/stylesheet.css.template +2 -0
- data/templates/default/payload.rb +65 -0
- data/templates/default/settings/actions/local_action.rb.example +14 -0
- data/templates/default/settings/config.rb.form +55 -0
- data/templates/default/settings/htmlizers.rb +0 -0
- data/templates/default/settings/wordcount.template +6 -0
- data/test/helper.rb +22 -0
- data/test/misc/config.rb +7 -0
- data/test/sample_text/sample.markdown +30 -0
- data/test/sample_text/sample.textile +24 -0
- data/test/test_custom_helpers.rb +22 -0
- data/test/test_directory_builder.rb +141 -0
- data/test/test_epf_root.rb +9 -0
- data/test/test_epubforge.rb +164 -0
- data/test/test_htmlizers.rb +24 -0
- data/test/test_runner.rb +15 -0
- data/test/test_utils.rb +39 -0
- metadata +328 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
class DirectoryBuilder
|
4
|
+
attr_accessor :current_path, :current_file
|
5
|
+
|
6
|
+
def initialize( path )
|
7
|
+
@paths = []
|
8
|
+
@current_path = path.fwf_filepath
|
9
|
+
make_path
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.create( path, &block )
|
13
|
+
builder = self.new( path )
|
14
|
+
yield builder if block_given?
|
15
|
+
builder
|
16
|
+
end
|
17
|
+
|
18
|
+
def dir( *args, &block )
|
19
|
+
descend( *args ) do
|
20
|
+
yield if block_given?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# block must be given
|
25
|
+
def self.tmpdir( &block )
|
26
|
+
if block_given?
|
27
|
+
Dir.mktmpdir do |dir|
|
28
|
+
self.create( dir ) do |builder|
|
29
|
+
yield builder
|
30
|
+
end
|
31
|
+
end
|
32
|
+
else
|
33
|
+
self.create( Dir.mktmpdir )
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Copies the given source file into a file in the current_path.
|
38
|
+
# If a dest_name is given, the new file will be given that name.
|
39
|
+
def copy( src_filepath, dst_name = nil )
|
40
|
+
dst_filepath = dst_name ? @current_path.join( dst_name ) : @current_path
|
41
|
+
FileUtils.copy( src_filepath, dst_filepath )
|
42
|
+
end
|
43
|
+
|
44
|
+
def file( name = nil, content = nil, &block )
|
45
|
+
# if name && content
|
46
|
+
# begin
|
47
|
+
# f = open_file( name )
|
48
|
+
# f << content
|
49
|
+
# ensure
|
50
|
+
# close_file
|
51
|
+
# end
|
52
|
+
if name
|
53
|
+
open_file( name )
|
54
|
+
@current_file << content if content
|
55
|
+
if block_given?
|
56
|
+
begin
|
57
|
+
yield @current_file
|
58
|
+
ensure
|
59
|
+
close_file
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
@current_file
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def current_file
|
68
|
+
@current_file ? FunWith::Files::FilePath.new( @current_file.path ) : nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# if file not given, the result is appended to the current file.
|
72
|
+
def download( url, file = nil )
|
73
|
+
if file
|
74
|
+
if file.fwf_filepath.relative?
|
75
|
+
file = FunWith::Files::FilePath.new( @current_path, file )
|
76
|
+
end
|
77
|
+
|
78
|
+
File.open( file, "w" ) do |f|
|
79
|
+
download_to_target( url, f )
|
80
|
+
end
|
81
|
+
elsif @current_file
|
82
|
+
download_to_target( url, @current_file )
|
83
|
+
else
|
84
|
+
puts "No current file to append #{url} to."
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def template( src, dst, vars = {} )
|
89
|
+
self.file( dst ) do |f|
|
90
|
+
f << Utils::TemplateEvaluator.new( src, vars ).result
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
def make_path
|
96
|
+
FileUtils.mkdir_p( @current_path ) unless @current_path.exist?
|
97
|
+
end
|
98
|
+
|
99
|
+
def descend( *args, &block )
|
100
|
+
if @current_path.directory?
|
101
|
+
close_file
|
102
|
+
@paths << @current_path
|
103
|
+
@current_path = @paths.last.join( *args )
|
104
|
+
make_path
|
105
|
+
yield
|
106
|
+
@current_path = @paths.pop
|
107
|
+
close_file
|
108
|
+
else
|
109
|
+
raise "Cannot descend."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def open_file( name )
|
114
|
+
close_file
|
115
|
+
@current_file = File.open( @current_path.join( name ), "w" )
|
116
|
+
end
|
117
|
+
|
118
|
+
def close_file
|
119
|
+
if @current_file
|
120
|
+
@current_file.flush
|
121
|
+
@current_file.close
|
122
|
+
end
|
123
|
+
|
124
|
+
@current_file = nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def download_to_target( url, file )
|
128
|
+
Downloader.new.download( url, file )
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
# sample code
|
136
|
+
#
|
137
|
+
# DirBuilder.create( '~/project' ) do |b| # starts by creating directory. If parent
|
138
|
+
# # directories don't exist, they will soon.
|
139
|
+
# # if you use DirBuilder.tmp('~/project'), a tempdir
|
140
|
+
# # is created, and its contents relocated to ~/project when the
|
141
|
+
# # block terminates.
|
142
|
+
# b.dir("images") do # creates subdirectory "images"
|
143
|
+
# for img in src_dir.entries.select{|img| img.extension == ".png"}
|
144
|
+
# b.copy( src_dir.join( img.filename ) ) # copies a bunch of files from another directory
|
145
|
+
# end # rises back to the initial '~/project directory
|
146
|
+
#
|
147
|
+
# b.copy( src_dir.join( "rorshach.xml" ) )
|
148
|
+
# b.download( "dest.bash", "http://get.rvm.io" ) # downloads file directly beneath '~/project'
|
149
|
+
# # maybe someday, though
|
150
|
+
#
|
151
|
+
# b.dir("text", "scenes") do # creates ~/project/text/scenes subdir
|
152
|
+
# b.file( "adventure_time.txt" ) do |f|
|
153
|
+
# f << "Fill this in later"
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
# # calling .file without feeding it a block leaves it open for writing,
|
157
|
+
# # until either the enclosing block terminates or .file is called
|
158
|
+
# # again with a string argument.
|
159
|
+
# b.file( "another_brick.txt" )
|
160
|
+
# b.file << "Hey, you!"
|
161
|
+
# b.file << "Yes, you!"
|
162
|
+
# b.file.push "Stand still, laddie!"
|
163
|
+
#
|
164
|
+
# b.template(templates_dir.join("blue_template.txt")) do |t|
|
165
|
+
# t.var(:fname, "John")
|
166
|
+
# t.var(:lname, "Macey")
|
167
|
+
# t.var(:state, "Ohio")
|
168
|
+
# t.vars(graduated: "2003")
|
169
|
+
# t.vars(quot: "That wasn't my duck.", photo: "john.png", css: "font-family: arial")
|
170
|
+
# end
|
171
|
+
#
|
172
|
+
# b.copy( [src_dir.join("abba.txt"), "baab.txt"] ) # contents of abba.txt copied into baab.txt
|
173
|
+
#
|
174
|
+
#
|
175
|
+
# b.file( ".lockfile" ) # creates an empty file
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# b.
|
179
|
+
# end
|
180
|
+
#
|
181
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
class Downloader
|
4
|
+
# stolen from:
|
5
|
+
# http://stackoverflow.com/questions/2263540/how-do-i-download-a-binary-file-over-http-using-ruby
|
6
|
+
def download( url, io )
|
7
|
+
@uri = URI.parse( url )
|
8
|
+
@io = io
|
9
|
+
|
10
|
+
open( url ) do |f|
|
11
|
+
@io << f.read
|
12
|
+
end
|
13
|
+
|
14
|
+
# @io << Net::HTTP.get( @uri )
|
15
|
+
|
16
|
+
# Net::HTTP.start( @uri.host, @uri.port ) do |http|
|
17
|
+
# http.request_get( @uri.path ) do |request|
|
18
|
+
# request.read_body do |seg|
|
19
|
+
# puts "============================== #{seg} ============================="
|
20
|
+
# io << seg
|
21
|
+
# #hack -- adjust to suit:
|
22
|
+
# sleep 0.005
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
rescue Exception => e
|
27
|
+
handle_network_errors( e )
|
28
|
+
end
|
29
|
+
|
30
|
+
def handle_network_errors( e )
|
31
|
+
raise e
|
32
|
+
rescue URI::InvalidURIError => e
|
33
|
+
puts "Tried to get #{@uri.path} but failed with URI::InvalidURIError."
|
34
|
+
rescue OpenURI::HTTPError => e
|
35
|
+
STDERR.write( "Couldn't fetch podcast info from #{@uri.path}\n" )
|
36
|
+
STDERR.write( "#{e}\n\n" )
|
37
|
+
rescue SocketError => e
|
38
|
+
STDERR.write( "Problem connecting to server (Socket error) when downloading #{@uri.path}." )
|
39
|
+
STDERR.write( "#{e}\n\n" )
|
40
|
+
rescue URI::InvalidURIError => e
|
41
|
+
STDERR.write( "URI::InvalidURIError for #{@uri.path}." )
|
42
|
+
STDERR.write( "#{e}\n\n" )
|
43
|
+
# this may be too broad a filter
|
44
|
+
# TODO: retry?
|
45
|
+
rescue SystemCallError => e
|
46
|
+
STDERR.write( "Problem connecting to server (System call error) when downloading #{@uri.path}" )
|
47
|
+
STDERR.write( "#{e}\n\n" )
|
48
|
+
rescue OpenSSL::SSL::SSLError => e
|
49
|
+
STDERR.write( "OpenSSL::SSL::SSLError while downloading #{@uri.path}" )
|
50
|
+
STDERR.write( "#{e}\n\n" )
|
51
|
+
# rescue Timeout::Error
|
52
|
+
# STDERR.write( "Timeout error connecting to #{@uri.path}" )
|
53
|
+
# STDERR.write( "#{e}\n\n" )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# FileOrderer holds a set of strings/regexes, then reorders a set of files (FunWith::Files::FilePaths, actually)
|
5
|
+
# by matching the filenames against one regex after another. Allows you to say,
|
6
|
+
# "I want the title page, the foreward, then all the chapters, then all the appendixes, then the afterword."
|
7
|
+
# Ex: FileOrderer( ["title_page", "forward", "chapter-.*", "afterword", "appendix.*" ).reorder( pages )
|
8
|
+
# Only compares the basename minus extension. Files should come from the same directory
|
9
|
+
class FileOrderer
|
10
|
+
def initialize( matchers )
|
11
|
+
@matchers = matchers.map do |m|
|
12
|
+
case m
|
13
|
+
when Regexp
|
14
|
+
m
|
15
|
+
when String
|
16
|
+
/^#{m}$/
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@matchers.push( /^.*$/ )
|
21
|
+
end
|
22
|
+
|
23
|
+
def reorder( files )
|
24
|
+
files = files.map(&:fwf_filepath)
|
25
|
+
|
26
|
+
files.sort_by!{ |f|
|
27
|
+
f.basename_no_ext.to_s
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
ordered_files = @matchers.inject( [] ) do |collector, matcher|
|
32
|
+
matched_files = files.select do |f|
|
33
|
+
name = f.basename_no_ext.to_s
|
34
|
+
matcher.match( name )
|
35
|
+
end
|
36
|
+
|
37
|
+
collector += matched_files
|
38
|
+
files -= matched_files
|
39
|
+
|
40
|
+
collector
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
class FilePath < Pathname
|
4
|
+
def initialize( *args )
|
5
|
+
super( File.join( *args ) )
|
6
|
+
end
|
7
|
+
|
8
|
+
# args implicitly joined to cwd
|
9
|
+
def self.cwd( *args )
|
10
|
+
Dir.pwd.fwf_filepath.join( *args )
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.pwd( *args )
|
14
|
+
self.cwd( *args )
|
15
|
+
end
|
16
|
+
|
17
|
+
def join( *args, &block )
|
18
|
+
if block_given?
|
19
|
+
yield self.class.new( super(*args) )
|
20
|
+
else
|
21
|
+
self.class.new( super(*args) )
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
alias :exists? :exist?
|
26
|
+
|
27
|
+
def up
|
28
|
+
self.class.new( self.join("..") ).expand
|
29
|
+
end
|
30
|
+
|
31
|
+
# opts:
|
32
|
+
# :class => [self.class] The class of objects you want returned (String, FilePath, ClassLoader, etc.)
|
33
|
+
# Should probably be a subclass of FilePath or String. Class.init must accept a string
|
34
|
+
# [representing a file path] as the sole argument.
|
35
|
+
#
|
36
|
+
# :recurse => [false]
|
37
|
+
# :ext => [] A single symbol, or a list containing strings/symbols representing file name extensions.
|
38
|
+
# No leading periods kthxbai.
|
39
|
+
#
|
40
|
+
# If opts not given, the user can still do it explicitly with arguments like .glob("**", "*.rb")
|
41
|
+
def glob( *args )
|
42
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
43
|
+
|
44
|
+
recurser = opts[:recurse] ? "**" : nil
|
45
|
+
extensions = case opts[:ext]
|
46
|
+
when Symbol, String
|
47
|
+
"*.#{opts[:ext]}"
|
48
|
+
when Array
|
49
|
+
extensions = opts[:ext].map(&:to_s).join(',')
|
50
|
+
"*.{#{extensions}}"
|
51
|
+
when NilClass
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
args += [recurser, extensions]
|
56
|
+
args.compact!
|
57
|
+
|
58
|
+
opts[:class] ||= self.class
|
59
|
+
Dir.glob( self.join(*args) ).map{ |f| opts[:class].new(f) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def expand
|
63
|
+
self.class.new( File.expand_path( self ) )
|
64
|
+
end
|
65
|
+
|
66
|
+
def touch
|
67
|
+
FileUtils.touch( self )
|
68
|
+
return true
|
69
|
+
rescue Errno::EACCESS
|
70
|
+
return false
|
71
|
+
end
|
72
|
+
|
73
|
+
def touch_dir
|
74
|
+
FileUtils.mkdir_p( self )
|
75
|
+
return true
|
76
|
+
rescue Errno::EEXIST
|
77
|
+
return true
|
78
|
+
rescue Errno::EACCESS
|
79
|
+
return false
|
80
|
+
end
|
81
|
+
|
82
|
+
def write( content = nil, &block )
|
83
|
+
File.open( self, "w" ) do |f|
|
84
|
+
f << content if content
|
85
|
+
if block_given?
|
86
|
+
yield f
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def append( content = nil, &block )
|
92
|
+
File.open( self, "a" ) do |f|
|
93
|
+
f << content if content
|
94
|
+
if block_given?
|
95
|
+
yield f
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def grep( regex )
|
101
|
+
return [] unless self.file?
|
102
|
+
matching = []
|
103
|
+
self.each_line do |line|
|
104
|
+
matching.push( line ) if line.match( regex )
|
105
|
+
end
|
106
|
+
matching
|
107
|
+
end
|
108
|
+
|
109
|
+
# Not the same as zero?
|
110
|
+
def empty?
|
111
|
+
raise Exceptions::FileDoesNotExist unless self.exist?
|
112
|
+
|
113
|
+
if self.file?
|
114
|
+
File.size( self ) == 0
|
115
|
+
elsif self.directory?
|
116
|
+
self.glob( "**", "*" ).length == 0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def basename_no_ext
|
121
|
+
self.basename.to_s.split(".")[0..-2].join(".").fwf_filepath
|
122
|
+
end
|
123
|
+
|
124
|
+
def without_ext
|
125
|
+
self.gsub(/\.#{self.ext}$/, '')
|
126
|
+
end
|
127
|
+
|
128
|
+
def ext
|
129
|
+
self.basename.to_s.split(".").last || ""
|
130
|
+
end
|
131
|
+
|
132
|
+
def relative_to( ancestor_dir )
|
133
|
+
depth = ancestor_dir.to_s.split(File::SEPARATOR).length
|
134
|
+
relative_path = self.to_s.split(File::SEPARATOR)
|
135
|
+
relative_path[(depth)..-1].join(File::SEPARATOR).fwf_filepath
|
136
|
+
end
|
137
|
+
|
138
|
+
def gsub( *args )
|
139
|
+
self.to_s.gsub(*args).fwf_filepath
|
140
|
+
end
|
141
|
+
|
142
|
+
def gsub!( *args )
|
143
|
+
new_str = self.to_s.gsub(*args)
|
144
|
+
self.instance_variable_set(:@path, new_str)
|
145
|
+
end
|
146
|
+
|
147
|
+
def fwf_filepath
|
148
|
+
self
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
# An individual translator, which receives a filename, determines if it's up to the job
|
4
|
+
# then returns the resulting HTML translation.
|
5
|
+
class HtmlTranslator
|
6
|
+
GROUP_NAMES = [:preferred, :user, :default, :fallback]
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
group( :user )
|
10
|
+
opts( "" )
|
11
|
+
end
|
12
|
+
|
13
|
+
def name( n = nil )
|
14
|
+
@name = n if n
|
15
|
+
@name
|
16
|
+
end
|
17
|
+
|
18
|
+
def group( g = nil )
|
19
|
+
if g
|
20
|
+
raise "group must be one of the following symbols: #{GROUP_NAMES.inspect}" unless GROUP_NAMES.include?(g)
|
21
|
+
@group = g
|
22
|
+
end
|
23
|
+
|
24
|
+
@group
|
25
|
+
end
|
26
|
+
|
27
|
+
def executable executable_name = nil
|
28
|
+
if executable_name
|
29
|
+
@executable_name = Htmlizer.instance.location( executable_name ) || `which #{executable_name}`.strip
|
30
|
+
end
|
31
|
+
@executable_name || ""
|
32
|
+
end
|
33
|
+
|
34
|
+
def format f = nil
|
35
|
+
@format = f if f
|
36
|
+
@format
|
37
|
+
end
|
38
|
+
|
39
|
+
def cmd c = nil
|
40
|
+
@cmd = c if c
|
41
|
+
@cmd
|
42
|
+
end
|
43
|
+
|
44
|
+
def custom_proc( p = nil, &block )
|
45
|
+
if block_given?
|
46
|
+
@custom_proc = block
|
47
|
+
else
|
48
|
+
@custom_proc = c if c
|
49
|
+
end
|
50
|
+
|
51
|
+
@custom_proc
|
52
|
+
end
|
53
|
+
|
54
|
+
def opts o = nil
|
55
|
+
@opts = o if o
|
56
|
+
@opts
|
57
|
+
end
|
58
|
+
|
59
|
+
def installed?
|
60
|
+
executable.length > 0
|
61
|
+
end
|
62
|
+
|
63
|
+
def handles_format?( f )
|
64
|
+
@format == determine_file_format( f ) || @format == :unknown
|
65
|
+
end
|
66
|
+
|
67
|
+
def can_do_job?( f )
|
68
|
+
installed? && handles_format?( f )
|
69
|
+
end
|
70
|
+
|
71
|
+
# opts allows you to override the normal command line arguments
|
72
|
+
# Maybe a description of the job's requirements should be more
|
73
|
+
# elaborate than just a filename. OTOH, simple can have its advantages.
|
74
|
+
def translate( filename, opts = "" )
|
75
|
+
return false unless can_do_job?( filename )
|
76
|
+
|
77
|
+
result = "<!-- generated from #{@format} by htmlizer #{@name} -->\n\n"
|
78
|
+
if @custom_proc
|
79
|
+
result += @custom_proc.call( filename, *opts )
|
80
|
+
elsif @cmd
|
81
|
+
exec_string = cmd.gsub( /\{\{f\}\}/, filename.to_s )
|
82
|
+
opts = @opts if opts.epf_blank?
|
83
|
+
exec_string.gsub!( /\{\{o\}\}/, opts )
|
84
|
+
exec_string.gsub!( /\{\{x\}\}/, executable )
|
85
|
+
|
86
|
+
result += `#{exec_string}`
|
87
|
+
else
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
|
91
|
+
result
|
92
|
+
end
|
93
|
+
|
94
|
+
def determine_file_format( file )
|
95
|
+
file.fwf_filepath.ext.to_sym
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
# A priority stack (like a priority queue, but FILO) with a simple job:
|
4
|
+
# keep track of the translators (by name and by group), and return them
|
5
|
+
# to the object user in the order they should be tried.
|
6
|
+
class HtmlTranslatorQueue
|
7
|
+
GROUP_NAMES = HtmlTranslator::GROUP_NAMES
|
8
|
+
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@translators = {}
|
12
|
+
@all_translators = []
|
13
|
+
@translators_named = {}
|
14
|
+
for name in GROUP_NAMES
|
15
|
+
@translators[name] = []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# def translators_handling_format( requested_format )
|
20
|
+
# htmlizers = GROUP_NAMES.map{ |group|
|
21
|
+
# (@translator_queue.keys - [:all, :named]).map do |format|
|
22
|
+
# htmlizers = @translator_queue[format][group]
|
23
|
+
# htmlizers ? htmlizers.select{|html| html.handles_format?(requested_format) } : []
|
24
|
+
# end
|
25
|
+
# }
|
26
|
+
#
|
27
|
+
# htmlizers.flatten
|
28
|
+
# end
|
29
|
+
|
30
|
+
# last installed, first yielded (within a given group)
|
31
|
+
#
|
32
|
+
# Returns them in priority order, user-defined ones first.
|
33
|
+
# At the moment, it is up to individual translators to accept or
|
34
|
+
# reject the translation job based on the file format (by extension, which is lame).
|
35
|
+
def each( &block )
|
36
|
+
ordered_translators = []
|
37
|
+
for group in GROUP_NAMES.map{|g| @translators[g].reverse }
|
38
|
+
ordered_translators += group
|
39
|
+
end
|
40
|
+
|
41
|
+
if block_given?
|
42
|
+
for translator in ordered_translators
|
43
|
+
yield translator
|
44
|
+
end
|
45
|
+
else
|
46
|
+
ordered_translators.to_enum
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def length
|
51
|
+
@all_translators.length
|
52
|
+
end
|
53
|
+
|
54
|
+
def categorize( htmlizer )
|
55
|
+
unless GROUP_NAMES.include?( htmlizer.group )
|
56
|
+
puts "No group specified for htmlizer #{htmlizer}. Group must be one of the following symbols: #{GROUP_NAMES.map(&:inspect).inspect}"
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
|
60
|
+
@all_translators << htmlizer
|
61
|
+
@translators_named[htmlizer.name] = htmlizer if htmlizer.name
|
62
|
+
@translators[htmlizer.group] << htmlizer
|
63
|
+
end
|
64
|
+
|
65
|
+
def named( sym )
|
66
|
+
@translators_named[sym]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# Htmlizer coordinates the discovery, selection, and running of HtmlTranslators.
|
5
|
+
# It can be handed basically any supported filetype (markdown, textile, txt), and
|
6
|
+
# hand back an HTML translation of the file.
|
7
|
+
class Htmlizer
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def setup_once
|
11
|
+
return false if @already_set_up
|
12
|
+
@already_set_up = true
|
13
|
+
@exec_location = {}
|
14
|
+
|
15
|
+
@translator_queue = HtmlTranslatorQueue.new
|
16
|
+
|
17
|
+
add_htmlizers( EpubForge.root( 'config', 'htmlizers.rb' ) )
|
18
|
+
add_htmlizers( EpubForge::USER_SETTINGS.join( 'htmlizers.rb' ) )
|
19
|
+
|
20
|
+
@already_set_up
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
def location( name, path = nil )
|
25
|
+
@exec_location[name] = path if path
|
26
|
+
@exec_location[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Commenting out for the moment. Philosophically, maybe it shouldn't provide access to individual translators.
|
30
|
+
# def translators_named( name )
|
31
|
+
# @translator_queue[:named][name]
|
32
|
+
# end
|
33
|
+
|
34
|
+
|
35
|
+
def self.define( &block )
|
36
|
+
htmlizer = HtmlTranslator.new
|
37
|
+
yield htmlizer
|
38
|
+
self.instance.categorize( htmlizer )
|
39
|
+
end
|
40
|
+
|
41
|
+
def categorize( htmlizer )
|
42
|
+
@translator_queue.categorize( htmlizer )
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_htmlizers( htmlizers_file )
|
46
|
+
if htmlizers_file.exist?
|
47
|
+
begin
|
48
|
+
require htmlizers_file.to_s
|
49
|
+
rescue Exception => e
|
50
|
+
puts e.message
|
51
|
+
puts e.backtrace.map{|line| "\t#{line}" }
|
52
|
+
puts "Failed to load htmlizers from project file #{htmlizers_file} Soldiering onward."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
# available options
|
59
|
+
# :htmlizer => the sym for the requested htmlizer.
|
60
|
+
# :opts => a string representing options to execute cmd with
|
61
|
+
def translate( filename, opts = {} )
|
62
|
+
translator = opts[:translator]
|
63
|
+
translator = @translator_queue.named( translator ) if translator.is_a?( Symbol )
|
64
|
+
opts = opts[:opts] || ""
|
65
|
+
|
66
|
+
if translator
|
67
|
+
if result = translator.translate( filename, {opts: opts } )
|
68
|
+
return result
|
69
|
+
else
|
70
|
+
puts "Named Htmlizer #{htmlizer} did not return html. Falling back on other htmlizers"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
for translator in @translator_queue
|
75
|
+
if result = translator.translate( filename, opts )
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
"<!-- COULD NOT FIND HTMLIZER FOR #{filename} -->"
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.format_from_filename( filename )
|
84
|
+
ext = filename.fwf_filepath.extname.gsub(/^\./, "")
|
85
|
+
ext.epf_blank? ? :unknown : ext.to_sym
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
Htmlizer.instance.setup_once
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|