From b135372c302ef615a86e7d80380f77166ab939b4 Mon Sep 17 00:00:00 2001 From: Adam Bouqdib Date: Mon, 30 May 2016 11:41:16 +0000 Subject: [PATCH] v0.1.0 --- .gitignore | 40 +---- .travis.yml | 33 ++++ Gemfile | 8 + README.md | 107 ++++++++++++- Rakefile | 1 + jekyll-pdf.gemspec | 23 +++ lib/jekyll-pdf.rb | 1 + lib/jekyll/pdf.rb | 1 + lib/jekyll/pdf/document.rb | 99 ++++++++++++ lib/jekyll/pdf/generator.rb | 18 +++ lib/jekyll/pdf/helper.rb | 10 ++ lib/jekyll/pdf/hooks.rb | 9 ++ lib/jekyll/pdf/liquid/tags/jekyll-assets.rb | 46 ++++++ lib/jekyll/pdf/partial.rb | 158 ++++++++++++++++++++ 14 files changed, 516 insertions(+), 38 deletions(-) create mode 100644 .travis.yml create mode 100644 Gemfile create mode 100644 Rakefile create mode 100644 jekyll-pdf.gemspec create mode 100644 lib/jekyll-pdf.rb create mode 100644 lib/jekyll/pdf.rb create mode 100644 lib/jekyll/pdf/document.rb create mode 100644 lib/jekyll/pdf/generator.rb create mode 100644 lib/jekyll/pdf/helper.rb create mode 100644 lib/jekyll/pdf/hooks.rb create mode 100644 lib/jekyll/pdf/liquid/tags/jekyll-assets.rb create mode 100644 lib/jekyll/pdf/partial.rb diff --git a/.gitignore b/.gitignore index a8b1cda..9102477 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,4 @@ -*.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 +/Gemfile.lock +/.bundle +/vendor +*.gem \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b87c869 --- /dev/null +++ b/.travis.yml @@ -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 + +# diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..8cb02c8 --- /dev/null +++ b/Gemfile @@ -0,0 +1,8 @@ +source "https://rubygems.org" +gemspec + +group :test do + gem "rake" +end + +gem "jekyll-assets", "~> 2.2", :require => false \ No newline at end of file diff --git a/README.md b/README.md index 458cc9a..a195fd0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,105 @@ -# jekyll-pdf -Create PDFs from Jekyll pages & documents. +# Jekyll PDF + +Dynamically generate PDFs from Jekyll pages, posts & 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 + + + + + + + + Page {{ page.pdf.page }} of {{ page.pdf.topage }} + + +``` + +### 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 \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..ccd64f3 --- /dev/null +++ b/Rakefile @@ -0,0 +1 @@ +task :default => nil \ No newline at end of file diff --git a/jekyll-pdf.gemspec b/jekyll-pdf.gemspec new file mode 100644 index 0000000..f48fba2 --- /dev/null +++ b/jekyll-pdf.gemspec @@ -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 \ No newline at end of file diff --git a/lib/jekyll-pdf.rb b/lib/jekyll-pdf.rb new file mode 100644 index 0000000..6bb8bd2 --- /dev/null +++ b/lib/jekyll-pdf.rb @@ -0,0 +1 @@ +require "jekyll/pdf" \ No newline at end of file diff --git a/lib/jekyll/pdf.rb b/lib/jekyll/pdf.rb new file mode 100644 index 0000000..3804904 --- /dev/null +++ b/lib/jekyll/pdf.rb @@ -0,0 +1 @@ +Dir[File.dirname(__FILE__) + '/pdf/**/*.rb'].each {|file| require file } \ No newline at end of file diff --git a/lib/jekyll/pdf/document.rb b/lib/jekyll/pdf/document.rb new file mode 100644 index 0000000..ab85949 --- /dev/null +++ b/lib/jekyll/pdf/document.rb @@ -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 diff --git a/lib/jekyll/pdf/generator.rb b/lib/jekyll/pdf/generator.rb new file mode 100644 index 0000000..69fe6c8 --- /dev/null +++ b/lib/jekyll/pdf/generator.rb @@ -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 diff --git a/lib/jekyll/pdf/helper.rb b/lib/jekyll/pdf/helper.rb new file mode 100644 index 0000000..f040746 --- /dev/null +++ b/lib/jekyll/pdf/helper.rb @@ -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 diff --git a/lib/jekyll/pdf/hooks.rb b/lib/jekyll/pdf/hooks.rb new file mode 100644 index 0000000..db10670 --- /dev/null +++ b/lib/jekyll/pdf/hooks.rb @@ -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 \ No newline at end of file diff --git a/lib/jekyll/pdf/liquid/tags/jekyll-assets.rb b/lib/jekyll/pdf/liquid/tags/jekyll-assets.rb new file mode 100644 index 0000000..684390a --- /dev/null +++ b/lib/jekyll/pdf/liquid/tags/jekyll-assets.rb @@ -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 \ No newline at end of file diff --git a/lib/jekyll/pdf/partial.rb b/lib/jekyll/pdf/partial.rb new file mode 100644 index 0000000..e79e197 --- /dev/null +++ b/lib/jekyll/pdf/partial.rb @@ -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 + "" + 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] = "" } + + # JavaScript to replace var placeholders with content + 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 in #{path}. Make sure your partial is a properly formatted HTML document (including DOCTYPE) e.g. + + + + + + + + Page {{ pdf.page }} of {{ pdf.topage }} + + + eos + # No body found - insert html into default template + content = %{ + + + #{self.output} + #{script} + + + } + end + + content + + end + end + end +end \ No newline at end of file