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,17 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class NotesToKindle < Kindle
|
4
|
+
description "Create a .mobi book from the notes and try to push it to your Kindle"
|
5
|
+
keywords :n2k
|
6
|
+
usage "#{$PROGRAM_NAME} n2k <project_directory>"
|
7
|
+
|
8
|
+
def do( project, args )
|
9
|
+
@project = project
|
10
|
+
@src_epub = @project.filename_for_epub_notes.fwf_filepath
|
11
|
+
@dst_mobi = @project.filename_for_mobi_notes.fwf_filepath
|
12
|
+
|
13
|
+
mobify
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'time' # for Time.parse
|
2
|
+
|
3
|
+
module EpubForge
|
4
|
+
module Action
|
5
|
+
class WordCount < ThorAction
|
6
|
+
WORD_COUNT_FILE = "wordcount"
|
7
|
+
|
8
|
+
description "Gives approximate word counts for book chapters and notes."
|
9
|
+
keywords :wc, :count
|
10
|
+
usage "#{$PROGRAM_NAME} count <project_directory>"
|
11
|
+
|
12
|
+
desc( "do:wc", "Countify words.")
|
13
|
+
def do( project, *args )
|
14
|
+
@project = project
|
15
|
+
@report = { "Notes" => wc_one_folder( @project.notes_dir ),
|
16
|
+
"Book" => wc_one_folder( @project.book_dir ) }
|
17
|
+
|
18
|
+
load_word_count_history
|
19
|
+
calculate_todays_word_count
|
20
|
+
append_word_count_history( @report )
|
21
|
+
print_report
|
22
|
+
|
23
|
+
say_all_is_well "Done"
|
24
|
+
@report
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def wc_one_folder( foldername )
|
29
|
+
foldername.glob( ext: EpubForge::Epub::PAGE_FILE_EXTENSIONS ).inject(0) do |count, file|
|
30
|
+
count += wc_one_file( file )
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# I assume the wc executable is more accurate,
|
35
|
+
# and I don't know which is faster.
|
36
|
+
def wc_one_file( filename )
|
37
|
+
if wc_installed?
|
38
|
+
result = `#{wc_installed?.to_s.strip} -w #{filename}`
|
39
|
+
$?.success? ? result.to_i : 0
|
40
|
+
else
|
41
|
+
filename.read.split.length
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_word_count_history
|
46
|
+
@wc_yaml = @project.settings_folder( WORD_COUNT_FILE )
|
47
|
+
@wc_yaml.touch
|
48
|
+
|
49
|
+
if @wc_yaml.empty?
|
50
|
+
# pretend that you wrote everything in the last six hours.
|
51
|
+
append_word_count_history( {"Notes" => 0, "Book" => 0}, beginning_of_day )
|
52
|
+
end
|
53
|
+
|
54
|
+
@history = YAML.load( @wc_yaml.read )
|
55
|
+
end
|
56
|
+
|
57
|
+
def beginning_of_day
|
58
|
+
@beginning_of_day ||= Time.parse( now.strftime("%Y-%m-%d") )
|
59
|
+
@beginning_of_day
|
60
|
+
end
|
61
|
+
|
62
|
+
def now
|
63
|
+
@now ||= Time.now
|
64
|
+
@now
|
65
|
+
end
|
66
|
+
|
67
|
+
def append_word_count_history( report, timestamp = now )
|
68
|
+
unless duplicates_previous_history_item( report )
|
69
|
+
@wc_yaml.append do |f|
|
70
|
+
f.write "- #{timestamp}:\n"
|
71
|
+
f.write " Notes: #{report["Notes"]}\n"
|
72
|
+
f.write " Book: #{report["Book"]}\n\n"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def duplicates_previous_history_item( report )
|
78
|
+
last = @history.last
|
79
|
+
prior_report = last.values.last
|
80
|
+
time_of_history_item( last ) > beginning_of_day &&
|
81
|
+
prior_report["Notes"] == report["Notes"] &&
|
82
|
+
prior_report["Book"] == report["Book"]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Works under the ginormous assumption that the last word count recorded for the previous
|
86
|
+
# day was actually the final count, and every word written since then was written for the
|
87
|
+
# current day. When running for the first time, assumes all prior work was completed the
|
88
|
+
# previous day, and falsifies a history to match that assumption.
|
89
|
+
def calculate_todays_word_count
|
90
|
+
prior_day = @history.reverse.find do |history_item|
|
91
|
+
time_of_history_item( history_item ) <= beginning_of_day
|
92
|
+
end
|
93
|
+
|
94
|
+
# This should never be nil, but...
|
95
|
+
prior_day = prior_day.nil? ? { "Book" => 0, "Notes" => 0 } : prior_day.values.first
|
96
|
+
|
97
|
+
@report["Today"] = @report["Notes"] + @report["Book"] - prior_day["Notes"] - prior_day["Book"]
|
98
|
+
end
|
99
|
+
|
100
|
+
def time_of_history_item( item )
|
101
|
+
t = item.keys.first
|
102
|
+
t = case( t )
|
103
|
+
when Time
|
104
|
+
t
|
105
|
+
when String
|
106
|
+
Time.parse( t )
|
107
|
+
else
|
108
|
+
raise "I have no idea what time it is."
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def wc_installed?
|
113
|
+
executable_installed?( "wc" )
|
114
|
+
end
|
115
|
+
|
116
|
+
def print_report
|
117
|
+
say "", BLUE
|
118
|
+
say "Wordcount", BLUE
|
119
|
+
say "---------", BLUE
|
120
|
+
say "Notes: #{@report["Notes"]}", BLUE
|
121
|
+
say "Book: #{@report["Book"]}", BLUE
|
122
|
+
say "Today: #{@report["Today"]}", BLUE
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class WrapSceneNotesInHiddenDiv < ThorAction
|
4
|
+
description "Assumes scenes are in book/scene-XXXX.markdown, and that the scene description is above the first horizontal row (a.k.a. ***** in Markdown)."
|
5
|
+
keywords :wrap_scene_notes
|
6
|
+
usage "#{$PROGRAM_NAME} wrap_scene_notes<project_directory (optional if current directory)>\n\tfollow with 'undo' to reverse transformation."
|
7
|
+
|
8
|
+
START_SCENE = 0
|
9
|
+
IN_SCENE = 1
|
10
|
+
IN_STORY = 2
|
11
|
+
ABORTING = 3
|
12
|
+
|
13
|
+
START_OF_SCENE_MARKER = "<!-- EPUBFORGE::SCENE_DESCRIPTION -->\n"
|
14
|
+
END_OF_SCENE_MARKER = "<!-- /EPUBFORGE::SCENE_DESCRIPTION -->\n"
|
15
|
+
|
16
|
+
desc( "do:wrap_scene_notes", "Wrap scene notes (obsolete. Do not use.)")
|
17
|
+
def do( project, *args )
|
18
|
+
@project = project
|
19
|
+
|
20
|
+
if args.first == "undo"
|
21
|
+
unwrap_files
|
22
|
+
else
|
23
|
+
wrap_files
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def wrap_files
|
29
|
+
transform_each_scene do |ft|
|
30
|
+
for line in ft.readlines
|
31
|
+
case @mode
|
32
|
+
when START_SCENE
|
33
|
+
if line =~ /epubforge_scene_description/
|
34
|
+
puts "scene description is already wrapped. skipping..."
|
35
|
+
@mode = ABORTING
|
36
|
+
break
|
37
|
+
end
|
38
|
+
|
39
|
+
ft << START_OF_SCENE_MARKER
|
40
|
+
ft << line
|
41
|
+
@mode = IN_SCENE
|
42
|
+
when IN_SCENE
|
43
|
+
if line =~ /^\*{3,}\s*$/ # looking for '******'
|
44
|
+
ft << END_OF_SCENE_MARKER
|
45
|
+
@mode = IN_STORY
|
46
|
+
end
|
47
|
+
ft << line
|
48
|
+
when IN_STORY
|
49
|
+
ft << line
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# what if you never find the end?
|
54
|
+
if @mode == IN_SCENE
|
55
|
+
ft << END_OF_SCENE_MARKER
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def unwrap_files
|
61
|
+
transform_each_scene do |ft|
|
62
|
+
for line in ft.readlines
|
63
|
+
# puts "---------------------------- #{ft.original_filename} - #{ft.finished?} -----------------------------------"
|
64
|
+
# puts "#{@mode} : #{line}"
|
65
|
+
# puts File.size?(ft.transformed_filename)
|
66
|
+
# puts ""
|
67
|
+
|
68
|
+
case @mode
|
69
|
+
when START_SCENE
|
70
|
+
if line =~ /#{START_OF_SCENE_MARKER}/
|
71
|
+
@mode = IN_SCENE
|
72
|
+
else
|
73
|
+
ft << line
|
74
|
+
end
|
75
|
+
when IN_SCENE
|
76
|
+
if line =~ /#{END_OF_SCENE_MARKER}/
|
77
|
+
@mode = IN_STORY
|
78
|
+
else
|
79
|
+
ft << line
|
80
|
+
end
|
81
|
+
when IN_STORY
|
82
|
+
ft << line
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def transform_each_scene &block
|
89
|
+
@transformed_files = [] # save output until the end, then mv them all
|
90
|
+
# when you're sure the process was successful
|
91
|
+
|
92
|
+
each_scene do |scene|
|
93
|
+
ft = FileTransformer.new( scene )
|
94
|
+
@transformed_files << ft
|
95
|
+
@mode = START_SCENE
|
96
|
+
|
97
|
+
yield ft
|
98
|
+
end
|
99
|
+
|
100
|
+
if ask( "Finalize?" ) == "Y"
|
101
|
+
@transformed_files.each do |t|
|
102
|
+
t.finalize
|
103
|
+
end
|
104
|
+
else
|
105
|
+
@transformed_files.each do |t|
|
106
|
+
t.abort
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def each_scene(&block)
|
112
|
+
for scene in Dir["#{@project.book_dir}/scene-????.markdown"].entries
|
113
|
+
yield scene
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/config/htmlizers.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
2
|
+
html.format :markdown
|
3
|
+
html.group :default # the default is :user, so user-defined ones don't have to set it
|
4
|
+
html.executable "multimarkdown"
|
5
|
+
html.cmd "{{x}} {{o}} {{f}}"
|
6
|
+
# html.opts "" # the default
|
7
|
+
end
|
8
|
+
|
9
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
10
|
+
html.format :markdown
|
11
|
+
html.group :default
|
12
|
+
html.executable "pandoc"
|
13
|
+
html.cmd "{{x}} {{o}} {{f}}"
|
14
|
+
html.opts "--from=markdown --to=html"
|
15
|
+
end
|
16
|
+
|
17
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
18
|
+
html.format :textile
|
19
|
+
html.group :default
|
20
|
+
html.executable "pandoc"
|
21
|
+
html.cmd "{{x}} {{o}} {{f}}"
|
22
|
+
html.opts "--from=textile --to=html"
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Emergency backups
|
27
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
28
|
+
html.format :markdown
|
29
|
+
html.group :fallback
|
30
|
+
html.executable "false"
|
31
|
+
html.cmd "echo \"<pre>\" && cat {{f}} && echo \"</pre>\""
|
32
|
+
end
|
33
|
+
|
34
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
35
|
+
html.format :textile
|
36
|
+
html.group :fallback
|
37
|
+
html.executable "false"
|
38
|
+
html.cmd "echo \"<pre>\" && cat {{f}} && echo \"</pre>\""
|
39
|
+
end
|
40
|
+
|
41
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
42
|
+
html.format :txt
|
43
|
+
html.group :fallback
|
44
|
+
html.executable "false"
|
45
|
+
html.cmd "echo \"<pre>\" && cat {{f}} && echo \"</pre>\""
|
46
|
+
end
|
47
|
+
|
48
|
+
# Would be nice to detect and strip out the outer tags
|
49
|
+
# leaving only the content.
|
50
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
51
|
+
html.format :html
|
52
|
+
html.group :fallback
|
53
|
+
html.executable "false"
|
54
|
+
html.cmd "cat {{f}}"
|
55
|
+
end
|
56
|
+
|
57
|
+
EpubForge::Utils::Htmlizer.define do |html|
|
58
|
+
html.format :unknown
|
59
|
+
html.group :fallback
|
60
|
+
html.executable "false"
|
61
|
+
html.cmd "echo \"<pre>\" && cat {{f}} && echo \"</pre>\""
|
62
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class ActionsLookup
|
4
|
+
attr_accessor :actions, :actions_directories, :keywords
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@keywords = {}
|
8
|
+
@actions = []
|
9
|
+
@actions_directories = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_actions( *args )
|
13
|
+
Utils::ActionLoader.require_me( *args )
|
14
|
+
|
15
|
+
new_actions = Utils::ActionLoader.loaded_classes - @actions
|
16
|
+
@actions += new_actions
|
17
|
+
new_directories = Utils::ActionLoader.loaded_directories - @actions_directories
|
18
|
+
@actions_directories += new_directories
|
19
|
+
|
20
|
+
for action in new_actions
|
21
|
+
for keyword in action.keywords
|
22
|
+
@keywords[keyword] = action
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Find all the actions with keywords that start with the given string.
|
28
|
+
# If this results in more than one action being found, the proper
|
29
|
+
# response is to panic and flail arms.
|
30
|
+
def keyword_to_action( keyword )
|
31
|
+
exact_match = @keywords.keys.select{ |k| k == keyword }
|
32
|
+
|
33
|
+
return [@keywords[exact_match.first]] if exact_match.length == 1
|
34
|
+
|
35
|
+
# if no exact match can be found, find a partial match, at the beginning
|
36
|
+
# of the keywords.
|
37
|
+
@keywords.keys.select{ |k| k.match(/^#{keyword}/) }.map{ |k| @keywords[k] }.uniq
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class CliCommand
|
4
|
+
# undo is an action that would be expected to reverse the consequences of this
|
5
|
+
# action. You can do without it if the action can't fail, if it has no real
|
6
|
+
# consequences, or if a prior action, when undone, will wipe out those consequences.
|
7
|
+
# For example, if an earlier command created the directory that the current command
|
8
|
+
# is writing a file to, the earlier command would be expected to delete the directory.
|
9
|
+
def initialize( command, undo = nil, opts = {} )
|
10
|
+
@command = command
|
11
|
+
@undo = undo
|
12
|
+
@opts = opts
|
13
|
+
@remote = opts[:remote] # Is this going to be executed here, or on a different server? Usually in the form "username@host"
|
14
|
+
@verbose = opts[:verbose]
|
15
|
+
@local_dir = opts[:local_dir] # the local directory to cd into before executing the command
|
16
|
+
@remote_dir = opts[:remote_dir] # the remote directory to cd into before executing the command
|
17
|
+
end
|
18
|
+
|
19
|
+
def execute( cmd = :cmd )
|
20
|
+
@remote ? remote_exec( cmd ) : local_exec( cmd )
|
21
|
+
end
|
22
|
+
|
23
|
+
def undo
|
24
|
+
execute( :undo )
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
def local_exec( cmd )
|
29
|
+
cmd = (cmd == :undo ? @undo : @command)
|
30
|
+
return pseudo_success if cmd.epf_blank?
|
31
|
+
|
32
|
+
execute_locally = @local_dir ? "cd #{@local_dir} && " : ""
|
33
|
+
|
34
|
+
@msg = "attempting to run locally: #{cmd}"
|
35
|
+
`#{execute_locally}#{cmd}`
|
36
|
+
print_result
|
37
|
+
$?
|
38
|
+
end
|
39
|
+
|
40
|
+
def remote_exec( cmd )
|
41
|
+
cmd = (cmd == :undo ? @undo : @command)
|
42
|
+
return pseudo_success if cmd.epf_blank?
|
43
|
+
|
44
|
+
execute_remotely = (@remote_dir ? "cd #{@remote_dir} && " : "") + cmd
|
45
|
+
|
46
|
+
@msg = "attempting to run remotely (#{@remote}): #{execute_remotely}"
|
47
|
+
`ssh #{@remote} "#{execute_remotely}"`
|
48
|
+
print_result
|
49
|
+
$?
|
50
|
+
end
|
51
|
+
|
52
|
+
def print_result
|
53
|
+
puts "#{$?.success? ? 'SUCCESS' : 'FAIL'}: #{@msg}" if @verbose
|
54
|
+
end
|
55
|
+
|
56
|
+
def pseudo_success
|
57
|
+
unless @pseudo_success_object
|
58
|
+
@pseudo_success_object = Object.new
|
59
|
+
m = Module.new do
|
60
|
+
def success?
|
61
|
+
true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
@pseudo_success_object.extend( m )
|
66
|
+
end
|
67
|
+
|
68
|
+
@pseudo_success_object
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class CliSequence
|
4
|
+
def initialize
|
5
|
+
@defaults = {}
|
6
|
+
@local_dir
|
7
|
+
@commands = []
|
8
|
+
@completed = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def default( k, v )
|
12
|
+
if k == :remote
|
13
|
+
@remote = v
|
14
|
+
else
|
15
|
+
@defaults[k] = v
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_local_command( command, undo = nil, opts = {} )
|
20
|
+
add_command( command, undo, opts )
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_remote_command( command, undo = nil, opts = {} )
|
24
|
+
opts[:remote] ||= @remote # the default username/host can be overridden by sending a different opts[:remote]
|
25
|
+
add_command( command, undo, opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute
|
29
|
+
@failed = false
|
30
|
+
while (cmd = @commands.shift) && (@failed == false)
|
31
|
+
@failed = true unless cmd.execute.success?
|
32
|
+
@completed.push( cmd )
|
33
|
+
end
|
34
|
+
|
35
|
+
undo unless @failed == false
|
36
|
+
!@failed
|
37
|
+
end
|
38
|
+
|
39
|
+
def undo
|
40
|
+
while cmd = @completed.pop
|
41
|
+
result = cmd.undo
|
42
|
+
@commands.unshift( cmd )
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_command( command, undo = "", opts = {} )
|
47
|
+
for default, setting in @defaults
|
48
|
+
opts[default] ||= setting
|
49
|
+
end
|
50
|
+
|
51
|
+
@commands.push( CliCommand.new(command, undo, opts) )
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class FileTransformer
|
4
|
+
attr_reader :original_filename, :transformed_filename
|
5
|
+
def initialize( file )
|
6
|
+
@original_filename = file
|
7
|
+
@transformed_filename = "#{@original_filename}.epubforge.#{sprintf("%07i", rand(1000000))}.tmp"
|
8
|
+
@out = File.open( @transformed_filename, "w" )
|
9
|
+
@finished = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def finalize
|
13
|
+
return if finished?
|
14
|
+
@finished = true
|
15
|
+
@out.close
|
16
|
+
|
17
|
+
FileUtils.mv( @transformed_filename, @original_filename )
|
18
|
+
end
|
19
|
+
|
20
|
+
def abort
|
21
|
+
return if finished?
|
22
|
+
@finished = true
|
23
|
+
FileUtils.rm( @transformed_filename )
|
24
|
+
end
|
25
|
+
|
26
|
+
def write( input )
|
27
|
+
return if finished?
|
28
|
+
@out << input
|
29
|
+
@out.flush
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<( input )
|
33
|
+
write( input )
|
34
|
+
end
|
35
|
+
|
36
|
+
def read_file
|
37
|
+
File.read( @original_filename )
|
38
|
+
end
|
39
|
+
|
40
|
+
def readlines( &block )
|
41
|
+
File.readlines( @original_filename ) do |line|
|
42
|
+
yield line
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def old_size
|
47
|
+
File.size?( @original_filename )
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_size
|
51
|
+
File.size?( @transformed_filename )
|
52
|
+
end
|
53
|
+
|
54
|
+
def finished?
|
55
|
+
@finished
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module EpubForge
|
2
|
+
module Action
|
3
|
+
class RunDescription
|
4
|
+
attr_accessor :args
|
5
|
+
attr_accessor :project
|
6
|
+
attr_accessor :keyword
|
7
|
+
attr_accessor :klass
|
8
|
+
attr_accessor :errors
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@args = nil
|
12
|
+
@project = nil
|
13
|
+
@keyword = nil
|
14
|
+
@klass = nil
|
15
|
+
@errors = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def runnable?
|
19
|
+
@errors.epf_blank?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Another trivial change
|
2
|
+
|
3
|
+
module EpubForge
|
4
|
+
module Action
|
5
|
+
class Runner
|
6
|
+
attr_accessor :actions_lookup
|
7
|
+
def initialize
|
8
|
+
reset
|
9
|
+
end
|
10
|
+
|
11
|
+
def reset
|
12
|
+
@args = []
|
13
|
+
@run_description = RunDescription.new
|
14
|
+
@actions_lookup = ActionsLookup.new
|
15
|
+
@actions_lookup.add_actions( EpubForge::ACTIONS_DIR )
|
16
|
+
@actions_lookup.add_actions( EpubForge::USER_ACTIONS_DIR ) if EpubForge::USER_ACTIONS_DIR.directory?
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
if @run_description.runnable?
|
21
|
+
@run_description.klass.new.do( @run_description.project, *(@run_description.args) )
|
22
|
+
else
|
23
|
+
puts "Error(s) trying to complete the requested action:"
|
24
|
+
puts @run_description.errors.join("\n")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# order: project_dir(optional), keyword, args
|
29
|
+
# If a project_dir is not given, the current working directory is prepended to the arguments list.
|
30
|
+
# In some cases -- well, really only 'init', this will be in error. Because the argument given does
|
31
|
+
# not exist yet, it will not recognize the first argument as pointing to a project.
|
32
|
+
def exec( *args )
|
33
|
+
# remove project from arguments
|
34
|
+
@args = args
|
35
|
+
# first argument is the action's keyword
|
36
|
+
# print help message if no keywords given
|
37
|
+
parse_args
|
38
|
+
|
39
|
+
# finish setting up run_description
|
40
|
+
@run_description.args = @args
|
41
|
+
|
42
|
+
run
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# The priority for the project directory
|
47
|
+
# 1) explicitly stated directory --project=/home/andersbr/writ/fic/new_project
|
48
|
+
# 2) the current working directory (if it's an existing project)
|
49
|
+
# 3) the final arg
|
50
|
+
#
|
51
|
+
# At this point,
|
52
|
+
protected
|
53
|
+
def parse_args
|
54
|
+
@run_description = RunDescription.new
|
55
|
+
@run_description.keyword = @args.shift || "help"
|
56
|
+
|
57
|
+
existing_project = false
|
58
|
+
project_dir = get_explicit_project_option( @args )
|
59
|
+
|
60
|
+
# see if the last argument is a project directory
|
61
|
+
unless project_dir || @args.length == 0
|
62
|
+
last_arg = @args.pop
|
63
|
+
unless project_dir = ( Project.is_project_dir?( last_arg ) )
|
64
|
+
@args.push( last_arg )
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# see if current working directory is a project directory
|
69
|
+
unless project_dir
|
70
|
+
cwd = FunWith::Files::FilePath.cwd
|
71
|
+
if Project.is_project_dir?( cwd )
|
72
|
+
project_dir = cwd
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# At this point, if we're going to find an existing project directory, we'll have found it by now.
|
77
|
+
# Time to load the actions and determine whether the keyword matches an existing action
|
78
|
+
if project_dir && Project.is_project_dir?( project_dir )
|
79
|
+
existing_project = true
|
80
|
+
@run_description.project = Project.new( project_dir )
|
81
|
+
@actions_lookup.add_actions( @run_description.project.settings_folder( "actions" ) )
|
82
|
+
Utils::Htmlizer.instance.add_htmlizers( @run_description.project.settings_folder( "htmlizers.rb" ) )
|
83
|
+
end
|
84
|
+
|
85
|
+
map_keyword_to_action
|
86
|
+
|
87
|
+
if !existing_project && @run_description.klass.project_required?
|
88
|
+
@run_description.errors << "Could not find a project directory, but the action #{@run_description.klass} requires one. Current directory is not an epubforge project."
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def map_keyword_to_action
|
93
|
+
actions = actions_lookup.keyword_to_action( @run_description.keyword )
|
94
|
+
|
95
|
+
if actions.length == 1
|
96
|
+
@run_description.klass = actions.first
|
97
|
+
elsif actions.length == 0
|
98
|
+
@run_description.errors << "Unrecognized keyword <#{keyword}>. Quitting."
|
99
|
+
false
|
100
|
+
else
|
101
|
+
@run_description.errors << "Ambiguous keyword <#{keyword}>. Did you mean...?\n#{actions.map(&:usage).join('\n')}"
|
102
|
+
false
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_explicit_project_option( args )
|
107
|
+
proj_opt_regex = /^--project=/
|
108
|
+
|
109
|
+
proj_opt = args.find do |arg|
|
110
|
+
arg.is_a?(String) && arg.match( proj_opt_regex )
|
111
|
+
end
|
112
|
+
|
113
|
+
if proj_opt
|
114
|
+
args.delete( proj_opt )
|
115
|
+
proj_opt.gsub( proj_opt, '' ).fwf_filepath
|
116
|
+
else
|
117
|
+
false
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|