epubforge 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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