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.
- data/.gitignore +4 -0
- data/.gitmodules +3 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +66 -0
- data/README.md +162 -0
- data/Rakefile +5 -0
- data/bin/bookshelf +5 -0
- data/bookshelf.gemspec +37 -0
- data/lib/bookshelf.rb +64 -0
- data/lib/bookshelf/adapters/markdown.rb +34 -0
- data/lib/bookshelf/cli.rb +125 -0
- data/lib/bookshelf/dependency.rb +15 -0
- data/lib/bookshelf/errors.rb +3 -0
- data/lib/bookshelf/exporter.rb +56 -0
- data/lib/bookshelf/extensions/redcloth.rb +69 -0
- data/lib/bookshelf/extensions/string.rb +11 -0
- data/lib/bookshelf/generator.rb +75 -0
- data/lib/bookshelf/parser.rb +54 -0
- data/lib/bookshelf/parser/epub.rb +146 -0
- data/lib/bookshelf/parser/html.rb +177 -0
- data/lib/bookshelf/parser/mobi.rb +14 -0
- data/lib/bookshelf/parser/pdf.rb +44 -0
- data/lib/bookshelf/parser/txt.rb +18 -0
- data/lib/bookshelf/stats.rb +45 -0
- data/lib/bookshelf/stream.rb +27 -0
- data/lib/bookshelf/syntax.rb +124 -0
- data/lib/bookshelf/toc.rb +6 -0
- data/lib/bookshelf/toc/epub.rb +41 -0
- data/lib/bookshelf/toc/html.rb +78 -0
- data/lib/bookshelf/version.rb +8 -0
- data/templates/Guardfile +12 -0
- data/templates/config.erb +44 -0
- data/templates/cover.erb +16 -0
- data/templates/cover.png +0 -0
- data/templates/ebook.png +0 -0
- data/templates/epub.css +500 -0
- data/templates/epub.erb +15 -0
- data/templates/helper.rb +29 -0
- data/templates/layout.css +353 -0
- data/templates/layout.erb +44 -0
- data/templates/user.css +1 -0
- metadata +244 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
module Parser
|
3
|
+
class PDF < Base
|
4
|
+
def parse
|
5
|
+
apply_footnotes!
|
6
|
+
spawn_command ["prince", with_footnotes_file.to_s, "-o", pdf_file.to_s]
|
7
|
+
end
|
8
|
+
|
9
|
+
def apply_footnotes!
|
10
|
+
html = Nokogiri::HTML(html_file.read)
|
11
|
+
|
12
|
+
# https://github.com/sparklemotion/nokogiri/issues/339
|
13
|
+
html.css("html").first.tap do |element|
|
14
|
+
next unless element
|
15
|
+
element.delete("xmlns")
|
16
|
+
element.delete("xml:lang")
|
17
|
+
end
|
18
|
+
|
19
|
+
html.css("p.footnote[id^='_fn']").each do |fn|
|
20
|
+
fn.node_name = "span"
|
21
|
+
fn.set_attribute("class", "fn")
|
22
|
+
|
23
|
+
html.css("[href='##{fn["id"]}']").each do |link|
|
24
|
+
link.add_next_sibling(fn)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
File.open(with_footnotes_file, "w") {|f| f << html.to_xhtml}
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_footnotes_file
|
32
|
+
Bookshelf.root_dir.join("output/#{name}.pdf.html")
|
33
|
+
end
|
34
|
+
|
35
|
+
def html_file
|
36
|
+
Bookshelf.root_dir.join("output/#{name}.html")
|
37
|
+
end
|
38
|
+
|
39
|
+
def pdf_file
|
40
|
+
Bookshelf.root_dir.join("output/#{name}.pdf")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
module Parser
|
3
|
+
class Txt < Base
|
4
|
+
def parse
|
5
|
+
spawn_command ["html2text", "-style", "pretty", "-nobs", "-o", txt_file.to_s, html_file.to_s]
|
6
|
+
end
|
7
|
+
|
8
|
+
def html_file
|
9
|
+
root_dir.join("output/#{name}.html")
|
10
|
+
end
|
11
|
+
|
12
|
+
def txt_file
|
13
|
+
root_dir.join("output/#{name}.txt")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
class Stats
|
3
|
+
attr_reader :root_dir
|
4
|
+
|
5
|
+
def initialize(root_dir)
|
6
|
+
@root_dir = root_dir
|
7
|
+
end
|
8
|
+
|
9
|
+
def text
|
10
|
+
@text ||= html.text
|
11
|
+
end
|
12
|
+
|
13
|
+
def html
|
14
|
+
@html ||= Nokogiri::HTML(content)
|
15
|
+
end
|
16
|
+
|
17
|
+
def words
|
18
|
+
@words ||= text.split(" ").size
|
19
|
+
end
|
20
|
+
|
21
|
+
def chapters
|
22
|
+
@chapters ||= html.css(".chapter").size
|
23
|
+
end
|
24
|
+
|
25
|
+
def images
|
26
|
+
@images ||= html.css("img").size
|
27
|
+
end
|
28
|
+
|
29
|
+
def footnotes
|
30
|
+
@footnotes ||= html.css("p.footnote").size
|
31
|
+
end
|
32
|
+
|
33
|
+
def links
|
34
|
+
@links ||= html.css("[href^='http']").size
|
35
|
+
end
|
36
|
+
|
37
|
+
def code_blocks
|
38
|
+
@code_blocks ||= html.css("pre").size
|
39
|
+
end
|
40
|
+
|
41
|
+
def content
|
42
|
+
@content ||= Parser::HTML.new(root_dir).content
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
class Stream
|
3
|
+
attr_accessor :listener, :content
|
4
|
+
attr_reader :html
|
5
|
+
|
6
|
+
def initialize(content, listener)
|
7
|
+
@content = content
|
8
|
+
@listener = listener
|
9
|
+
@html = Nokogiri::HTML.parse(content)
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse
|
13
|
+
traverse(html)
|
14
|
+
end
|
15
|
+
|
16
|
+
def traverse(node)
|
17
|
+
node.children.each do |child|
|
18
|
+
emit(child)
|
19
|
+
traverse(child)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def emit(node)
|
24
|
+
listener.send(:tag, node) if node.name =~ /h[1-6]/
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
class Syntax
|
3
|
+
autoload :Highlight, "bookshelf/syntax/highlight"
|
4
|
+
|
5
|
+
attr_reader :io
|
6
|
+
attr_reader :lines
|
7
|
+
attr_reader :book_dir
|
8
|
+
attr_reader :format
|
9
|
+
|
10
|
+
# Render syntax blocks from specified source code.
|
11
|
+
#
|
12
|
+
# dir = Pathname.new(File.dirname(__FILE__))
|
13
|
+
# text = File.read(dir.join("text/some_file.textile"))
|
14
|
+
# Bookshelf::Syntax.render(dir, :textile, text)
|
15
|
+
#
|
16
|
+
def self.render(book_dir, format, source_code, raw = false)
|
17
|
+
source_code.gsub(/@@@(.*?)@@@/m) do |match|
|
18
|
+
new(book_dir, format, $1, raw).process
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Process each syntax block individually.
|
23
|
+
#
|
24
|
+
def initialize(book_dir, format, code, raw = false)
|
25
|
+
@format = format
|
26
|
+
@book_dir = book_dir
|
27
|
+
@io = StringIO.new(code)
|
28
|
+
@lines = io.readlines.collect(&:chomp)
|
29
|
+
@language = 'text' if raw
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return unprocessed line codes.
|
33
|
+
#
|
34
|
+
def raw
|
35
|
+
lines[1..-1].join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return meta data from syntax annotation.
|
39
|
+
#
|
40
|
+
def meta
|
41
|
+
@meta ||= begin
|
42
|
+
line = lines.first.squish
|
43
|
+
_, language, file, modifier, reference = *line.match(/^([^ ]+)(?: ([^:#]+)(?:(:|#)(.*?))?)?$/)
|
44
|
+
|
45
|
+
if modifier == "#"
|
46
|
+
type = :block
|
47
|
+
elsif modifier == ":"
|
48
|
+
type = :range
|
49
|
+
elsif file
|
50
|
+
type = :file
|
51
|
+
else
|
52
|
+
type = :inline
|
53
|
+
end
|
54
|
+
|
55
|
+
{
|
56
|
+
:language => language,
|
57
|
+
:file => file,
|
58
|
+
:type => type,
|
59
|
+
:reference => reference
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Process syntax block, returning a +pre+ HTML tag.
|
65
|
+
#
|
66
|
+
def process
|
67
|
+
code = raw.to_s.strip_heredoc
|
68
|
+
code = process_file.gsub(/\n^.*?@(begin|end):.*?$/, "") if meta[:file]
|
69
|
+
|
70
|
+
code = Highlight.apply(code, language)
|
71
|
+
|
72
|
+
# escape for textile
|
73
|
+
code = %[<notextile>#{code}</notextile>] if format == :textile
|
74
|
+
code
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
# Process line range as in <tt>@@@ ruby some_file.rb:15,20 @@@</tt>.
|
79
|
+
#
|
80
|
+
def process_range(code)
|
81
|
+
starts, ends = meta[:reference].split(",").collect(&:to_i)
|
82
|
+
code = StringIO.new(code).readlines[starts-1..ends-1].join("\n").strip_heredoc.chomp
|
83
|
+
end
|
84
|
+
|
85
|
+
# Process block name as in <tt>@@@ ruby some_file.rb#some_block @@@</tt>.
|
86
|
+
#
|
87
|
+
def process_block(code)
|
88
|
+
code.gsub!(/\r\n/, "\n")
|
89
|
+
re = %r[@begin: *\b(#{meta[:reference]})\b *[^\n]*\n(.*?)\n[^\n]*@end: \1]im
|
90
|
+
|
91
|
+
if code.match(re)
|
92
|
+
$2.strip_heredoc
|
93
|
+
else
|
94
|
+
"[missing '#{meta[:reference]}' block name]"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Process file and its relatives.
|
99
|
+
#
|
100
|
+
def process_file
|
101
|
+
file_path = book_dir.join("code/#{meta[:file]}")
|
102
|
+
|
103
|
+
if File.exist?(file_path)
|
104
|
+
code = File.read(file_path)
|
105
|
+
|
106
|
+
if meta[:type] == :range
|
107
|
+
process_range(code)
|
108
|
+
elsif meta[:type] == :block
|
109
|
+
process_block(code)
|
110
|
+
else
|
111
|
+
code
|
112
|
+
end
|
113
|
+
else
|
114
|
+
"[missing 'code/#{meta[:file]}' file]"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Return the language used for this syntax block. Overrideable
|
119
|
+
# for epub generation.
|
120
|
+
def language
|
121
|
+
@language || meta[:language]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
module TOC
|
3
|
+
class Epub
|
4
|
+
attr_accessor :navigation
|
5
|
+
|
6
|
+
def initialize(navigation)
|
7
|
+
@navigation = navigation
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_html
|
11
|
+
ERB.new(template).result OpenStruct.new(:navigation => navigation).instance_eval{ binding }
|
12
|
+
end
|
13
|
+
|
14
|
+
def template
|
15
|
+
<<-HTML.strip_heredoc.force_encoding("utf-8")
|
16
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
17
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
18
|
+
<html xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
19
|
+
<head>
|
20
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
21
|
+
<link rel="stylesheet" type="text/css" href="epub.css"/>
|
22
|
+
<meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
|
23
|
+
<title>Table of Contents</title>
|
24
|
+
</head>
|
25
|
+
<body>
|
26
|
+
<div id="toc">
|
27
|
+
<ul>
|
28
|
+
<% navigation.each do |nav| %>
|
29
|
+
<li>
|
30
|
+
<a href="<%= nav[:content] %>"><%= nav[:label] %></a>
|
31
|
+
</li>
|
32
|
+
<% end %>
|
33
|
+
</ul>
|
34
|
+
</div>
|
35
|
+
</body>
|
36
|
+
</html>
|
37
|
+
HTML
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Bookshelf
|
2
|
+
module TOC
|
3
|
+
class HTML
|
4
|
+
# Return the table of contents in hash format.
|
5
|
+
#
|
6
|
+
attr_reader :toc
|
7
|
+
|
8
|
+
private_class_method :new
|
9
|
+
attr_reader :buffer # :nodoc:
|
10
|
+
attr_reader :attrs # :nodoc:
|
11
|
+
attr_accessor :content # :nodoc:
|
12
|
+
|
13
|
+
# Traverse every title and add a +id+ attribute.
|
14
|
+
# Return the modified content.
|
15
|
+
#
|
16
|
+
def self.normalize(content)
|
17
|
+
counter = {}
|
18
|
+
html = Nokogiri::HTML.parse(content)
|
19
|
+
html.search("h1, h2, h3, h4, h5, h6").each do |tag|
|
20
|
+
title = tag.inner_text
|
21
|
+
permalink = title.to_permalink
|
22
|
+
|
23
|
+
counter[permalink] ||= 0
|
24
|
+
counter[permalink] += 1
|
25
|
+
|
26
|
+
permalink = "#{permalink}-#{counter[permalink]}" if counter[permalink] > 1
|
27
|
+
|
28
|
+
tag.set_attribute("id", permalink)
|
29
|
+
end
|
30
|
+
|
31
|
+
html.css("body").to_xhtml.gsub(/<body>(.*?)<\/body>/m, "\\1")
|
32
|
+
end
|
33
|
+
|
34
|
+
# Traverse every title normalizing its content as a permalink.
|
35
|
+
#
|
36
|
+
def self.generate(content)
|
37
|
+
content = normalize(content)
|
38
|
+
listener = new
|
39
|
+
listener.content = content
|
40
|
+
Stream.new(content, listener).parse
|
41
|
+
listener
|
42
|
+
end
|
43
|
+
|
44
|
+
def initialize # :nodoc:
|
45
|
+
@toc = []
|
46
|
+
@counters = {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def tag(node) # :nodoc:
|
50
|
+
toc << {
|
51
|
+
:level => node.name.gsub(/[^\d]/, "").to_i,
|
52
|
+
:text => node.text,
|
53
|
+
:permalink => node["id"]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return a hash with all normalized attributes.
|
58
|
+
#
|
59
|
+
def to_hash
|
60
|
+
{
|
61
|
+
:content => content,
|
62
|
+
:html => to_html,
|
63
|
+
:toc => toc
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return the table of contents in HTML format.
|
68
|
+
#
|
69
|
+
def to_html
|
70
|
+
String.new.tap do |html|
|
71
|
+
toc.each do |options|
|
72
|
+
html << %[<div class="level#{options[:level]} #{options[:permalink]}"><a href="##{options[:permalink]}"><span>#{CGI.escape_html(options[:text])}</span></a></div>]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/templates/Guardfile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# The book's title. Will be used everywhere!
|
2
|
+
title: "[Your Book Title]"
|
3
|
+
|
4
|
+
# The book's language.
|
5
|
+
language: en
|
6
|
+
|
7
|
+
# Your book copyright info.
|
8
|
+
# Here's some examples:
|
9
|
+
#
|
10
|
+
# Copyright <%= @year %> by <%= @name %>.
|
11
|
+
# Copyright <%= @year %> by <%= @name %>. This work is licensed under MIT License.
|
12
|
+
# This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
|
13
|
+
# This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
|
14
|
+
# This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
|
15
|
+
#
|
16
|
+
copyright: "Copyright (C) <%= @year %> <%= @name %>."
|
17
|
+
|
18
|
+
# Who's publishing this book.
|
19
|
+
publisher: "<%= @name %>"
|
20
|
+
|
21
|
+
# When this book was published.
|
22
|
+
published_at: "<%= Date.today %>"
|
23
|
+
|
24
|
+
# Some book description.
|
25
|
+
subject: "[Your book description]"
|
26
|
+
|
27
|
+
# Some keywords that identify this book.
|
28
|
+
keywords: "[Your book keywords (comma-separated)]"
|
29
|
+
|
30
|
+
# Some unique identification. Works great with your domain
|
31
|
+
# like `http://yourbook.example.com`.
|
32
|
+
uid: "<%= @uid %>"
|
33
|
+
|
34
|
+
# Your book identification like ISBN or ISSN.
|
35
|
+
identifier:
|
36
|
+
id: "http://yourbook.example.com"
|
37
|
+
type: "URL" # can be ISBN, ISSN or URL
|
38
|
+
|
39
|
+
# This book authors.
|
40
|
+
authors:
|
41
|
+
- "<%= @name %>"
|
42
|
+
|
43
|
+
# The base URL from your source code.
|
44
|
+
base_url: http://example.com
|