require 'ostruct' class Softcover::BookManifest < OpenStruct include Softcover::Utils class NotFound < StandardError def message "Invalid book directory, no manifest file found!" end end class Chapter < OpenStruct def path File.join('chapters', slug + '.tex') end def fragment_name "#{slug}_fragment.html" end def fragment_path File.join('html', fragment_name) end def nodes @nodes ||= [] end # Returns a chapter heading for use in the navigation menu. def menu_heading raw_html = Polytexnic::Pipeline.new(title).to_html html = Nokogiri::HTML(raw_html).at_css('p').inner_html chapter_number.zero? ? html : "Chapter #{chapter_number}: #{html}" end def to_hash marshal_dump.merge({ menu_heading: menu_heading }) end end class Section < OpenStruct end MD_PATH = 'Book.txt' YAML_PATH = "book.yml" def initialize(options = {}) @source = options[:source] || :polytex @origin = options[:origin] yaml_attrs = read_from_yml attrs = case when polytex? then yaml_attrs when markdown? then yaml_attrs.merge(read_from_md) else self.class.not_found! end.symbolize_keys! marshal_load attrs if polytex? tex_filename = filename + '.tex' self.chapters = [] self.frontmatter = [] base_contents = File.read(tex_filename) if base_contents.match(/frontmatter/) @frontmatter = true chapters.push Chapter.new(slug: 'frontmatter', title: 'Frontmatter', sections: nil, chapter_number: 0) end raw_frontmatter = remove_frontmatter(base_contents, frontmatter) if frontmatter? self.frontmatter = chapter_includes(raw_frontmatter) else self.frontmatter = [] end self.author = base_contents.scan(/^\s*\\author\{(.*?)\}/).flatten.first chapter_includes(base_contents).each_with_index do |name, i| slug = File.basename(name, '.*') title_regex = /^\s*\\chapter{(.*)}/ content = File.read(File.join(polytex_dir, slug + '.tex')) title = content[title_regex, 1] j = 0 sections = content.scan(/^\s*\\section{(.*)}/).flatten.map do |name| Section.new(name: name, section_number: j += 1) end chapters.push Chapter.new(slug: slug, title: title, sections: sections, chapter_number: i + 1) end end verify_paths! if options[:verify_paths] end # Returns the directory where the LaTeX files are located. # We put them in the a separate directory when using them as an intermediate # format when working with Markdown books. Otherwise, we use the chapters # directory, which is the default location when writing LaTeX/PolyTeX books. def polytex_dir dir = (markdown? || @origin == :markdown) ? 'generated_polytex' : 'chapters' mkdir dir dir end # Returns an array of the chapters to include. def chapter_includes(string) chapter_regex = /^\s*\\include\{#{polytex_dir}\/(.*?)\}/ string.scan(chapter_regex).flatten end # Removes frontmatter. # The frontmatter shouldn't be included in the chapter slugs, so we remove # it. For example, in # \frontmatter # \maketitle # \tableofcontents # % List frontmatter sections here (preface, foreword, etc.). # \include{chapters/preface} # \mainmatter # % List chapters here in the order they should appear in the book. # \include{chapters/a_chapter} # we don't want to include the preface. def remove_frontmatter(base_contents, frontmatter) base_contents.gsub!(/\\frontmatter(.*)\\mainmatter/m, '') $1 end # Returns true if the book has frontmatter. def frontmatter? @frontmatter end # Returns the first full chapter. # This arranges to skip the frontmatter, if any. def first_chapter frontmatter? ? chapters[1] : chapters[0] end # Returns true if converting Markdown source. def markdown? @source == :markdown || @source == :md end alias :md? :markdown? # Returns true if converting PolyTeX source. def polytex? @source == :polytex end # Returns an iterator for the chapter file paths. def chapter_file_paths pdf_chapter_names.map do |name| file_path = case when markdown? || @origin == :markdown File.join("chapters", "#{name}.md") when polytex? File.join("chapters", "#{name}.tex") end yield file_path if block_given? file_path end end # Returns chapters for the PDF. # The frontmatter pseudo-chapter exists for the sake of HTML/EPUB/MOBI, so # it's not returned as part of the chapters. def pdf_chapter_names chaps = chapters.reject { |chapter| chapter.slug.match(/frontmatter/) }. collect(&:slug) frontmatter? ? frontmatter + chaps : chaps end # Returns the full chapter filenames for the PDF. def pdf_chapter_filenames pdf_chapter_names.map { |name| File.join(polytex_dir, "#{name}.tex") } end def find_chapter_by_slug(slug) chapters.find { |chapter| chapter.slug == slug } end def find_chapter_by_number(number) chapters.find { |chapter| chapter.chapter_number == number } end # Returns a URL for the chapter with the given number. def url(chapter_number) if (chapter = find_chapter_by_number(chapter_number)) chapter.slug else '#' end end # Returns the chapter range for book previews. # We could `eval` the range, but that would allow users to execute arbitrary # code (maybe not a big problem on their system, but it would be a Bad Thing # on a server). def preview_chapter_range first, last = epub_mobi_preview_chapter_range.split('..').map(&:to_i) first..last end # Returns the chapters to use in the preview as a range. def preview_chapters chapters[preview_chapter_range] end def self.valid_directory? [YAML_PATH, MD_PATH].any? { |f| File.exist?(f) } end # Changes the directory until in the book's root directory. def self.find_book_root! loop do return true if valid_directory? return not_found! if Dir.pwd == '/' Dir.chdir '..' end end def self.not_found! raise NotFound end private def read_from_yml require 'softcover/config' require 'yaml/store' self.class.find_book_root! YAML.load_file(YAML_PATH) end def read_from_md self.class.find_book_root! chapters = File.readlines(MD_PATH).select do |path| path =~ /(.*)\.md/ end.map do |file| Chapter.new(slug: File.basename(file.strip, '.md')) end { chapters: chapters, filename: MD_PATH } end def verify_paths! chapter_file_paths do |chapter_path, i| next if chapter_path =~ /frontmatter/ unless File.exist?(chapter_path) raise "Chapter file in manifest not found in #{chapter_path}" end end end end