This commit is contained in:
Adam Bouqdib 2016-05-30 11:41:16 +00:00
parent fc63988c8d
commit b135372c30
14 changed files with 516 additions and 38 deletions

38
.gitignore vendored
View File

@ -1,36 +1,4 @@
/Gemfile.lock
/.bundle
/vendor
*.gem *.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/
## Specific to RubyMotion:
.dat*
.repl_history
build/
## Documentation cache and generated files:
/.yardoc/
/_yardoc/
/doc/
/rdoc/
## Environment normalization:
/.bundle/
/vendor/bundle
/lib/bundler/man/
# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# Gemfile.lock
# .ruby-version
# .ruby-gemset
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc

33
.travis.yml Normal file
View File

@ -0,0 +1,33 @@
language: ruby
cache: bundler
sudo: false
#
rvm:
- &ruby1 2.3.0
- &ruby2 2.2.4
- &ruby3 2.1.8
- &rhead ruby-head
#
matrix:
fast_finish: true
allow_failures:
- rvm: *rhead
#
notifications:
email:
recipients:
- adam@abemedia.co.uk
#
branches:
only:
- master
#

8
Gemfile Normal file
View File

@ -0,0 +1,8 @@
source "https://rubygems.org"
gemspec
group :test do
gem "rake"
end
gem "jekyll-assets", "~> 2.2", :require => false

107
README.md
View File

