bookshelf 1.0.0

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.
@@ -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