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
         |