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,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
|