bookit 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Gemfile +4 -0
- data/Rakefile +2 -0
- data/bookit.gemspec +30 -0
- data/examples/article.html +19 -0
- data/examples/article.md +68 -0
- data/examples/article2.html +77 -0
- data/examples/base-article.html +28 -0
- data/examples/base-output.txt +29 -0
- data/lib/bookit.rb +24 -0
- data/lib/bookit/article.rb +27 -0
- data/lib/bookit/content.rb +39 -0
- data/lib/bookit/content/generic.rb +29 -0
- data/lib/bookit/content/header.rb +19 -0
- data/lib/bookit/content/image.rb +22 -0
- data/lib/bookit/content/link.rb +21 -0
- data/lib/bookit/content/list.rb +20 -0
- data/lib/bookit/content/paragraph.rb +22 -0
- data/lib/bookit/content/text.rb +19 -0
- data/lib/bookit/emitter.rb +4 -0
- data/lib/bookit/emitter/abstract.rb +35 -0
- data/lib/bookit/emitter/base.rb +37 -0
- data/lib/bookit/emitter/pdf.rb +71 -0
- data/lib/bookit/parser.rb +4 -0
- data/lib/bookit/parser/html.rb +47 -0
- data/lib/bookit/persistable_object.rb +77 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/bookit.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = "bookit"
|
4
|
+
s.version = "0.0.1"
|
5
|
+
s.platform = Gem::Platform::RUBY
|
6
|
+
|
7
|
+
s.authors = ["Luke van der Hoeven"]
|
8
|
+
s.email = ["hungerandthirst@gmail.com"]
|
9
|
+
|
10
|
+
s.homepage = "http://github.com/plukevdh/bookit"
|
11
|
+
s.summary = %q{BOOKS WHAT}
|
12
|
+
s.description = %q{A quick way to format and output generic book information or articles in multiple formats. Mainly: PDF, EPUB and MOBI}
|
13
|
+
|
14
|
+
s.rubyforge_project = "bookit"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# output
|
22
|
+
s.add_dependency("prawn")
|
23
|
+
s.add_dependency("eeepub")
|
24
|
+
|
25
|
+
# parsing
|
26
|
+
s.add_dependency("nokogiri")
|
27
|
+
|
28
|
+
# storage
|
29
|
+
s.add_dependency("redis")
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<div><div class="entry grid_8 prefix_2">
|
2
|
+
<p>I’m not sure how old I was – seven or eight, maybe – when my Grandmother caught my cousin and I prying the wood paneling off the wall in one of her house’s bedrooms using the plastic ketchup and mustard bottles from a grocery store playset we had. I can count on one hand the number of times I remember her actually becoming visibly angry with any of her grandkids, and that was one of them.</p>
|
3
|
+
<p>But we hadn’t intended to be destructive. We were searching for a secret passage. The house was pretty old – my Mom had grown up in it – and so, according to the logic of kids’ fantasy and detective media, it had secret passages and we were going to find them, little ears to the walls, palming and knocking softly like I must have seen someone do in a movie. My cousin was too young to do much other than completely believe my logic, that the slightly loose piece of paneling we discovered in that particular bedroom had to conceal a door.</p>
|
4
|
+
<p>I’m not entirely sure what I hoped it’d be a door <em>to</em>. Crawlspaces to enable professional espionage against our family? Dark, quiet passages to underworld chasms, mysterious cities, foreign tombs laden with treasure or haunted by royal ghosts? Long-concealed empire drawing rooms somehow forgotten as part of the infrastructure of the house? It didn’t really matter, as any of these outcomes were implied in ‘secret passage.’ We’d figure it out when we found it.</p>
|
5
|
+
<p>I was always doing things like that. Crawling, fingers outstretched, eyes shut, for the backs of closets, feeling deterred – but never defeated – should my fingertips brush cool plaster and not the foreign foliage of another realm. Searching inside of flowers for miniature cities, listening to trees in case one of them was a prince from another world, transformed by a curse. Whispering at the moon in the hopes of stumbling upon an incantation – all of it, a continuous (often inspiring, often lonesome) search for ways to go <em>elsewhere</em>.</p>
|
6
|
+
<p>Entertainment media aimed at children has always promised that escape. Fairy tales illuminate a world that exists beyond what the eye can see, a realm of magic available to one special person with the right sort of heart. Unicorns, the legends go, only appear for the pure. The hero chosen to save the kingdom is always one unlikely, often luck and love-starved child. Even modern blockbuster movies about kids getting lost in the big city, of babysitting nights gone awry, about young detectives, have helped contribute to an entire culture aimed at teaching kids to dream of something better than ‘all there is,’ of powers undiscovered, a way out of the mundane.</p>
|
7
|
+
<p>Imagination is generally the realm of children. Young people have not yet learned the world is boring and hard, and so learning to create those psychic buffer zones, how to be their own companion, how to seek escape, is a skill foundation that must be laid early.</p>
|
8
|
+
<p>Even the grown-ups who, as the stereotype goes, tell kids to get their heads out of the clouds, to stop talking to people that aren’t there, are, intentionally or otherwise, strengthening the dreamer’s urge that will help them be great creators and great innovators as adults, in the face of a society that loves to quash the tall poppy with prescribed social norms, to fling up bureaucratic roadblocks, to declare: ‘Impossible.’</p>
|
9
|
+
<p>A resilient spine of imagination is what drives human beings to strive for impossible things – like dream of space travel, of cures for incurable disease, of world peace. Technology, interaction and even the development  of architecture and vehicle design marches onward in no small part because imaginative kids grow up into ambitious adults who really, really want to actualize the shiny adventures of the science fiction books they love – flying cars, sentient robots, space needles and all.</p>
|
10
|
+
<p>Because ‘wanting out’ is a quintessential part of the adult life. All people need to believe there is a way to escape normalcy and the status quo. If we all believed there was nothing to do but accept reality, life would be intolerable. We could not progress.</p>
|
11
|
+
<p>Most important about the things we dreamed as children is that there was never any real danger. A secret passage walled into an old house is far more likely to contain a corpse than a luminous parallel universe, but I never thought about that. Even if as a kid you <em>did</em> manage to build a functioning time machine so that you could visit the age of the dinosaurs, you never stopped to think that you might become the meal of some vicious primordial monster.</p>
|
12
|
+
<p>We sought portals to new lands without worrying that we might end up their permanent prisoner. Any potential threat – black knights, malfunctioning robots, dragons and ghosts – only served to promise us the chance to be heroic. Dreamer-children learned to consider possibilities with endless wellsprings of excitement and ambition, not fear.</p>
|
13
|
+
<p>The worst in humanity arises from the chasm between the real world and our dreams. We are liable to become inert, we become avoidant, we become addicts, or we self-sabotage because of the cruelty of the real, and the crushing lesson that there is only a certain extent to which we will ever subvert it.</p>
|
14
|
+
<p>But survival and great success are born from the refusal to accept boundaries, though it may be strange to conceive of our greatest creative endeavors as direct responses to the fact that life sucks. Or to think that the most impressive thing any individual will ever accomplish is born out of the friction between what we want to be and what we are.  That’s what we were learning when playing imagination games as children – how to want a way out, always, from the world we were given.</p>
|
15
|
+
<p>And the worse that world was, the more we wanted that escape. That’s why the greatest strides in this world are often made by the people who’ve suffered most – and why you never meet anyone interesting who says they had a great time in high school. <span class="tc_mark"><img src="http://thoughtcatalog.com/wp-content/themes/thought_catalog/images/tc_mark.gif" alt="TC mark"></span></p>
|
16
|
+
<h3>You should follow Thought Catalog on Twitter <a href="http://www.twitter.com/thoughtcatalog" rel="nofollow">here</a>.</h3>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
|
data/examples/article.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
Ruby + Upskirt = Markdown that doesn't suck
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
Redcarpet is a Ruby wrapper for Upskirt. It is mostly based on Ryan
|
5
|
+
Tomayko's RDiscount wrapper, and inspired by Rick Astley wearing a kilt.
|
6
|
+
|
7
|
+
Redcarpet is powered by the Upskirt library, which can be found at
|
8
|
+
|
9
|
+
https://www.github.com/tanoku/upskirt
|
10
|
+
|
11
|
+
You might want to find out more about Upskirt to see what makes these Ruby
|
12
|
+
bindings so awesome.
|
13
|
+
|
14
|
+
Credits
|
15
|
+
-------
|
16
|
+
|
17
|
+
* Natacha Porté, lady of Markdown
|
18
|
+
* Vicent Martí, wannabe
|
19
|
+
* With special thanks to Ryan Tomayko
|
20
|
+
|
21
|
+
Install
|
22
|
+
-------
|
23
|
+
|
24
|
+
Redcarpet is readily available as a Ruby gem:
|
25
|
+
|
26
|
+
$ [sudo] gem install redcarpet
|
27
|
+
|
28
|
+
The Redcarpet source (including Upskirt as a submodule) is available at GitHub:
|
29
|
+
|
30
|
+
$ git clone git://github.com/tanoku/redcarpet.git
|
31
|
+
|
32
|
+
Usage
|
33
|
+
-----
|
34
|
+
|
35
|
+
Redcarpet implements the basic protocol popularized by RedCloth:
|
36
|
+
|
37
|
+
require 'redcarpet'
|
38
|
+
markdown = Redcarpet.new("Hello World!")
|
39
|
+
puts markdown.to_html
|
40
|
+
|
41
|
+
Additional processing options can be turned on when creating the
|
42
|
+
Redcarpet object:
|
43
|
+
|
44
|
+
markdown = Redcarpet.new("Hello World!", :smart, :filter_html)
|
45
|
+
|
46
|
+
Note that by default, Redcarpet parses standard Markdown (with no extensions)
|
47
|
+
and offers a sane subset of parse options which allow you to modify the rendering
|
48
|
+
output and to enable MD extensions on a per-case basis.
|
49
|
+
|
50
|
+
Redcarpet also offers a wrapper class, `RedcarpetCompat` with the same flags
|
51
|
+
and behavior as the RDiscount library, which acts as a drop-in replacement.
|
52
|
+
|
53
|
+
License
|
54
|
+
-------
|
55
|
+
|
56
|
+
Permission to use, copy, modify, and distribute this software for any
|
57
|
+
purpose with or without fee is hereby granted, provided that the above
|
58
|
+
copyright notice and this permission notice appear in all copies.
|
59
|
+
|
60
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
61
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
62
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
63
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
64
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
65
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
66
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
67
|
+
|
68
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
<div><div class="entry-content">
|
2
|
+
<p>I was a Java guy for 10 years and I’ve been a Rubyist for the last 5 years. Over the years, I’ve tried to develop expertise in a particular area of technology that will both pay the bills and make me happy as a programmer while also watching for upcoming changes in the tech world. I often find myself diving into a particular technology just to get my hands dirty and get a feel for its strengths and weaknesses. As my JavaScript skills have always been weak, I’ve decided to deep dive into <a href="http://nodejs.org" rel="nofollow">Node.js</a> to understand what it does well and improve my JavaScript skills at the same time.</p>
|
3
|
+
<p>For this post, I’m just going to cover the basics; I’ll follow up soon with deeper posts.<br></p>
|
4
|
+
<h2>Overview</h2>
|
5
|
+
<p>JavaScript has an interesting history – it hasn’t developed like most other languages; until recently, executing JavaScript meant embedding it in a web page for a browser to execute. A few things happened which radically hastened the rise in JavaScript as an reasonable server-side language:</p>
|
6
|
+
<ul><li>AJAX and the Browser Wars have resulted in dramatic improvements in Javascript runtime performance and high-quality developer tools.</li>
|
7
|
+
<li>Node.js built Process, File and Network I/O APIs on top of Google’s <a href="http://code.google.com/p/v8/" rel="nofollow">V8 JavaScript engine</a>, allowing command line programs and daemons to be built in JavaScript.</li>
|
8
|
+
</ul><p>Node.js adds a friendly command line face to V8 and APIs that are conceptually similar to Ruby’s EventMachine library: all I/O is asynchronous and threads are unavailable to user code. Additionally JavaScript is a prototype-based language, not object-oriented. This makes for a programming model that is radically different from what Ruby or Java developers are used to.</p>
|
9
|
+
<h2>Installation</h2>
|
10
|
+
<p>I’m going to assume OSX and I like to install things with <a href="http://mxcl.github.com/homebrew/" rel="nofollow">Homebrew</a>. We’ll install node and npm, node’s package manager, with these commands:</p>
|
11
|
+
<pre class="brush: bash;">
|
12
|
+
brew update # update Homebrew's formulas to the latest
|
13
|
+
brew install node # install node
|
14
|
+
curl http://npmjs.org/install.sh | sudo sh # install npm
|
15
|
+
</pre>
|
16
|
+
<p>Once installed, you should be able to run <code>node --help</code> and <code>npm --help</code>.</p>
|
17
|
+
<p>A minimal web server using Node.js:</p>
|
18
|
+
<pre class="brush: jscript;">
|
19
|
+
var http = require('http');
|
20
|
+
http.createServer(function (req, res) {
|
21
|
+
res.writeHead(200, {'Content-Type': 'text/plain'});
|
22
|
+
res.end('Hello World\n');
|
23
|
+
}).listen(8124, "127.0.0.1");
|
24
|
+
</pre>
|
25
|
+
<p>Copy that code into <code>hello.js</code> and run it:</p>
|
26
|
+
<pre>
|
27
|
+
node hello.js
|
28
|
+
</pre>
|
29
|
+
<p>Now let’s slam it with some requests:</p>
|
30
|
+
<pre>
|
31
|
+
ab -n 10000 -c 50 http://127.0.0.1:8124/
|
32
|
+
</pre>
|
33
|
+
<p>Results:</p>
|
34
|
+
<pre>
|
35
|
+
Server Hostname: 127.0.0.1
|
36
|
+
Server Port: 8124
|
37
|
+
Document Path: /
|
38
|
+
Document Length: 12 bytes
|
39
|
+
Concurrency Level: 50
|
40
|
+
Time taken for tests: 1.479 seconds
|
41
|
+
Complete requests: 10000
|
42
|
+
Failed requests: 0
|
43
|
+
Write errors: 0
|
44
|
+
Total transferred: 760000 bytes
|
45
|
+
HTML transferred: 120000 bytes
|
46
|
+
Requests per second: 6760.79 [#/sec] (mean)
|
47
|
+
Time per request: 7.396 [ms] (mean)
|
48
|
+
Time per request: 0.148 [ms] (mean, across all concurrent requests)
|
49
|
+
Transfer rate: 501.78 [Kbytes/sec] received
|
50
|
+
|
51
|
+
Connection Times (ms)
|
52
|
+
min mean[+/-sd] median max
|
53
|
+
Connect: 0 0 0.2 0 3
|
54
|
+
Processing: 1 7 3.6 7 20
|
55
|
+
Waiting: 1 7 3.6 7 20
|
56
|
+
Total: 1 7 3.6 7 22
|
57
|
+
|
58
|
+
Percentage of the requests served within a certain time (ms)
|
59
|
+
50% 7
|
60
|
+
66% 9
|
61
|
+
75% 10
|
62
|
+
80% 11
|
63
|
+
90% 12
|
64
|
+
95% 13
|
65
|
+
98% 15
|
66
|
+
99% 16
|
67
|
+
100% 22 (longest request)
|
68
|
+
</pre>
|
69
|
+
<p>Not bad. Of course, this is using localhost and a trivial app but at least we know it’s up and running well. In my next post, we’ll explore the Node.js source code itself.
|
70
|
+
</p><div class="snap_nopreview sharing robots-nocontent">
|
71
|
+
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
|
75
|
+
|
76
|
+
</div>
|
77
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<div>
|
2
|
+
<div>
|
3
|
+
<h1>TEST DOC</h2>
|
4
|
+
<p>
|
5
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed diam eget risus varius blandit sit amet non magna. Fusce dapibus, tellus ac cursus commodo, tortor mauris <a href="http://test.link.com">condimentum nibh</a>, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue. Maecenas faucibus mollis interdum. Maecenas sed diam eget risus varius blandit sit amet non magna. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
|
6
|
+
</p>
|
7
|
+
|
8
|
+
|
9
|
+
<a href="http://text.com">Nullam quis</a>
|
10
|
+
<p>
|
11
|
+
Nullam quis risus eget urna mollis ornare vel eu leo. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vestibulum id ligula porta felis euismod semper. Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna.
|
12
|
+
|
13
|
+
<img src="http://snarkerati.com/movie-news/files/2011/01/russell_brand.jpg" width="120" >
|
14
|
+
</p>
|
15
|
+
|
16
|
+
<ul>
|
17
|
+
<li>One</li>
|
18
|
+
<li>
|
19
|
+
<a href="http://text.com">Two</a>
|
20
|
+
</li>
|
21
|
+
<li>Three</li>
|
22
|
+
</ul>
|
23
|
+
|
24
|
+
<p>
|
25
|
+
Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Nulla vitae elit libero, a pharetra augue. Cras mattis consectetur purus sit amet fermentum. Maecenas sed diam eget risus varius blandit sit amet non magna. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.
|
26
|
+
</p>
|
27
|
+
</div>
|
28
|
+
</div>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
HEADER "TEST DOC"
|
2
|
+
PARAGRAPH
|
3
|
+
TEXT: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas sed diam eget risus varius blandit sit amet non magna. Fusce dapibus, tellus ac cursus commodo, tortor mauris
|
4
|
+
LINK:
|
5
|
+
TEXT: "http://test.link.com":"condimentum nibh",
|
6
|
+
TEXT: ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue. Maecenas faucibus mollis interdum. Maecenas sed diam eget risus varius blandit sit amet non magna. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.
|
7
|
+
|
8
|
+
LINK:
|
9
|
+
TEXT: "http://text.com":"Nullam quis"
|
10
|
+
PARAGRAPH:
|
11
|
+
TEXT: Nullam quis risus eget urna mollis ornare vel eu leo. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vestibulum id ligula porta felis euismod semper. Donec id elit non mi porta gravida at eget metus. Maecenas sed diam eget risus varius blandit sit amet non magna.
|
12
|
+
|
13
|
+
IMAGE: "http://snarkerati.com/movie-news/files/2011/01/russell_brand.jpg"
|
14
|
+
|
15
|
+
LIST:
|
16
|
+
TEXT: "One"
|
17
|
+
LINK:
|
18
|
+
TEXT: "http://text.com":"Two"
|
19
|
+
TEXT: "Three"
|
20
|
+
|
21
|
+
PARAGRAPH:
|
22
|
+
TEXT: Maecenas sed diam eget risus varius blandit sit amet non magna. Maecenas faucibus mollis interdum. Nulla vitae elit libero, a pharetra augue. Cras mattis consectetur purus sit amet fermentum. Maecenas sed diam eget risus varius blandit sit amet non magna. Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.
|
23
|
+
|
24
|
+
#<Bookit::Content::Header:0x000001013c2c18>
|
25
|
+
#<Bookit::Content::Paragraph:0x000001013ac8f0>
|
26
|
+
#<Bookit::Content::Link:0x000001013abab8>
|
27
|
+
#<Bookit::Content::Paragraph:0x000001013ab2c0>
|
28
|
+
#<Bookit::Content::List:0x0000010139ce00>
|
29
|
+
#<Bookit::Content::Paragraph:0x0000010139c310>
|
data/lib/bookit.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Bookit
|
2
|
+
require 'prawn'
|
3
|
+
require 'eeepub'
|
4
|
+
require 'redis'
|
5
|
+
|
6
|
+
autoload :PersistableObject, './bookit/persistable_object'
|
7
|
+
autoload :Article, './bookit/article'
|
8
|
+
autoload :Parser, './bookit/parser'
|
9
|
+
autoload :Content, './bookit/content'
|
10
|
+
|
11
|
+
class Content
|
12
|
+
autoload :Generic, './bookit/content/generic'
|
13
|
+
end
|
14
|
+
|
15
|
+
class Parser
|
16
|
+
autoload :Html, './bookit/parser/html'
|
17
|
+
end
|
18
|
+
|
19
|
+
class Emitter
|
20
|
+
autoload :Abstract, './bookit/emitter/abstract'
|
21
|
+
autoload :Pdf, './bookit/emitter/pdf'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require './bookit/parser/html'
|
2
|
+
require './bookit/emitter/pdf'
|
3
|
+
|
4
|
+
module Bookit
|
5
|
+
class Article
|
6
|
+
REQUIRED_ATTRS = [:author, :date_published, :url, :title, :content]
|
7
|
+
attr_accessor *REQUIRED_ATTRS, :attributes
|
8
|
+
|
9
|
+
def initialize(attrs, parser=Bookit::Parser::Html)
|
10
|
+
@attributes = attrs.dup
|
11
|
+
|
12
|
+
REQUIRED_ATTRS.each do |key|
|
13
|
+
raise "Required attribute #{key} not found." unless @attributes[key]
|
14
|
+
self.send "#{key}=", @attributes[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
# parse raw content into array of generic content
|
18
|
+
@content = Content.new(@content, parser)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Article's to print method outputs the given input content into the given
|
22
|
+
# emitter document type. We output PDF by default.
|
23
|
+
def to_print(emitter=Bookit::Emitter::Pdf)
|
24
|
+
emitter.new(self).generate
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# the Content class is an AST of sorts. it holds the generic structure
|
2
|
+
# of the article's content so that we can easily parse out important content
|
3
|
+
# attributes and then later output into other formats.
|
4
|
+
|
5
|
+
module Bookit
|
6
|
+
class Content
|
7
|
+
attr_reader :raw_content
|
8
|
+
attr_accessor :formatted_content
|
9
|
+
|
10
|
+
autoload :Generic, './bookit/content/generic'
|
11
|
+
autoload :Paragraph, './bookit/content/paragraph'
|
12
|
+
autoload :Text, './bookit/content/text'
|
13
|
+
autoload :Link, './bookit/content/link'
|
14
|
+
autoload :Image, './bookit/content/image'
|
15
|
+
autoload :Header, './bookit/content/header'
|
16
|
+
autoload :List, './bookit/content/list'
|
17
|
+
|
18
|
+
TYPES = {
|
19
|
+
paragraph: Content::Paragraph,
|
20
|
+
image: Content::Image,
|
21
|
+
header: Content::Header,
|
22
|
+
link: Content::Link,
|
23
|
+
list: Content::List
|
24
|
+
}
|
25
|
+
|
26
|
+
def initialize(raw_content, parser)
|
27
|
+
@raw_content = raw_content
|
28
|
+
@formatted_content = parser.new.parse(raw_content)
|
29
|
+
end
|
30
|
+
|
31
|
+
# #render for any Content object should always return a string
|
32
|
+
# it is also possible to pass a block to render actions so that
|
33
|
+
# you can fine-tune the content output from your Emitters
|
34
|
+
def render
|
35
|
+
@formatted_content.each &:render
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Content::Generic
|
3
|
+
attr_accessor :attributes
|
4
|
+
|
5
|
+
# Content can always take options. These can usually be used
|
6
|
+
# to add additional formatting information for the Emitters
|
7
|
+
def initialize(options={})
|
8
|
+
@attributes = options
|
9
|
+
end
|
10
|
+
|
11
|
+
# #render for any Content object should always return a string
|
12
|
+
# it is also possible to pass a block to render actions so that
|
13
|
+
# you can fine-tune the content output from your Emitters
|
14
|
+
def render
|
15
|
+
inspect
|
16
|
+
end
|
17
|
+
|
18
|
+
# gives us an easy to compare type symbol
|
19
|
+
def type
|
20
|
+
self.class.to_s.split('::').last.downcase.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
# returns true if this symbol is tied to the class type
|
24
|
+
def is_type? sym
|
25
|
+
self.class == Content::TYPES[sym]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Content
|
3
|
+
class Header < Generic
|
4
|
+
attr_accessor :text
|
5
|
+
|
6
|
+
# headers will only ever contain text, but usually contained separately
|
7
|
+
# for formatting purposes
|
8
|
+
def initialize(text, options={})
|
9
|
+
@text = text
|
10
|
+
|
11
|
+
super options
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
text
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
|
3
|
+
module Bookit
|
4
|
+
class Content
|
5
|
+
class Image < Generic
|
6
|
+
attr_accessor :source
|
7
|
+
|
8
|
+
# takes a source url
|
9
|
+
def initialize(source, options={})
|
10
|
+
@source = source
|
11
|
+
|
12
|
+
super options
|
13
|
+
end
|
14
|
+
|
15
|
+
# returns the url to the image
|
16
|
+
def render
|
17
|
+
[open(source), attributes]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Content
|
3
|
+
|
4
|
+
# Content that will be clickable to web urls. Can be images or text.
|
5
|
+
class Link < Generic
|
6
|
+
attr_accessor :url, :text
|
7
|
+
|
8
|
+
def initialize(url, objects, options={})
|
9
|
+
@url = url
|
10
|
+
@objects = (objects.class == Array) ? objects : [objects]
|
11
|
+
|
12
|
+
super options
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
[@url, @objects.map(&:render)]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Content
|
3
|
+
class List < Generic
|
4
|
+
attr_accessor :items
|
5
|
+
|
6
|
+
# a List could contain text or links.
|
7
|
+
def initialize(items, options={})
|
8
|
+
items.compact!
|
9
|
+
@items = (items.class == Array) ? items : [items]
|
10
|
+
|
11
|
+
super options
|
12
|
+
end
|
13
|
+
|
14
|
+
def render
|
15
|
+
@items
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Content
|
3
|
+
class Paragraph < Generic
|
4
|
+
attr_accessor :contents
|
5
|
+
|
6
|
+
# Paragraph can contain most other object types. Text, images or links.
|
7
|
+
def initialize(contents, options={})
|
8
|
+
# this could be an array of images, text, links or just a single element
|
9
|
+
contents.compact!
|
10
|
+
@contents = (contents.class == Array) ? contents : [contents]
|
11
|
+
|
12
|
+
super options
|
13
|
+
end
|
14
|
+
|
15
|
+
def render
|
16
|
+
@contents
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Emitter
|
3
|
+
class Abstract
|
4
|
+
attr_accessor :article, :options
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
font_size: 12,
|
8
|
+
|
9
|
+
header_size: 20,
|
10
|
+
header_style: :bold,
|
11
|
+
|
12
|
+
subtitle_size: 10,
|
13
|
+
subtitle_style: :italic
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(article, options={})
|
17
|
+
@article = article
|
18
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
19
|
+
|
20
|
+
# have all our type renderers raise an error unless defined in the subclass.
|
21
|
+
Bookit::Content::TYPES.each do |type|
|
22
|
+
self.class.send(:define_method, "render_#{type}".to_sym, -> { raise_abstract })
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def raise_abstract
|
29
|
+
raise "Abstract class, please defined this to render for your filetype."
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Bookit
|
2
|
+
class Emitter
|
3
|
+
class Abstract
|
4
|
+
attr_accessor :article, :options
|
5
|
+
|
6
|
+
DEFAULT_OPTIONS = {
|
7
|
+
font_size: 12,
|
8
|
+
|
9
|
+
header_size: 20,
|
10
|
+
header_style: :bold,
|
11
|
+
|
12
|
+
subtitle_size: 10,
|
13
|
+
subtitle_style: :italic
|
14
|
+
}
|
15
|
+
|
16
|
+
def initialize(article, options={})
|
17
|
+
@article = article
|
18
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
19
|
+
|
20
|
+
# have all our type renderers raise an error unless defined in the subclass.
|
21
|
+
Bookit::Content::TYPES.each do |type|
|
22
|
+
define_method "render_#{type}".to_sym do
|
23
|
+
raise_abstract
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
def raise_abstract
|
31
|
+
raise "Abstract class, please defined this to render for your filetype."
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'prawn'
|
2
|
+
require 'prawn/measurement_extensions'
|
3
|
+
|
4
|
+
module Bookit
|
5
|
+
class Emitter
|
6
|
+
class Pdf < Abstract
|
7
|
+
def generate
|
8
|
+
@pdf = Prawn::Document.new(info: {
|
9
|
+
:Title => @article.title,
|
10
|
+
:CreationDate => @article.date_published,
|
11
|
+
:Author => @article.author,
|
12
|
+
:Source => @article.url },
|
13
|
+
margin: 0.5.in, page_size: 'LEGAL')
|
14
|
+
|
15
|
+
@pdf.font_size = options[:font_size]
|
16
|
+
|
17
|
+
@pdf.text @article.title, size: options[:header_size], style: options[:header_style]
|
18
|
+
@pdf.text @article.author, style: :bold
|
19
|
+
|
20
|
+
@pdf.text @article.date_published, size: options[:subtitle_size], style: options[:subtitle_style]
|
21
|
+
@pdf.text @article.url, size: options[:subtitle_size], style: options[:subtitle_style]
|
22
|
+
|
23
|
+
@pdf.text "\n\n"
|
24
|
+
|
25
|
+
@article.content.formatted_content.each do |item|
|
26
|
+
output(render(item))
|
27
|
+
end
|
28
|
+
|
29
|
+
@pdf
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def render(item)
|
34
|
+
send "render_#{item.type}", item.render
|
35
|
+
end
|
36
|
+
|
37
|
+
def output(group)
|
38
|
+
group[2] = {inline_format: true}
|
39
|
+
@pdf.send *group.flatten
|
40
|
+
@pdf.text("\n")
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_paragraph(items)
|
44
|
+
["text", items.map {|i| render(i)[1..-1]}.join("")]
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_text(text)
|
48
|
+
["text", text]
|
49
|
+
end
|
50
|
+
|
51
|
+
# render images on finding them. not embeddable within a paragraph for now.
|
52
|
+
def render_image(img)
|
53
|
+
@pdf.image *img
|
54
|
+
["text"]
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_header(text)
|
58
|
+
["text", text, size: options[:header_size]]
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_list(items)
|
62
|
+
["text", items.map {|i| "- #{render(i)[1..-1].join(" ")}"}.join("\n")]
|
63
|
+
end
|
64
|
+
|
65
|
+
def render_link(link)
|
66
|
+
["text", "<color rgb='#0000ff'><u><link href='#{link[0]}'>#{link[1].join("")}</link></u></color>"]
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
module Bookit
|
4
|
+
class Parser
|
5
|
+
class Html
|
6
|
+
def parse(content)
|
7
|
+
elements = []
|
8
|
+
|
9
|
+
doc = Nokogiri::HTML(content)
|
10
|
+
elements = walk(doc.root, [])
|
11
|
+
|
12
|
+
elements.compact
|
13
|
+
end
|
14
|
+
|
15
|
+
def walk(element, tree)
|
16
|
+
return tree if element.nil? || (element.content.strip.empty? if element.name != "img")
|
17
|
+
|
18
|
+
tree << case element.name
|
19
|
+
when "p"
|
20
|
+
Bookit::Content::Paragraph.new(walk_children(element, []))
|
21
|
+
when "text"
|
22
|
+
Bookit::Content::Text.new(element.content.strip)
|
23
|
+
when "h1", "h2", "h3", "h4"
|
24
|
+
Bookit::Content::Header.new(element.content.strip)
|
25
|
+
when "a"
|
26
|
+
Bookit::Content::Link.new(element.attributes["href"].value, walk_children(element, []))
|
27
|
+
when "img"
|
28
|
+
attrs = {}
|
29
|
+
['width', 'height'].each {|a| attrs[a.to_sym] = element.attributes[a] ? element.attributes[a].value.to_i : nil}
|
30
|
+
|
31
|
+
Bookit::Content::Image.new(element.attributes["src"].value, attrs)
|
32
|
+
when "ul", "ol"
|
33
|
+
Bookit::Content::List.new(walk_children(element, []))
|
34
|
+
else
|
35
|
+
walk_children(element, tree)
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
return tree
|
40
|
+
end
|
41
|
+
|
42
|
+
def walk_children(element, tree)
|
43
|
+
element.children.inject(tree) {|past, child| walk(child, past)}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# super-meta object persistence class to Redis
|
2
|
+
require 'redis'
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
module Bookit
|
6
|
+
class PersistableObject
|
7
|
+
include ActiveModel::Serialization
|
8
|
+
|
9
|
+
attr_accessor :attributes, :id
|
10
|
+
cattr_writer :namespace
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def namespace
|
14
|
+
@namespace || self
|
15
|
+
end
|
16
|
+
|
17
|
+
def key(id)
|
18
|
+
"#{namespace}:%s" % id
|
19
|
+
end
|
20
|
+
|
21
|
+
# specifically use the generic initializer to rebuild object
|
22
|
+
def find(id)
|
23
|
+
return nil unless redis.exists key(id)
|
24
|
+
|
25
|
+
new redis.hgetall(key(id)).to_options
|
26
|
+
end
|
27
|
+
|
28
|
+
def last_id
|
29
|
+
redis.get key("LAST_ID")
|
30
|
+
end
|
31
|
+
|
32
|
+
def last
|
33
|
+
find last_id
|
34
|
+
end
|
35
|
+
|
36
|
+
def redis(host="localhost", port=6379)
|
37
|
+
@redis ||= Redis.new(host: host, port: port)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(params = {})
|
42
|
+
@attributes = params
|
43
|
+
@id = params[:id] || nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def redis
|
47
|
+
self.class.redis
|
48
|
+
end
|
49
|
+
|
50
|
+
def read_attribute_for_validation(key)
|
51
|
+
@attributes[key]
|
52
|
+
end
|
53
|
+
alias :[] :read_attribute_for_validation
|
54
|
+
|
55
|
+
def key(id=@id)
|
56
|
+
self.class.key(id)
|
57
|
+
end
|
58
|
+
|
59
|
+
# store in redis
|
60
|
+
def persist
|
61
|
+
@attributes[:id] = @id = incr_key
|
62
|
+
|
63
|
+
redis.hmset key, *serializable_hash.flatten
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def clear
|
68
|
+
redis.del key
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
def incr_key
|
73
|
+
redis.incr key("LAST_ID")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bookit
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Luke van der Hoeven
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-05-03 00:00:00 -04:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: prawn
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: eeepub
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "0"
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: nokogiri
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: redis
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :runtime
|
59
|
+
version_requirements: *id004
|
60
|
+
description: "A quick way to format and output generic book information or articles in multiple formats. Mainly: PDF, EPUB and MOBI"
|
61
|
+
email:
|
62
|
+
- hungerandthirst@gmail.com
|
63
|
+
executables: []
|
64
|
+
|
65
|
+
extensions: []
|
66
|
+
|
67
|
+
extra_rdoc_files: []
|
68
|
+
|
69
|
+
files:
|
70
|
+
- .gitignore
|
71
|
+
- Gemfile
|
72
|
+
- Rakefile
|
73
|
+
- bookit.gemspec
|
74
|
+
- examples/article.html
|
75
|
+
- examples/article.md
|
76
|
+
- examples/article2.html
|
77
|
+
- examples/base-article.html
|
78
|
+
- examples/base-output.txt
|
79
|
+
- lib/bookit.rb
|
80
|
+
- lib/bookit/article.rb
|
81
|
+
- lib/bookit/content.rb
|
82
|
+
- lib/bookit/content/generic.rb
|
83
|
+
- lib/bookit/content/header.rb
|
84
|
+
- lib/bookit/content/image.rb
|
85
|
+
- lib/bookit/content/link.rb
|
86
|
+
- lib/bookit/content/list.rb
|
87
|
+
- lib/bookit/content/paragraph.rb
|
88
|
+
- lib/bookit/content/text.rb
|
89
|
+
- lib/bookit/emitter.rb
|
90
|
+
- lib/bookit/emitter/abstract.rb
|
91
|
+
- lib/bookit/emitter/base.rb
|
92
|
+
- lib/bookit/emitter/pdf.rb
|
93
|
+
- lib/bookit/parser.rb
|
94
|
+
- lib/bookit/parser/html.rb
|
95
|
+
- lib/bookit/persistable_object.rb
|
96
|
+
- lib/test.pdf
|
97
|
+
has_rdoc: true
|
98
|
+
homepage: http://github.com/plukevdh/bookit
|
99
|
+
licenses: []
|
100
|
+
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: "0"
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: "0"
|
118
|
+
requirements: []
|
119
|
+
|
120
|
+
rubyforge_project: bookit
|
121
|
+
rubygems_version: 1.6.2
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: BOOKS WHAT
|
125
|
+
test_files: []
|
126
|
+
|