epubforge 0.0.5
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/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
|
+
|