@ -1,2 +1,105 @@
# jekyll-pdf # Jekyll PDF
Create PDFs from Jekyll pages &amp; documents.
Dynamically generate PDFs from Jekyll pages, posts &amp; documents.
[![Build Status](https://travis-ci.org/abeMedia/jekyll-pdf.svg?branch=master)](https://travis-ci.org/abeMedia/jekyll-pdf)
[![Dependency Status](https://gemnasium.com/badges/github.com/abeMedia/jekyll-pdf.svg)](https://gemnasium.com/github.com/abeMedia/jekyll-pdf)
## Usage
Add `gem "jekyll-pdf"` to your `Gemfile` and run `bundle`, then add `jekyll-pdf` to your `_config.yml` like so:
```yaml
gems:
- jekyll-pdf
```
Now add `pdf: true` to any page's or document's front-matter, that you'd like to create a PDF version of.
To activate **Jekyll PDF** for multiple pages or entire collections you can use Jekyll's [front-matter defaults](https://jekyllrb.com/docs/configuration/#front-matter-defaults). The following example will create PDFs for each post in your blog.
```yaml
defaults:
-
scope:
path: ""
type: "posts"
values:
pdf: true
```
Link to the PDF using the `{{ page.pdf_url }}` liquid variable.
## Configuration
**Jekyll PDF** supports any configuration parameters [wkhtmltopdf](http://wkhtmltopdf.org/) does. For a full list of configuration parameters it supports see http://wkhtmltopdf.org/usage/wkhtmltopdf.txt
```yaml
pdf:
cache: false | directory | default: .asset-cache
page_size: A4, Letter, etc. | default: A4
layout: layout | default: pdf
```
All configuration parameters (with exception of `cache`) can be overridden from your page's or it's PDF layout's front-matter.
### Cache Folder
If Jekyll Assets is installed, Jekyll PDF will automatically use the same cache folder as Jekyll Assets (unless specified otherwise).
## Layouts
**Jekyll PDF** will check for your current layout suffixed with `_pdf` e.g. if you're using a layout called `post`, it will look for `_layouts/post_pdf.html`, falling back to your default PDF layout (usually `_layouts/pdf.html`).
To override this behaviour, add the `pdf_layout` variable to your page's YAML front-matter. For example:
```yaml
pdf_layout: my_custom_pdf_layout
```
## Partials (Header, Footer & Cover Page)
We'll automatically look for all partials in `_includes` directory, e.g. `header_html: pdf_header.html` will tell Jekyll PDF use `_includes/pdf_header.html`.
Please note that wkhtmltopdf requires all partials to be valid HTML documents for example:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.css">
</head>
<body>
Page {{ page.pdf.page }} of {{ page.pdf.topage }}
</body>
</html>
```
### Supported header & footer variables
| Liquid | Description |
|--------------------------------|-------------------------------------------------------------|
| `{{ page.pdf.page }}` | Replaced by the number of the pages currently being printed |
| `{{ page.pdf.topage }}` | Replaced by the number of the last page to be printed |
| `{{ page.pdf.section }}` | Replaced by the content of the current h1 tag |
| `{{ page.pdf.subsection }}` | Replaced by the content of the current h2 tag |
| `{{ page.pdf.subsubsection }}` | Replaced by the content of the current h3 tag |
## Troubleshooting
### Images aren't displaying in the PDF
If your images aren't displaying in the PDF, this is most likely due to the fact that wkhtmltopdf doesn't know where to look. Try prefixing your image URLs with `file://{{ site.dest }}`.
For asset URLs in CSS files we recommend creating a separate CSS file overriding the URLs with the prefix mentioned above.
---
## To Do
- Remove dependencies for ActiveSupport & PDFKit
- Write tests (rspec)
- Package default PDF layout file in Gem
- Support layouts in partials

1
Rakefile Normal file
View File

@ -0,0 +1 @@
task :default => nil

23
jekyll-pdf.gemspec Normal file
View File

@ -0,0 +1,23 @@
Gem::Specification.new do |spec|
spec.version = "0.1.0"
spec.homepage = "http://github.com/abemedia/jekyll-pdf/"
spec.authors = ["Adam Bouqdib"]
spec.email = ["adam@abemedia.co.uk"]
spec.files = %W(Gemfile README.md LICENSE) + Dir["lib/**/*"]
spec.summary = "PDF generator for Jekyll"
spec.name = "jekyll-pdf"
spec.license = "GPL-3.0"
spec.has_rdoc = false
spec.require_paths = ["lib"]
spec.description = spec.description = <<-DESC
A Jekyll plugin, that allows you to create PDF versions of your pages & documents.
DESC
spec.add_runtime_dependency("wkhtmltopdf-installer", "~> 0.12")
spec.add_runtime_dependency("pdfkit", "~> 0.8")
spec.add_runtime_dependency("digest", "~> 0")
spec.add_runtime_dependency("activesupport", "~> 4.2")
spec.add_runtime_dependency("jekyll", ">= 2.0", "~> 3.1")
spec.add_development_dependency "bundler", "~> 1.6"
end

1
lib/jekyll-pdf.rb Normal file
View File

@ -0,0 +1 @@
require "jekyll/pdf"

1
lib/jekyll/pdf.rb Normal file
View File

@ -0,0 +1 @@
Dir[File.dirname(__FILE__) + '/pdf/**/*.rb'].each {|file| require file }

View File

@ -0,0 +1,99 @@
require 'pdfkit'
require 'active_support/core_ext/hash/deep_merge'
module Jekyll
module PDF
class Document < Jekyll::Page
include Helper
def initialize(site, base, page)
@site = site
@base = base
@dir = File.dirname(page.url)
@name = File.basename(page.url, File.extname(page.url)) + ".pdf"
@settings = site.config['pdf'] || {}
@partials = ['cover','header_html','footer_html']
self.process(@name)
self.data = page.data.clone
self.content = page.content.clone
# Set layout to the PDF layout
self.data['layout'] = layout
# Get PDF settings from the layouts
@settings = (site.config['pdf'] || {}).deep_merge(self.getConfig(self.data))
PDFKit.configure do |config|
config.verbose = site.config['verbose']
end
# Set pdf_url variable in the source page (for linking to the PDF version)
page.data['pdf_url'] = self.url
# Set html_url variable in the source page (for linking to the HTML version)
self.data['html_url'] = page.url
# create the partial objects
@partials.each do |partial|
@settings[partial] = Jekyll::PDF::Partial.new(self, @settings[partial]) if @settings[partial] != nil
end
end
# Recursively merge settings from the page, layout, site config & jekyll-pdf defaults
# todo: use jekyll's merge function
def getConfig(data)
settings = data['pdf'].is_a?(Hash) ? data['pdf'] : {}
layout = @site.layouts[data['layout']].data.clone if data['layout'] != nil
# No parent layout found - return settings hash
return settings if layout == nil
# Merge settings with parent layout settings
layout['pdf'] = (layout['pdf'] || {}).deep_merge(settings)
return self.getConfig(layout)
end
def write(dest_prefix, dest_suffix = nil)
self.render(@site.layouts, @site.site_payload) if self.output == nil
path = File.join(dest_prefix, CGI.unescape(self.url))
dest = File.dirname(path)
# Create directory
FileUtils.mkdir_p(dest) unless File.exist?(dest)
# write partials
@partials.each do |partial|
@settings[partial].write if @settings[partial] != nil
end
# Debugging - create html version of PDF
File.open("#{path}.html", 'w') {|f| f.write(self.output) } if @settings["debug"]
@settings.delete("debug")
# Build PDF file
fix_relative_paths
kit = PDFKit.new(self.output, @settings)
file = kit.to_file(path)
#self.output = kit.to_pdf
end
def layout()
# Set page layout to the PDF layout
layout = self.data['pdf_layout'] || @settings['layout']
# Check if a PDF version exists for the current layout (e.g. layout_pdf)
if layout == nil && self.data['layout'] != nil && File.exist?("_layouts/" + self.data['layout'] + "_pdf.html")
layout = self.data['layout'] + "_pdf"
end
layout || 'pdf'
end
end
end
end

View File

@ -0,0 +1,18 @@
module Jekyll
module PDF
class Generator < Jekyll::Generator
safe true
priority :lowest
def generate(site)
# Loop through pages & documents and build PDFs
[site.pages, site.documents].each do |items|
items.each do |item|
site.static_files << Document.new(site, site.source, item) if item.data['pdf']
end
end
end
end
end
end

10
lib/jekyll/pdf/helper.rb Normal file
View File

@ -0,0 +1,10 @@
module Jekyll
module PDF
module Helper
def fix_relative_paths
prefix = "file://#{site.dest}/"
output = output.gsub(/(href|src)=(['"])\/([^\/"']([^\"']*|[^"']*))?['"]/, "\\1=\\2#{prefix}\\3\\2")
end
end
end
end

9
lib/jekyll/pdf/hooks.rb Normal file
View File

@ -0,0 +1,9 @@
# Delete temp files
Jekyll::Hooks.register :site, :post_write do |jekyll, payload|
if jekyll.data[:jekyll_pdf_partials]
jekyll.data[:jekyll_pdf_partials].each do |partial|
partial.clean
end
jekyll.data.delete(:jekyll_pdf_partials)
end
end

View File

@ -0,0 +1,46 @@
try_require "jekyll-assets" do
module Jekyll
module PDF
class AssetsTag < Jekyll::Assets::Liquid::Tag
# --------------------------------------------------------------------
# Tags that we allow our users to use.
# --------------------------------------------------------------------
AcceptableTags = %W(
pdf_img
pdf_image
pdf_javascript
pdf_stylesheet
pdf_asset_path
pdf_style
pdf_css
pdf_js
).freeze
def initialize(tag, args, tokens)
tag = tag.to_s.sub!("pdf_", "")
super(tag, args, tokens)
end
def render(context)
@path_prefix = "file://" + context.registers[:site].dest
super
end
private
def build_html(args, sprockets, asset, path = get_path(sprockets, asset))
data = @path_prefix + (args.key?(:data) && args[:data].key?(:uri) ? asset.data_uri : path)
format(Jekyll::Assets::Liquid::Tag::Tags[@tag], data, args.to_html)
end
end
end
end
Jekyll::PDF::AssetsTag::AcceptableTags.each do |tag|
Liquid::Template.register_tag tag, Jekyll::PDF::AssetsTag
end
end

158
lib/jekyll/pdf/partial.rb Normal file
View File

@ -0,0 +1,158 @@
require 'tmpdir'
require 'digest/md5'
module Jekyll
module PDF
class Partial
extend Forwardable
attr_accessor :doc
attr_accessor :partial
attr_accessor :write
attr_accessor :content, :ext
attr_writer :output
def_delegators :@doc, :site, :name, :ext, :relative_path, :extname,
:render_with_liquid?, :collection, :related_posts
# Initialize this Partial instance.
#
# doc - The Document.
#
# Returns the new Partial.
def initialize(doc, partial)
self.doc = doc
self.partial = partial
self.content = build_partial(partial)
end
# Fetch YAML front-matter data from related doc, without layout key
#
# Returns Hash of doc data
def data
@data ||= doc.data.dup
@data.delete("layout")
@data
end
def trigger_hooks(*)
end
def path
File.join(doc.path, partial)
end
# Returns the file name for the temporary file
def filename
File.basename(path, File.extname(path)) + "-" + Digest::MD5.hexdigest(to_s) + File.extname(path)
end
# Returns the cache directory
def dir
@dir ||= cache_dir
end
def to_s
output || content
end
def to_liquid
doc.data[partial] = nil
@to_liquid ||= doc.to_liquid
doc.data[partial] = self
@to_liquid
end
# Returns the shorthand String identifier of this doc.
def inspect
"<Partial: #{self.id}>"
end
def output
@output ||= Renderer.new(doc.site, self, site.site_payload).run
end
# generate temp file & set output to it's path
def write
tempfile = File.join(dir, filename)
unless File.exist?(tempfile)
FileUtils.mkdir_p(File.dirname(tempfile)) unless File.exist?(File.dirname(tempfile))
File.open(tempfile, 'w') {|f| f.write(to_s) }
end
site.data[:jekyll_pdf_partials] ||= []
site.data[:jekyll_pdf_partials] << self
@output = tempfile
end
# delete temp file
def clean
File.delete(@output)
end
def place_in_layout?
false
end
protected
def cache_dir
return site.config["pdf"]["cache"] if site.config["pdf"] != nil && site.config["pdf"].has_key?('cache')
# Use jekyll-assets cache directory if it exists
cache_dir = site.config["assets"]["cache"] || '.asset-cache' if site.config["assets"] != nil
File.join(cache_dir || Dir.tmpdir(), 'pdf')
end
# Internal: Generate partial html
#
# Partials are rendered same time as content is rendered.
#
# Returns partial html String
def build_partial(path)
# vars to insert into partial
vars = ['frompage','topage','page','webpage','section','subsection','subsubsection']
doc.data["pdf"] = {}
vars.each { |var| doc.data["pdf"][var] = "<span class='__#{var}'></span>" }
# JavaScript to replace var placeholders with content
script = "<script>!function(){var t={},n=document.location.search.substring(1).split('&');for(var e in n){var o=n[e].split('=',2);t[o[0]]=decodeURIComponent(o[1])}var n=#{vars};for(var e in n)for(var r=document.getElementsByClassName('__'+n[e]),a=0;a<r.length;++a)r[a].textContent=t[n[e]]}();</script>\n"
# Parse & render
content = File.read(File.join("_includes", path))
# Add replacer script to body
if content =~ /<\/body>/i
content[/(<\/body>)/i] = script + content[/(<\/body>)/i]
else
Jekyll.logger.warn <<-eos
Couldn't find <body> in #{path}. Make sure your partial is a properly formatted HTML document (including DOCTYPE) e.g.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
Page {{ pdf.page }} of {{ pdf.topage }}
</body>
</html>
eos
# No body found - insert html into default template
content = %{<!DOCTYPE html>
<html>
<body>
#{self.output}
#{script}
</body>
</html>
}
end
content
end
end
end
end