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.
Files changed (83) hide show
  1. data/Gemfile +26 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.rdoc +26 -0
  4. data/Rakefile +71 -0
  5. data/VERSION +1 -0
  6. data/bin/epubforge +10 -0
  7. data/config/actions/book_to_epub.rb +20 -0
  8. data/config/actions/generate.rb +24 -0
  9. data/config/actions/generate_chapter.rb +26 -0
  10. data/config/actions/git_backup.rb +23 -0
  11. data/config/actions/gitify.rb +72 -0
  12. data/config/actions/globals.rb +77 -0
  13. data/config/actions/help.rb +21 -0
  14. data/config/actions/init.rb +137 -0
  15. data/config/actions/kindle.rb +68 -0
  16. data/config/actions/notes_to_epub.rb +20 -0
  17. data/config/actions/notes_to_kindle.rb +17 -0
  18. data/config/actions/word_count.rb +126 -0
  19. data/config/actions/wrap_scene_notes_in_hidden_div.rb +118 -0
  20. data/config/htmlizers.rb +62 -0
  21. data/lib/action/actions_lookup.rb +41 -0
  22. data/lib/action/cli_command.rb +72 -0
  23. data/lib/action/cli_sequence.rb +55 -0
  24. data/lib/action/file_transformer.rb +59 -0
  25. data/lib/action/run_description.rb +24 -0
  26. data/lib/action/runner.rb +122 -0
  27. data/lib/action/thor_action.rb +149 -0
  28. data/lib/core_extensions/array.rb +5 -0
  29. data/lib/core_extensions/kernel.rb +42 -0
  30. data/lib/core_extensions/nil_class.rb +5 -0
  31. data/lib/core_extensions/object.rb +5 -0
  32. data/lib/core_extensions/string.rb +37 -0
  33. data/lib/custom_helpers.rb +60 -0
  34. data/lib/epub/assets/asset.rb +11 -0
  35. data/lib/epub/assets/html.rb +8 -0
  36. data/lib/epub/assets/image.rb +18 -0
  37. data/lib/epub/assets/markdown.rb +8 -0
  38. data/lib/epub/assets/page.rb +32 -0
  39. data/lib/epub/assets/stylesheet.rb +22 -0
  40. data/lib/epub/assets/textile.rb +8 -0
  41. data/lib/epub/builder.rb +270 -0
  42. data/lib/epub/packager.rb +16 -0
  43. data/lib/epubforge.rb +97 -0
  44. data/lib/errors.rb +8 -0
  45. data/lib/project/project.rb +65 -0
  46. data/lib/utils/action_loader.rb +7 -0
  47. data/lib/utils/class_loader.rb +83 -0
  48. data/lib/utils/directory_builder.rb +181 -0
  49. data/lib/utils/downloader.rb +58 -0
  50. data/lib/utils/file_orderer.rb +45 -0
  51. data/lib/utils/file_path.rb +152 -0
  52. data/lib/utils/html_translator.rb +99 -0
  53. data/lib/utils/html_translator_queue.rb +70 -0
  54. data/lib/utils/htmlizer.rb +92 -0
  55. data/lib/utils/misc.rb +20 -0
  56. data/lib/utils/root_path.rb +20 -0
  57. data/lib/utils/settings.rb +146 -0
  58. data/lib/utils/template_evaluator.rb +20 -0
  59. data/templates/default/book/afterword.markdown.template +4 -0
  60. data/templates/default/book/chapter-%i%.markdown.sequence +4 -0
  61. data/templates/default/book/foreword.markdown.template +6 -0
  62. data/templates/default/book/images/cover.png +0 -0
  63. data/templates/default/book/stylesheets/stylesheet.css.template +2 -0
  64. data/templates/default/book/title_page.markdown.template +4 -0
  65. data/templates/default/notes/character.named.markdown.template +4 -0
  66. data/templates/default/notes/stylesheets/stylesheet.css.template +2 -0
  67. data/templates/default/payload.rb +65 -0
  68. data/templates/default/settings/actions/local_action.rb.example +14 -0
  69. data/templates/default/settings/config.rb.form +55 -0
  70. data/templates/default/settings/htmlizers.rb +0 -0
  71. data/templates/default/settings/wordcount.template +6 -0
  72. data/test/helper.rb +22 -0
  73. data/test/misc/config.rb +7 -0
  74. data/test/sample_text/sample.markdown +30 -0
  75. data/test/sample_text/sample.textile +24 -0
  76. data/test/test_custom_helpers.rb +22 -0
  77. data/test/test_directory_builder.rb +141 -0
  78. data/test/test_epf_root.rb +9 -0
  79. data/test/test_epubforge.rb +164 -0
  80. data/test/test_htmlizers.rb +24 -0
  81. data/test/test_runner.rb +15 -0
  82. data/test/test_utils.rb +39 -0
  83. 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
@@ -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