bookshelf 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ module Bookshelf
2
+ class Dependency
3
+ def self.kindlegen?
4
+ @kindlegen ||= `which kindlegen` && $?.success?
5
+ end
6
+
7
+ def self.prince?
8
+ @prince ||= `which prince` && $?.success?
9
+ end
10
+
11
+ def self.html2text?
12
+ @html2text ||= `which html2text` && $?.success?
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module Bookshelf
2
+ class DirectoryAlreadyCreatedError < StandardError; end
3
+ end
@@ -0,0 +1,56 @@
1
+ module Bookshelf
2
+ class Exporter
3
+ def self.run(book_dir, options)
4
+ exporter = new(book_dir, options)
5
+ exporter.export!
6
+ end
7
+
8
+ attr_accessor :book_dir
9
+ attr_accessor :options
10
+
11
+ def initialize(book_dir, options)
12
+ @book_dir = book_dir
13
+ @options = options
14
+ end
15
+
16
+ def ui
17
+ @ui ||= Thor::Base.shell.new
18
+ end
19
+
20
+ def export!
21
+ helper = Bookshelf.root_dir.join("config/helper.rb")
22
+ load(helper) if helper.exist?
23
+
24
+ export_pdf = [nil, "pdf"].include?(options[:only])
25
+ export_epub = [nil, "mobi", "epub"].include?(options[:only])
26
+ export_mobi = [nil, "mobi"].include?(options[:only])
27
+ export_txt = [nil, "txt"].include?(options[:only])
28
+
29
+ exported = []
30
+ exported << Parser::HTML.parse(book_dir)
31
+ exported << Parser::PDF.parse(book_dir) if export_pdf && Dependency.prince?
32
+ exported << Parser::Epub.parse(book_dir) if export_epub
33
+ exported << Parser::Mobi.parse(book_dir) if export_mobi && Dependency.kindlegen?
34
+ exported << Parser::Txt.parse(book_dir) if export_txt && Dependency.html2text?
35
+
36
+ if exported.all?
37
+ color = :green
38
+ message = options[:auto] ? "exported!" : "** e-book has been exported"
39
+ Notifier.notify(
40
+ :image => Bookshelf::ROOT.join("templates/ebook.png"),
41
+ :title => "Bookshelf",
42
+ :message => "Your \"#{config[:title]}\" e-book has been exported!"
43
+ )
44
+ else
45
+ color = :red
46
+ message = options[:auto] ? "could not be exported!" : "** e-book couldn't be exported"
47
+ end
48
+
49
+ ui.say message, color
50
+ end
51
+
52
+ def config
53
+ Bookshelf.config(book_dir)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ module RedCloth
2
+ INLINE_FORMATTERS = [:textile, :footnote, :link]
3
+
4
+ def self.convert(text)
5
+ new(text).to_html(*INLINE_FORMATTERS)
6
+ end
7
+
8
+ module Inline
9
+ FN_RE = /
10
+ (\s+)? # getting spaces
11
+ (\\)?%\{ # opening
12
+ (.*?) # footnote
13
+ \}# # closing
14
+ /xm
15
+
16
+ def footnote(text)
17
+ text.gsub!(FN_RE) do |m|
18
+ if $2
19
+ %[#{$1}%{#{$3}}]
20
+ else
21
+ %(<span class="footnote">#{$3}</span>)
22
+ end
23
+ end
24
+ end
25
+
26
+ LINK_RE = /
27
+ <
28
+ ((?:https?|ftp):\/\/.*?)
29
+ >
30
+ /xm
31
+
32
+ def link(text)
33
+ text.gsub!(LINK_RE) do |m|
34
+ %(<a href="#{$1}">#{$1}</a>)
35
+ end
36
+ end
37
+ end
38
+
39
+ module Formatters
40
+ module HTML
41
+ def figure(options = {})
42
+ %[<p class="figure"><img src="../images/#{options[:text]}" alt="#{options[:class]}" /><br/><span class="caption">#{options[:class]}</span></p>]
43
+ end
44
+
45
+ def note(options = {})
46
+ %[<p class="note">#{options[:text]}</p>]
47
+ end
48
+
49
+ def attention(options = {})
50
+ %[<p class="attention">#{options[:text]}</p>]
51
+ end
52
+
53
+ def file(options = {})
54
+ base_url = Bookshelf.config[:base_url]
55
+
56
+ if base_url
57
+ url = File.join(base_url, options[:text])
58
+ else
59
+ url = content
60
+ $stderr << "\nYou're using `file. #{content}` but didn't set base_url in your configuration file.\n"
61
+ end
62
+
63
+ %[<p class="file"><span><strong>Download</strong> <a href="#{url}">#{options[:text]}</a></span></p>]
64
+ end
65
+ end
66
+ end
67
+ end
68
+
69
+ RedCloth.send(:include, RedCloth::Inline)
@@ -0,0 +1,11 @@
1
+ class String
2
+ def to_permalink
3
+ str = ActiveSupport::Multibyte::Chars.new(self.dup)
4
+ str = str.normalize(:kd).gsub(/[^\x00-\x7F]/,'').to_s
5
+ str.gsub!(/[^-\w\d]+/xim, "-")
6
+ str.gsub!(/-+/xm, "-")
7
+ str.gsub!(/^-?(.*?)-?$/, '\1')
8
+ str.downcase!
9
+ str
10
+ end
11
+ end
@@ -0,0 +1,75 @@
1
+ module Bookshelf
2
+ # The Bookshelf::Generator class will create a new book structure.
3
+ #
4
+ # ebook = Bookshelf::Generator.new
5
+ # ebook.destination_root = "/some/path/book-name"
6
+ # ebook.invoke_all
7
+ #
8
+ class Generator < Thor::Group
9
+ include Thor::Actions
10
+
11
+ desc "Generate a new e-Book structure"
12
+
13
+ def self.source_root
14
+ File.dirname(__FILE__) + "/../../templates"
15
+ end
16
+
17
+ def copy_html_templates
18
+ copy_file "layout.erb" , "templates/html/layout.erb"
19
+ copy_file "layout.css" , "templates/html/layout.css"
20
+ copy_file "user.css" , "templates/html/user.css"
21
+ copy_file "syntax.css" , "templates/html/syntax.css"
22
+ end
23
+
24
+ def copy_epub_templates
25
+ copy_file "cover.erb" , "templates/epub/cover.erb"
26
+ copy_file "epub.css" , "templates/epub/user.css"
27
+ copy_file "epub.erb" , "templates/epub/page.erb"
28
+ copy_file "cover.png" , "templates/epub/cover.png"
29
+ end
30
+
31
+ def copy_sample_page
32
+ copy_file "sample.md" , "text/01_Welcome.md"
33
+ end
34
+
35
+ def copy_config_file
36
+ @name = full_name
37
+ @uid = Digest::MD5.hexdigest("#{Time.now}--#{rand}")
38
+ @year = Date.today.year
39
+ template "config.erb", "config/bookshelf.yml"
40
+ end
41
+
42
+ def copy_helper_file
43
+ copy_file "helper.rb", "config/helper.rb"
44
+ end
45
+
46
+ def create_directories
47
+ empty_directory "output"
48
+ empty_directory "images"
49
+ empty_directory "code"
50
+ end
51
+
52
+ def create_git_files
53
+ create_file ".gitignore" do
54
+ "output/*.{html,epub,pdf}\noutput/tmp"
55
+ end
56
+
57
+ create_file "output/.gitkeep"
58
+ create_file "images/.gitkeep"
59
+ create_file "code/.gitkeep"
60
+ end
61
+
62
+ def copy_guardfile
63
+ copy_file "Guardfile", "Guardfile"
64
+ end
65
+
66
+ private
67
+ # Retrieve user's name using finger.
68
+ # Defaults to <tt>John Doe</tt>.
69
+ #
70
+ def full_name
71
+ name = `finger $USER 2> /dev/null | grep Login | colrm 1 46`.chomp
72
+ name.present? ? name.squish : "John Doe"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ require 'open3'
2
+
3
+ module Bookshelf
4
+ module Parser
5
+ autoload :HTML , "bookshelf/parser/html"
6
+ autoload :PDF , "bookshelf/parser/pdf"
7
+ autoload :Epub , "bookshelf/parser/epub"
8
+ autoload :Mobi , "bookshelf/parser/mobi"
9
+ autoload :Txt , "bookshelf/parser/txt"
10
+
11
+ class Base
12
+ # The e-book directory.
13
+ #
14
+ attr_accessor :book_dir
15
+
16
+ def self.parse(book_dir)
17
+ new(book_dir).parse
18
+ end
19
+
20
+ def initialize(book_dir)
21
+ @book_dir = Pathname.new(book_dir)
22
+ end
23
+
24
+ # Return directory's basename.
25
+ #
26
+ def name
27
+ File.basename(book_dir)
28
+ end
29
+
30
+ # Return the configuration file.
31
+ #
32
+ def config
33
+ Bookshelf.config(book_dir)
34
+ end
35
+
36
+ # Render a eRb template using +locals+ as data seed.
37
+ #
38
+ def render_template(file, locals = {})
39
+ ERB.new(File.read(file)).result OpenStruct.new(locals).instance_eval{ binding }
40
+ end
41
+
42
+ def spawn_command(cmd)
43
+ begin
44
+ stdout_and_stderr, status = Open3.capture2e(*cmd)
45
+ rescue Errno::ENOENT => e
46
+ puts e.message
47
+ else
48
+ puts stdout_and_stderr unless status.success?
49
+ status.success?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,146 @@
1
+ module Bookshelf
2
+ module Parser
3
+ class Epub < Base
4
+ def sections
5
+ @sections ||= html.css("div.chapter").each_with_index.map do |chapter, index|
6
+ OpenStruct.new({
7
+ :index => index,
8
+ :filename => "section_#{index}.html",
9
+ :filepath => tmp_dir.join("section_#{index}.html").to_s,
10
+ :html => Nokogiri::HTML(chapter.inner_html)
11
+ })
12
+ end
13
+ end
14
+
15
+ def epub
16
+ @epub ||= EeePub::Maker.new
17
+ end
18
+
19
+ def html
20
+ @html ||= Nokogiri::HTML(html_path.read)
21
+ end
22
+
23
+ def parse
24
+ epub.title config[:title]
25
+ epub.language config[:language]
26
+ epub.creator config[:authors].to_sentence
27
+ epub.publisher config[:publisher]
28
+ epub.date config[:published_at]
29
+ epub.uid config[:uid]
30
+ epub.identifier config[:identifier][:id], :scheme => config[:identifier][:type]
31
+ epub.cover_page cover_image if cover_image && File.exist?(cover_image)
32
+
33
+ write_sections!
34
+ write_toc!
35
+
36
+ epub.files sections.map(&:filepath) + assets
37
+ epub.nav navigation
38
+ epub.toc_page toc_path
39
+
40
+ epub.save(epub_path)
41
+
42
+ true
43
+ rescue Exception
44
+ p $!, $@
45
+ false
46
+ end
47
+
48
+ def write_toc!
49
+ toc = TOC::Epub.new(navigation)
50
+
51
+ File.open(toc_path, "w") do |file|
52
+ file << toc.to_html
53
+ end
54
+ end
55
+
56
+ def write_sections!
57
+ # First we need to get all ids, which are used as
58
+ # the anchor target.
59
+ #
60
+ links = sections.inject({}) do |buffer, section|
61
+ section.html.css("[id]").each do |element|
62
+ anchor = "##{element["id"]}"
63
+ buffer[anchor] = "#{section.filename}#{anchor}"
64
+ end
65
+
66
+ buffer
67
+ end
68
+
69
+ # Then we can normalize all links and
70
+ # manipulate other paths.
71
+ #
72
+ sections.each do |section|
73
+ section.html.css("a[href^='#']").each do |link|
74
+ href = link["href"]
75
+ link.set_attribute("href", links.fetch(href, href))
76
+ end
77
+
78
+ # Replace all srcs.
79
+ #
80
+ section.html.css("[src]").each do |element|
81
+ src = File.basename(element["src"]).gsub(/\.svg$/, ".png")
82
+ element.set_attribute("src", src)
83
+ element.set_attribute("alt", "")
84
+ element.node_name = "img"
85
+ end
86
+
87
+ FileUtils.mkdir_p(tmp_dir)
88
+
89
+ # Save file to disk.
90
+ #
91
+ File.open(section.filepath, "w") do |file|
92
+ body = section.html.css("body").to_xhtml.gsub(%r[<body>(.*?)</body>]m, "\\1")
93
+ file << render_chapter(body)
94
+ end
95
+ end
96
+ end
97
+
98
+ def render_chapter(content)
99
+ locals = config.merge(:content => content)
100
+ render_template(template_path, locals)
101
+ end
102
+
103
+ def assets
104
+ @assets ||= begin
105
+ assets = Dir[Bookshelf.root_dir.join("templates/epub/*.css")]
106
+ assets += Dir[Bookshelf.root_dir.join("assets/images/**/*.{jpg,png,gif}")]
107
+ assets
108
+ end
109
+ end
110
+
111
+ def cover_image
112
+ path = Dir[book_dir.join("cover.{jpg,png,gif}").to_s].first
113
+ return path if path && File.exist?(path)
114
+ end
115
+
116
+ def navigation
117
+ sections.map do |section|
118
+ {
119
+ :label => section.html.css("h2:first-of-type").text,
120
+ :content => section.filename
121
+ }
122
+ end
123
+ end
124
+
125
+ def template_path
126
+ Bookshelf.root_dir.join("templates/epub/page.erb")
127
+ end
128
+
129
+ def html_path
130
+ Bookshelf.root_dir.join("output/#{name}.html")
131
+ end
132
+
133
+ def epub_path
134
+ Bookshelf.root_dir.join("output/#{name}.epub")
135
+ end
136
+
137
+ def tmp_dir
138
+ Bookshelf.root_dir.join("output/tmp")
139
+ end
140
+
141
+ def toc_path
142
+ tmp_dir.join("toc.html")
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,177 @@
1
+ module Bookshelf
2
+ module Parser
3
+ class HTML < Base
4
+ # List of directories that should be skipped.
5
+ #
6
+ IGNORE_DIR = %w[. .. .svn]
7
+
8
+ # Files that should be skipped.
9
+ #
10
+ IGNORE_FILES = /^(TOC)\..*?$/
11
+
12
+ # List of recognized extensions.
13
+ #
14
+ EXTENSIONS = %w[md mkdn markdown textile html]
15
+
16
+ class << self
17
+ # The footnote index control. We have to manipulate footnotes
18
+ # because each chapter starts from 1, so we have duplicated references.
19
+ #
20
+ attr_accessor :footnote_index
21
+ end
22
+
23
+ # Parse all files and save the parsed content
24
+ # to <tt>output/book_name.html</tt>.
25
+ #
26
+ def parse
27
+ reset_footnote_index!
28
+
29
+ File.open(Bookshelf.root_dir.join("output/#{name}.html"), "w") do |file|
30
+ file << parse_layout(content)
31
+ end
32
+ true
33
+ # rescue Exception
34
+ # false
35
+ end
36
+
37
+ def reset_footnote_index!
38
+ self.class.footnote_index = 1
39
+ end
40
+
41
+ # Return all chapters wrapped in a <tt>div.chapter</tt> tag.
42
+ #
43
+ def content
44
+ String.new.tap do |chapters|
45
+ entries.each do |entry|
46
+ files = chapter_files(entry)
47
+
48
+ # no markup files, so skip to the next one!
49
+ next if files.empty?
50
+
51
+ chapters << %[<div class="chapter">#{render_chapter(files)}</div>]
52
+ end
53
+ end
54
+ end
55
+
56
+ # Return a list of all recognized files.
57
+ #
58
+ def entries
59
+ Dir.entries(book_dir).sort.inject([]) do |buffer, entry|
60
+ buffer << book_dir.join(entry) if valid_entry?(entry)
61
+ buffer
62
+ end
63
+ end
64
+
65
+ private
66
+ def chapter_files(entry)
67
+ # Chapters can be files outside a directory.
68
+ if File.file?(entry)
69
+ [entry]
70
+ else
71
+ Dir.glob("#{entry}/**/*.{#{EXTENSIONS.join(",")}}").sort
72
+ end
73
+ end
74
+
75
+ # Check if path is a valid entry.
76
+ # Files/directories that start with a dot or underscore will be skipped.
77
+ #
78
+ def valid_entry?(entry)
79
+ entry !~ /^(\.|_)/ && (valid_directory?(entry) || valid_file?(entry))
80
+ end
81
+
82
+ # Check if path is a valid directory.
83
+ #
84
+ def valid_directory?(entry)
85
+ File.directory?(book_dir.join(entry)) && !IGNORE_DIR.include?(File.basename(entry))
86
+ end
87
+
88
+ # Check if path is a valid file.
89
+ #
90
+ def valid_file?(entry)
91
+ ext = File.extname(entry).gsub(/\./, "").downcase
92
+ File.file?(book_dir.join(entry)) && EXTENSIONS.include?(ext) && entry !~ IGNORE_FILES
93
+ end
94
+
95
+ # Render +file+ considering its extension.
96
+ #
97
+ def render_file(file, plain_syntax = false)
98
+ file_format = format(file)
99
+ content = Bookshelf::Syntax.render(book_dir, file_format, File.read(file), plain_syntax)
100
+ content = case file_format
101
+ when :markdown
102
+ Markdown.to_html(content)
103
+ when :textile
104
+ RedCloth.convert(content)
105
+ else
106
+ content
107
+ end
108
+
109
+ render_footnotes(content, plain_syntax)
110
+ end
111
+
112
+ def render_footnotes(content, plain_syntax = false)
113
+ html = Nokogiri::HTML(content)
114
+ footnotes = html.css("p[id^='fn']")
115
+
116
+ return content if footnotes.empty?
117
+
118
+ reset_footnote_index! unless self.class.footnote_index
119
+
120
+ footnotes.each do |fn|
121
+ index = self.class.footnote_index
122
+ actual_index = fn["id"].gsub(/[^\d]/, "")
123
+
124
+ fn.set_attribute("id", "_fn#{index}")
125
+
126
+ html.css("a[href='#fn#{actual_index}']").each do |link|
127
+ link.set_attribute("href", "#_fn#{index}")
128
+ end
129
+
130
+ html.css("a[href='#fnr#{actual_index}']").each do |link|
131
+ link.set_attribute("href", "#_fnr#{index}")
132
+ end
133
+
134
+ html.css("[id=fnr#{actual_index}]").each do |tag|
135
+ tag.set_attribute("id", "_fnr#{index}")
136
+ end
137
+
138
+ self.class.footnote_index += 1
139
+ end
140
+
141
+ html.css("body").inner_html
142
+ end
143
+
144
+ def format(file)
145
+ case File.extname(file).downcase
146
+ when ".markdown", ".mkdn", ".md"
147
+ :markdown
148
+ when ".textile"
149
+ :textile
150
+ else
151
+ :html
152
+ end
153
+ end
154
+
155
+ # Parse layout file, making available all configuration entries.
156
+ #
157
+ def parse_layout(html)
158
+ toc = TOC::HTML.generate(html)
159
+ locals = config.merge({
160
+ :content => toc.content,
161
+ :toc => toc.to_html
162
+ })
163
+ render_template(Bookshelf.root_dir.join("templates/html/layout.erb"), locals)
164
+ end
165
+
166
+ # Render all +files+ from a given chapter.
167
+ #
168
+ def render_chapter(files, plain_syntax = false)
169
+ String.new.tap do |chapter|
170
+ files.each do |file|
171
+ chapter << render_file(file, plain_syntax) << "\n\n"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end