├── .yardopts ├── spec ├── fixtures │ ├── recursion │ │ ├── level1.textile │ │ └── level2 │ │ │ └── level2.markdown │ ├── code_block_with_utf8.md │ ├── integration │ │ ├── style.css │ │ ├── textile.textile │ │ └── markdown.md │ ├── autolink_url.md │ ├── code_block.textile │ ├── code_with_utf8.textile │ ├── yaml_front_matter.textile │ ├── code_block_with_utf8.textile │ ├── table.md │ └── long_code_block.md ├── spec_helper.rb ├── gimli │ ├── markup │ │ ├── yaml_frontmatter_remover_spec.rb │ │ ├── code_block_spec.rb │ │ ├── renderer_spec.rb │ │ └── code_spec.rb │ ├── wkhtmltopdf_spec.rb │ ├── path_spec.rb │ ├── markup_file_spec.rb │ └── converter_spec.rb └── integration_spec.rb ├── gemfiles └── Gemfile.ci ├── lib ├── gimli │ ├── version.rb │ ├── markup │ │ ├── yaml_frontmatter_remover.rb │ │ ├── code_block.rb │ │ ├── code.rb │ │ └── renderer.rb │ ├── config.rb │ ├── path.rb │ ├── markup.rb │ ├── wkhtmltopdf.rb │ ├── markupfile.rb │ ├── setup.rb │ └── converter.rb └── gimli.rb ├── .gitignore ├── .travis.yml ├── Guardfile ├── Gemfile ├── Dockerfile-alpine ├── Rakefile ├── ext └── github_markup.rb ├── Dockerfile ├── bin └── gimli ├── LICENSE ├── gimli.gemspec ├── CHANGELOG.md ├── README.md └── config └── style.css /.yardopts: -------------------------------------------------------------------------------- 1 | lib/**/*.rb README.textile 2 | 3 | -------------------------------------------------------------------------------- /spec/fixtures/recursion/level1.textile: -------------------------------------------------------------------------------- 1 | h1. Level 1 2 | -------------------------------------------------------------------------------- /spec/fixtures/code_block_with_utf8.md: -------------------------------------------------------------------------------- 1 | ``` 2 | まつもとゆきひろ 3 | ``` -------------------------------------------------------------------------------- /spec/fixtures/recursion/level2/level2.markdown: -------------------------------------------------------------------------------- 1 | = Level 2 = 2 | -------------------------------------------------------------------------------- /spec/fixtures/integration/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f00; 3 | } -------------------------------------------------------------------------------- /gemfiles/Gemfile.ci: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec :path => '..' 3 | -------------------------------------------------------------------------------- /spec/fixtures/autolink_url.md: -------------------------------------------------------------------------------- 1 | # Autolink url 2 | 3 | Autolink this https://github.com url. -------------------------------------------------------------------------------- /spec/fixtures/code_block.textile: -------------------------------------------------------------------------------- 1 | a 2 | 3 | ```ruby 4 | x = 1 5 | ``` 6 | 7 | b 8 | 9 | -------------------------------------------------------------------------------- /lib/gimli/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | VERSION = "0.5.9" 5 | end 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/code_with_utf8.textile: -------------------------------------------------------------------------------- 1 | a 2 | 3 | @Abcåäö@ 4 | 5 | b 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | doc/ 3 | coverage/ 4 | .yardoc/ 5 | /.bundle/ 6 | /tags 7 | Gemfile.lock 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: "rake" 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - 2.1.3 6 | gemfile: gemfiles/Gemfile.ci 7 | -------------------------------------------------------------------------------- /spec/fixtures/yaml_front_matter.textile: -------------------------------------------------------------------------------- 1 | --- 2 | layout: test 3 | --- 4 | 5 | 6 | 7 | This should be at the top of the file -------------------------------------------------------------------------------- /spec/fixtures/code_block_with_utf8.textile: -------------------------------------------------------------------------------- 1 | Abcåäö 2 | 3 | ```html 4 |

Abcåäö

5 | ÅÄÖ 6 | ``` 7 | 8 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', :version => 2 do 2 | watch(%r{^spec/.+_spec\.rb}) 3 | watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | 7 | -------------------------------------------------------------------------------- /spec/fixtures/table.md: -------------------------------------------------------------------------------- 1 | # Table 2 | 3 | | Tables | Are | Cool | 4 | | ------------- |:-------------:| -----:| 5 | | col 3 is | right-aligned | $1600 | 6 | | col 2 is | centered | $12 | 7 | | zebra stripes | are neat | $1 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | if ENV['COVERAGE'] 4 | require 'simplecov' 5 | SimpleCov.start 6 | end 7 | 8 | RSpec.configure do |config| 9 | config.mock_with :rr 10 | config.expect_with :rspec do |c| 11 | c.syntax = :should 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | 4 | group :test do 5 | gem 'yard' 6 | gem 'guard' 7 | gem 'guard-rspec' 8 | gem 'simplecov', :require => false 9 | if RUBY_PLATFORM =~ /linux/i 10 | gem 'rb-inotify' 11 | gem 'libnotify' 12 | end 13 | end 14 | 15 | -------------------------------------------------------------------------------- /Dockerfile-alpine: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4-alpine 2 | 3 | MAINTAINER Fredrik Wallgren 4 | 5 | RUN apk --update add --no-cache --virtual .gimli-build-dependencies \ 6 | build-base \ 7 | && gem install gimli \ 8 | && apk del .gimli-build-dependencies 9 | 10 | ENTRYPOINT ["gimli"] 11 | 12 | # Show the extended help 13 | CMD ["-h"] 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | require 'bundler' 4 | Bundler::GemHelper.install_tasks 5 | 6 | begin 7 | require 'rspec/core/rake_task' 8 | 9 | RSpec::Core::RakeTask.new do |t| 10 | t.rspec_opts = %w(-fd -c) 11 | end 12 | rescue LoadError 13 | desc message = %{"gem install rspec --pre" to run the specs} 14 | task(:spec) { abort message } 15 | end 16 | 17 | task :default => :spec 18 | task :test => :spec 19 | 20 | -------------------------------------------------------------------------------- /lib/gimli/markup/yaml_frontmatter_remover.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | module Markup 6 | 7 | # Class that knows how to remove yaml front matter 8 | class YamlFrontmatterRemover 9 | 10 | # Removes YAML Front Matter 11 | # Useful if you want to PDF your Jekyll site. 12 | def process(data) 13 | data.gsub /^(---\s*\n.*?\n?)^(---\s*$\n?)/m, '' 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ext/github_markup.rb: -------------------------------------------------------------------------------- 1 | require 'redcarpet' 2 | 3 | # Monkey patch github markup to use redcarpet with support for autolinks and tables 4 | module GitHub::Markup 5 | alias_method :old_render, :render 6 | def render(filename, content = nil) 7 | if Regexp.compile("\\.(md|mkdn?|mdwn|mdown|markdown|litcoffee)$") =~ filename 8 | Redcarpet::Markdown.new(Redcarpet::Render::HTML, :autolink => true, :tables => true).render(content) 9 | else 10 | old_render(filename, content) 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/gimli/config.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | # Class that keeps the config parameters 6 | class Config 7 | attr_accessor :file, :recursive, :merge, :debug, :wkhtmltopdf_parameters, :remove_front_matter, :output_filename, :output_dir, :stylesheet, :cover 8 | 9 | # Sets default values 10 | def initialize 11 | @recursive = false 12 | @merge = false 13 | @debug = false 14 | @page_numbers = false 15 | @table_of_contents = false 16 | @remove_front_matter = false 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.4-slim 2 | 3 | MAINTAINER Fredrik Wallgren 4 | 5 | ENV DEBIAN_FRONTEND noninteractive 6 | RUN buildDependencies=' \ 7 | build-essential \ 8 | ' \ 9 | && apt-get update \ 10 | && apt-get install -y --no-install-recommends --no-install-suggests ${buildDependencies} \ 11 | && gem install gimli \ 12 | && apt-get purge -y --auto-remove ${buildDependencies} \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # Make this image a wrapper for the CLI 16 | ENTRYPOINT ["gimli"] 17 | 18 | # Show the extended help by default 19 | CMD ["-h"] 20 | -------------------------------------------------------------------------------- /spec/gimli/markup/yaml_frontmatter_remover_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | require './lib/gimli/markup' 7 | 8 | describe Gimli::Markup::YamlFrontmatterRemover do 9 | 10 | it 'should remove front matter' do 11 | data = "---\nlayout: test\n---\n\n\nReal data" 12 | expected = 'Real data' 13 | Gimli::Markup::YamlFrontmatterRemover.new.process(data).should == expected 14 | end 15 | 16 | it 'should not modify string if no front matter exists' do 17 | data = 'Should not be modified' 18 | Gimli::Markup::YamlFrontmatterRemover.new.process(data).should == data 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /spec/gimli/markup/code_block_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | require './lib/gimli/markup' 7 | 8 | describe Gimli::Markup::CodeBlock do 9 | 10 | it 'should highlight code if language is supplied' do 11 | code_block = Gimli::Markup::CodeBlock.new('1', 'ruby', 'puts "hi"') 12 | code_block.highlighted.should include('class="CodeRay"') 13 | code_block.highlighted.should include('class="delimiter"') 14 | end 15 | 16 | it 'should highlight code if no language is supplied' do 17 | code_block = Gimli::Markup::CodeBlock.new('1', nil, 'puts "hi"') 18 | code_block.highlighted.should include('class="CodeRay"') 19 | end 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/gimli/markup/code_block.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'coderay' 4 | 5 | module Gimli 6 | 7 | module Markup 8 | 9 | # Class that contains data for a code block 10 | class CodeBlock 11 | attr_reader :id, :language, :code 12 | 13 | def initialize(id, language, code) 14 | @id, @language, @code = id, language, code 15 | end 16 | 17 | # Returns the code with syntax highlightning 18 | # @return [String] 19 | def highlighted 20 | if @language 21 | CodeRay.scan(@code, @language).html(:line_numbers => :table) 22 | else 23 | CodeRay.scan(@code, :text).div 24 | end 25 | end 26 | end 27 | end 28 | end 29 | 30 | 31 | -------------------------------------------------------------------------------- /bin/gimli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__)) + '/../lib/') 4 | 5 | require 'gimli' 6 | 7 | if ARGV.flags.version? 8 | puts "Version: #{Gimli::VERSION}" 9 | exit 10 | end 11 | 12 | config = Gimli.configure do |config| 13 | config.file = ARGV.flags.file 14 | config.recursive = ARGV.flags.recursive? 15 | config.merge = ARGV.flags.merge? 16 | config.debug = ARGV.flags.debug? 17 | config.wkhtmltopdf_parameters = ARGV.flags.wkhtmltopdfparameters 18 | config.remove_front_matter = ARGV.flags.removefrontmatter 19 | config.output_filename = ARGV.flags.outputfilename 20 | config.output_dir = ARGV.flags.outputdir 21 | config.stylesheet = ARGV.flags.stylesheet 22 | config.cover = ARGV.flags.cover 23 | end 24 | 25 | Gimli.process! config 26 | 27 | -------------------------------------------------------------------------------- /lib/gimli/path.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | # Class used to interact with directory structure 6 | class Path 7 | 8 | # Return an array of paths to valid markup file matching the passed pattern 9 | # @param [String] target 10 | # @param [Bool] recursive 11 | # @return [Array] an array of valid files 12 | def self.list_valid(target, recursive = false) 13 | if recursive 14 | target ||= Dir.pwd 15 | if File.directory?(target) 16 | target = File.join(target, '**', '*') 17 | end 18 | else 19 | target ||= Dir.pwd 20 | if File.directory?(target) 21 | target = File.join(target, '*') 22 | end 23 | end 24 | 25 | # Use select to support ruby 1.8 26 | Dir.glob(target).sort().select { |file| MarkupFile.new(file).valid? } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/gimli.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'gimli/version' 4 | require 'gimli/config' 5 | require 'gimli/setup' 6 | require 'gimli/markupfile' 7 | require 'gimli/converter' 8 | require 'gimli/path' 9 | require 'gimli/wkhtmltopdf' 10 | 11 | require File.expand_path(File.dirname(__FILE__)) + '/../ext/github_markup.rb' 12 | 13 | module Gimli 14 | 15 | # Create a config object 16 | # @example Example usage 17 | # config = Gimli.configure |config| do 18 | # config.file = './test.md' 19 | # config.output_dir = '/tmp' 20 | # config.table_of_contents = true 21 | # end 22 | def self.configure 23 | config = Config.new 24 | yield config 25 | config 26 | end 27 | 28 | # Starts the processing of selected files 29 | def self.process!(config) 30 | @files = Path.list_valid(config.file, config.recursive).map { |file| MarkupFile.new(file) } 31 | Converter.new(@files, config).convert! 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/gimli/markup/renderer_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | describe Gimli::Markup::Renderer do 8 | 9 | it "should remove yaml front matter if asked to" do 10 | output = "

This should be at the top of the file

" 11 | 12 | file = Gimli::MarkupFile.new File.expand_path('../../../fixtures/yaml_front_matter.textile', __FILE__) 13 | markup = Gimli::Markup::Renderer.new file, true 14 | 15 | markup.render.should == output 16 | end 17 | 18 | it "should render html with code blocks including utf-8 strings" do 19 | output = <<-HTML 20 |
21 |
まつもとゆきひろ
22 |
23 | HTML 24 | 25 | file = Gimli::MarkupFile.new File.expand_path('../../../fixtures/code_block_with_utf8.md', __FILE__) 26 | markup = Gimli::Markup::Renderer.new file, true 27 | 28 | markup.render.strip.should == output.strip 29 | end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /lib/gimli/markup.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This file is based on the markup class in gollum - https://github.com/github/gollum 4 | # (The MIT License) 5 | # 6 | # Copyright (c) Tom Preston-Werner, Rick Olson 7 | 8 | # Permission is hereby granted, free of charge, to any person obtaining a copy 9 | # of this software and associated documentation files (the 'Software'), to deal 10 | # in the Software without restriction, including without limitation the rights 11 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | # copies of the Software, and to permit persons to whom the Software is 13 | # furnished to do so, subject to the following conditions: 14 | 15 | require 'digest/sha1' 16 | 17 | require 'github/markup' 18 | require 'nokogiri' 19 | 20 | require 'gimli/markup/yaml_frontmatter_remover' 21 | require 'gimli/markup/code' 22 | require 'gimli/markup/code_block' 23 | require 'gimli/markup/renderer' 24 | 25 | module Gimli 26 | module Markup 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | require 'fileutils' 8 | 9 | describe 'Integration' do 10 | 11 | it 'should generate files from fixtures' do 12 | config = Gimli.configure do |c| 13 | c.file = 'spec/fixtures/integration/' 14 | c.output_dir = 'spec/tmp' 15 | end 16 | 17 | Gimli.process! config 18 | 19 | ::File.exists?('spec/tmp/markdown.pdf').should be true 20 | ::File.exists?('spec/tmp/textile.pdf').should be true 21 | end 22 | 23 | it 'should generate files from fixtures with aditional parameters' do 24 | config = Gimli.configure do |c| 25 | c.file = 'spec/fixtures/integration/' 26 | c.output_dir = 'spec/tmp/merged' 27 | c.merge = true 28 | c.stylesheet = 'spec/fixtures/integration/style.css' 29 | c.output_filename = 'merged' 30 | end 31 | 32 | Gimli.process! config 33 | 34 | ::File.exists?('spec/tmp/merged/merged.pdf').should be true 35 | end 36 | 37 | after do 38 | ::FileUtils.rm_rf 'spec/tmp' 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Fredrik Wallgren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /spec/gimli/wkhtmltopdf_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | describe Gimli::Wkhtmltopdf do 8 | 9 | before do 10 | @wkhtmltopdf = Gimli::Wkhtmltopdf.new 11 | end 12 | 13 | it 'should assemble correct command' do 14 | mock(@wkhtmltopdf).bin { '"wkhtmltopdf"' } 15 | args = @wkhtmltopdf.command('test.pdf') 16 | args.size.should eq 4 17 | args.should include '"wkhtmltopdf"' 18 | args.should include '-q' 19 | args.should include '-' 20 | args.should include '"test.pdf"' 21 | end 22 | 23 | it 'should use which to find wkhtmltopdf first time' do 24 | mock(@wkhtmltopdf).__double_definition_create__.call(:`, "which wkhtmltopdf") { '~/wkhtmltopdf' } 25 | @wkhtmltopdf.bin.should eq '"~/wkhtmltopdf"' 26 | @wkhtmltopdf.bin.should eq '"~/wkhtmltopdf"' # Should be cached 27 | end 28 | 29 | it 'should generate a pdf' do 30 | mock(@wkhtmltopdf).__double_definition_create__.call(:`, "which wkhtmltopdf") { '~/wkhtmltopdf' } 31 | mock(IO).popen("\"~/wkhtmltopdf\" -q - \"\"", "wb+") { true } 32 | @wkhtmltopdf.output_pdf('', '') 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /lib/gimli/markup/code.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | module Markup 6 | 7 | # Class that knows how to extract code blocks and render them with syntax highlightning 8 | class Code 9 | 10 | def initialize 11 | @code_blocks = [] 12 | end 13 | 14 | # Extract all code blocks into the codemap and replace with placeholders. 15 | # 16 | # @return [String] Returns the placeholder'd String data. 17 | def extract(data) 18 | data.gsub!(/^``` ?([^\r\n]+)?\r?\n(.+?)\r?\n```\r?$/m) do 19 | id = Digest::SHA1.hexdigest($2) 20 | @code_blocks << CodeBlock.new(id, $1, $2) 21 | id 22 | end 23 | data 24 | end 25 | 26 | # Process all code from the codemap and replace the placeholders with the 27 | # final HTML. 28 | # 29 | # @return [String] Returns the marked up String data. 30 | def process(data) 31 | return data if data.nil? || data.size.zero? || @code_blocks.size.zero? 32 | @code_blocks.each do |block| 33 | data.gsub!(block.id, block.highlighted) 34 | end 35 | data 36 | end 37 | end 38 | end 39 | end 40 | 41 | -------------------------------------------------------------------------------- /lib/gimli/wkhtmltopdf.rb: -------------------------------------------------------------------------------- 1 | module Gimli 2 | 3 | # The class that communicates with wkhtmltopdf 4 | class Wkhtmltopdf 5 | 6 | # Set up options for wkhtmltopdf 7 | # @param [String] parameters 8 | def initialize(parameters = nil) 9 | @parameters = parameters 10 | end 11 | 12 | # Convert the html to pdf and write it to file 13 | # @param [String] html the html input 14 | # @param [String] filename the name of the output file 15 | def output_pdf(html, filename) 16 | args = command(filename) 17 | invoke = args.join(' ') 18 | 19 | IO.popen(invoke, "wb+") do |pdf| 20 | pdf.puts(html) 21 | pdf.close_write 22 | pdf.gets(nil) 23 | end 24 | end 25 | 26 | # Assemble the command to run 27 | # @param [String] filename the outputed pdf's filename 28 | # @return [Array] a list of strings that make out the call to wkhtmltopdf 29 | def command(filename) 30 | [bin, @parameters, '-q', '-', "\"#{filename}\""].compact 31 | end 32 | 33 | # Find the wkhtmltopdf binary 34 | # @return [String] the path to the binary 35 | def bin 36 | @bin ||= "\"#{(`which wkhtmltopdf`).chomp}\"" 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/gimli/path_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | describe Gimli::Path do 8 | 9 | it 'should find all files in the current directory, with no specified target, without recursion' do 10 | mock(Dir).pwd { './spec/fixtures/recursion/' } 11 | Gimli::Path.list_valid(nil, false).length.should == 1 12 | end 13 | 14 | it 'should find all files in all subdirectories, with no specified target, with recursion ' do 15 | mock(Dir).pwd { './spec/fixtures/recursion/' } 16 | Gimli::Path.list_valid(nil, true).length.should == 2 17 | end 18 | 19 | it 'should find all files in all subdirectories, with a specified directory, with recursion' do 20 | Gimli::Path.list_valid('./spec/fixtures/recursion/', true).length.should == 2 21 | end 22 | 23 | it 'should find one file, with a specified file, without recursion' do 24 | Gimli::Path.list_valid('./spec/fixtures/recursion/level1.textile', false).length.should == 1 25 | end 26 | 27 | # What is the sound of one file recursing? 28 | it 'should find one file, with a specified file, with recursion' do 29 | Gimli::Path.list_valid('README.md', true).length.should == 1 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /gimli.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $:.unshift lib unless $:.include? lib 3 | 4 | require "gimli/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "gimli" 8 | s.version = Gimli::VERSION 9 | s.authors = "Fredrik Wallgren" 10 | s.email = "fredrik.wallgren@gmail.com" 11 | s.homepage = "https://github.com/walle/gimli" 12 | s.summary = "Utility for converting markup files to pdf files." 13 | s.description = "Utility for converting markup files to pdf files. Useful for reports etc." 14 | 15 | s.rubyforge_project = s.name 16 | 17 | s.rdoc_options = ["--charset=UTF-8"] 18 | s.extra_rdoc_files = %w[README.md LICENSE] 19 | 20 | s.add_dependency 'github-markup', '~> 1.3' 21 | s.add_dependency 'redcarpet', '~> 3.2' 22 | s.add_dependency 'RedCloth', '~> 4.2.7' 23 | 24 | s.add_dependency 'coderay', '~> 1.1' 25 | s.add_dependency 'nokogiri', '~> 1.8.0' 26 | 27 | s.add_dependency 'wkhtmltopdf-binary', '~> 0.12.3' 28 | s.add_dependency 'optiflag', '~> 0.7' 29 | 30 | s.add_development_dependency 'rake' 31 | s.add_development_dependency 'rspec' 32 | s.add_development_dependency 'rr' 33 | s.add_development_dependency 'bundler' 34 | 35 | s.files = Dir.glob("{bin,ext,lib,spec,config}/**/*") + ['LICENSE', 'README.md', 'CHANGELOG.md'] 36 | s.executables = ['gimli'] 37 | s.require_paths = ['lib'] 38 | end 39 | 40 | -------------------------------------------------------------------------------- /spec/gimli/markup/code_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | require './lib/gimli/markup' 7 | 8 | describe Gimli::Markup::Code do 9 | 10 | before do 11 | @code = Gimli::Markup::Code.new 12 | end 13 | 14 | it 'should extract code block with language' do 15 | language = 'ruby' 16 | code = "\tputs 'hi'" 17 | markup = "```#{language}\n#{code}\n```" 18 | @code.extract(markup) 19 | code_block = @code.instance_variable_get("@code_blocks").first 20 | code_block.id.should eq Digest::SHA1.hexdigest(code) 21 | code_block.language.should eq language 22 | code_block.code.should eq code 23 | end 24 | 25 | it 'should extract code block without language' do 26 | code = "\tputs 'hi'" 27 | markup = "```\n#{code}\n```" 28 | @code.extract(markup) 29 | code_block = @code.instance_variable_get("@code_blocks").first 30 | code_block.id.should eq Digest::SHA1.hexdigest(code) 31 | code_block.language.should be nil 32 | code_block.code.should eq code 33 | end 34 | 35 | it 'should process markup and give back code from id' do 36 | language = 'ruby' 37 | code = "\tputs 'hi'" 38 | id = Digest::SHA1.hexdigest(code) 39 | markup = "```#{language}\n#{code}\n```" 40 | markup = @code.extract(markup) 41 | code_block = @code.instance_variable_get("@code_blocks").first 42 | @code.process(markup).should eq code_block.highlighted 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /spec/gimli/markup_file_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | describe Gimli::MarkupFile do 8 | 9 | before do 10 | @file = Gimli::MarkupFile.new 'fake' 11 | end 12 | 13 | it 'should recognize valid format' do 14 | @file.valid_format?('textile').should be true 15 | end 16 | 17 | it 'should recognize invalid format' do 18 | @file.valid_format?('abc123').should be false 19 | end 20 | 21 | it 'should recognize nil as invalid format' do 22 | @file.valid_format?(nil).should be false 23 | end 24 | 25 | it 'should give the name as the filename without the extension' do 26 | file = Gimli::MarkupFile.new 'test.txt' 27 | file.name.should == File.basename(file.filename, File.extname(file.filename)) 28 | end 29 | 30 | it 'should know which formats that are valid' do 31 | @file.load_format(:md).should eq :markdown 32 | @file.load_format(:textile).should eq :textile 33 | @file.load_format(:rdoc).should eq :rdoc 34 | @file.load_format(:org).should eq :org 35 | @file.load_format(:creole).should eq :creole 36 | @file.load_format(:rst).should eq :rest 37 | @file.load_format(:asciidoc).should eq :asciidoc 38 | @file.load_format(:pod).should eq :pod 39 | @file.load_format('1').should eq :roff 40 | @file.load_format(:mediawiki).should eq :mediawiki 41 | @file.load_format('fake').should be nil 42 | @file.load_format(nil).should be nil 43 | end 44 | end 45 | 46 | -------------------------------------------------------------------------------- /spec/fixtures/long_code_block.md: -------------------------------------------------------------------------------- 1 | # Google! 2 | Following is a long line from `view-source:https://www.google.com/`. 3 | ```HTML 4 | window.google={kEI:"-YueUd_QCYLc8AS01IHgDg",getEI:function(a){for(var b;a&&(!a.getAttribute||!(b=a.getAttribute("eid")));)a=a.parentNode;return b||google.kEI},https:function(){return"https:"==window.location.protocol},kEXPI:"31215,140438,3300066,3300103,3300124,3300133,3300135,3300156,3310062,4000116,4001351,4001948,4002855,4003714,4003881,4003921,4004205,4004320,4004334,4004702,4004788,4004844,4004897,4004939,4004949,4004953,4004972,4005031,4005155,4005198,4005335,4005602,4005864,4005874,4006191,4006374,4006426,4006442,4006541,4006578,4006609,4006727,4006766,4006793,4006796,4006806,4007007,4007009,4007020,4007055,4007060,4007073,4007077,4007117,4007118,4007131,4007132,4007140,4007158,4007217,4007334",kCSI:{e:"31215,140438,3300066,3300103,3300124,3300133,3300135,3300156,3310062,4000116,4001351,4001948,4002855,4003714,4003881,4003921,4004205,4004320,4004334,4004702,4004788,4004844,4004897,4004939,4004949,4004953,4004972,4005031,4005155,4005198,4005335,4005602,4005864,4005874,4006191,4006374,4006426,4006442,4006541,4006578,4006609,4006727,4006766,4006793,4006796,4006806,4007007,4007009,4007020,4007055,4007060,4007073,4007077,4007117,4007118,4007131,4007132,4007140,4007158,4007217,4007334",ei:"-YueUd_QCYLc8AS01IHgDg"},authuser:0,ml:function(){},kHL:"en",time:function(){return(new Date).getTime()},log:function(a,b,c,h){var d=new Image,f=google.lc,e=google.li,g="";d.onerror=d.onload= 5 | ``` -------------------------------------------------------------------------------- /spec/fixtures/integration/textile.textile: -------------------------------------------------------------------------------- 1 | h1. Test file 2 | 3 | * A 4 | * Simple 5 | * List 6 | 7 | *** 8 | 9 | # A 10 | # Numbered 11 | # List 12 | 13 | h2. Tests 14 | 15 | h3. Link 16 | 17 | * http://google.com 18 | * "an example":http://example.com/ 19 | * Google[1] 20 | 21 | h3. Image 22 | 23 | !https://www.google.se/images/srpr/logo3w.png! 24 | 25 | h3. Blockquote 26 | 27 | bq. This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 28 | consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 29 | Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 30 | 31 | bq. Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 32 | id sem consectetuer libero luctus adipiscing. 33 | 34 | h3. Code 35 | 36 | ```ruby 37 | puts 'Hello world!' 38 | (1..10).each do |number| 39 | p number 40 | end 41 | ``` 42 | 43 | ## Subheading 44 | 45 | *Lorem* ipsum -dolor- sit ~amet~, _consectetur_ adipiscing elit. Phasellus aliquet hendrerit velit eu bibendum. Phasellus et metus at velit vulputate posuere ac a turpis. Vivamus lobortis lectus at augue pretium vel dictum enim mollis. Fusce quis urna eget urna porta vulputate. Mauris eget sapien quis orci auctor adipiscing ac vitae magna. Nulla mauris quam, convallis porta tincidunt quis, imperdiet vitae magna. Maecenas orci odio, ullamcorper et ullamcorper nec, commodo ut lacus. Morbi tortor nulla, facilisis id laoreet non, imperdiet scelerisque sapien. Cras posuere arcu mauris. Nullam ac arcu tellus, a pharetra tellus. Nulla ac odio felis, nec porttitor nisi. Mauris fringilla adipiscing urna, vel ullamcorper felis congue et. Sed sollicitudin tempor odio, vel dignissim diam facilisis et. Nam rhoncus lacinia commodo. 46 | 47 | http://google.com/[1] -------------------------------------------------------------------------------- /lib/gimli/markupfile.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | # Class used to load files and determine if they are valid 6 | class MarkupFile 7 | attr_reader :filename, :name, :data, :format 8 | 9 | # Accepted formats 10 | FORMATS = [:markdown, :textile, :rdoc, :org, :creole, :rest, :asciidoc, :pod, :roff, :mediawiki] 11 | 12 | # Initializes the file object. Only reads contents if it's a valid file 13 | def initialize(filename) 14 | @filename = filename 15 | extension = ::File.extname(@filename) 16 | @format = load_format(extension) 17 | @name = ::File.basename(@filename, extension) 18 | @data = ::File.open(@filename, 'rb') { |f| f.read } if valid? && ::File.exists?(@filename) 19 | end 20 | 21 | # Is the file valid 22 | # @return [Boolean] 23 | def valid? 24 | valid_format? @format 25 | end 26 | 27 | # Converts the format to a symbol if it's a valid format nil otherwise 28 | # @param [String] format 29 | # @return [Symbol|nil] 30 | def load_format(format) 31 | case format.to_s 32 | when /(md|mkdn?|mdown|markdown)$/i 33 | :markdown 34 | when /(textile)$/i 35 | :textile 36 | when /(rdoc)$/i 37 | :rdoc 38 | when /(org)$/i 39 | :org 40 | when /(creole)$/i 41 | :creole 42 | when /(re?st(\.txt)?)$/i 43 | :rest 44 | when /(asciidoc)$/i 45 | :asciidoc 46 | when /(pod)$/i 47 | :pod 48 | when /(\d)$/i 49 | :roff 50 | when /(media)?wiki$/i 51 | :mediawiki 52 | else 53 | nil 54 | end 55 | end 56 | 57 | # Checks if the format is a valid one 58 | # @param [String] format 59 | # @return [Boolean] 60 | def valid_format?(format) 61 | return false if format.nil? 62 | 63 | FORMATS.include? format.to_sym 64 | end 65 | end 66 | end 67 | 68 | -------------------------------------------------------------------------------- /lib/gimli/setup.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'optiflag' 4 | 5 | # Set up the flags available 6 | module Gimli extend OptiFlagSet 7 | optional_flag 'file' do 8 | description 'The file or folder to convert if you do not want to convert all in the current folder' 9 | alternate_forms 'f' 10 | end 11 | optional_flag 'outputdir' do 12 | description 'The directory to save parsed files if you do not want to use the current folder. The directory is created if it does not exist' 13 | alternate_forms 'o' 14 | end 15 | optional_flag 'stylesheet' do 16 | description 'The stylesheet to use to override the standard' 17 | alternate_forms 's' 18 | end 19 | optional_flag 'outputfilename' do 20 | description 'Sets the name of the output file. Only used with single file and in merge mode' 21 | alternate_forms 'n' 22 | end 23 | optional_flag 'wkhtmltopdfparameters' do 24 | description 'Parameters to be passed on to wkhtmltopdf. Use "" if more than one parameter. See wkhtmltopdf usage for possible parameters.' 25 | alternate_forms 'w' 26 | end 27 | optional_switch_flag 'recursive' do 28 | description 'Recurse current or target directory and convert all valid markup files' 29 | alternate_forms 'r' 30 | end 31 | optional_switch_flag 'merge' do 32 | description 'Merge markup files into single pdf file' 33 | alternate_forms 'm' 34 | end 35 | optional_switch_flag 'debug' do 36 | description 'Debugs wkhtmltopdf command and html output to stdout' 37 | alternate_forms 'd' 38 | end 39 | optional_switch_flag 'removefrontmatter' do 40 | description 'Remove yaml frontmatter from your files.' 41 | alternate_forms 'y' 42 | end 43 | optional_switch_flag 'version' do 44 | description 'Show version information and quit' 45 | alternate_forms 'v' 46 | end 47 | optional_flag 'cover' do 48 | description 'Create cover with the first headline' 49 | alternate_forms 'C' 50 | end 51 | usage_flag 'h', 'help', '?' 52 | 53 | and_process! 54 | end 55 | 56 | -------------------------------------------------------------------------------- /spec/fixtures/integration/markdown.md: -------------------------------------------------------------------------------- 1 | # Test file 2 | 3 | * A 4 | * Simple 5 | * List 6 | 7 | *** 8 | 9 | 1. A 10 | 2. Numbered 11 | 3. List 12 | 13 | ## Tests 14 | 15 | ### Link 16 | 17 | * http://google.com 18 | * 19 | * [an example](http://example.com/ "Title") 20 | * [Google][] 21 | 22 | ### Image 23 | 24 | ![Alt text](https://www.google.se/images/srpr/logo3w.png) 25 | 26 | ### Blockquote 27 | 28 | > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, 29 | > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. 30 | > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus. 31 | > 32 | > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse 33 | > id sem consectetuer libero luctus adipiscing. 34 | 35 | ### Code 36 | 37 | ```ruby 38 | puts 'Hello world!' 39 | (1..10).each do |number| 40 | p number 41 | end 42 | ``` 43 | 44 | ## Subheading 45 | 46 | **Lorem** ipsum __dolor__ sit *amet*, _consectetur_ adipiscing elit. Phasellus aliquet hendrerit velit eu bibendum. Phasellus et metus at velit vulputate posuere ac a turpis. Vivamus lobortis lectus at augue pretium vel dictum enim mollis. Fusce quis urna eget urna porta vulputate. Mauris eget sapien quis orci auctor adipiscing ac vitae magna. Nulla mauris quam, convallis porta tincidunt quis, imperdiet vitae magna. Maecenas orci odio, ullamcorper et ullamcorper nec, commodo ut lacus. Morbi tortor nulla, facilisis id laoreet non, imperdiet scelerisque sapien. Cras posuere arcu mauris. Nullam ac arcu tellus, a pharetra tellus. Nulla ac odio felis, nec porttitor nisi. Mauris fringilla adipiscing urna, vel ullamcorper felis congue et. Sed sollicitudin tempor odio, vel dignissim diam facilisis et. Nam rhoncus lacinia commodo. 47 | 48 | [Google]: http://google.com/ 49 | 50 | ## Autolink 51 | 52 | https://github.com 53 | 54 | ## Table 55 | 56 | | Tables | Are | Cool | 57 | | ------------- |:-------------:| -----:| 58 | | col 3 is | right-aligned | $1600 | 59 | | col 2 is | centered | $12 | 60 | | zebra stripes | are neat | $1 | -------------------------------------------------------------------------------- /lib/gimli/markup/renderer.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Gimli 4 | 5 | module Markup 6 | 7 | # Contains functionality to render html from a markup file 8 | class Renderer 9 | # Initialize a new Markup object. 10 | # 11 | # @param [Gimli::File] file The Gimli::File to process 12 | # @param [Boolean] do_remove_yaml_front_matter Should we remove the front matter? 13 | # @return [Gimli::Markup] 14 | def initialize(file, do_remove_yaml_front_matter = false) 15 | @file = file 16 | @do_remove_yaml_front_matter = do_remove_yaml_front_matter 17 | 18 | @data = file.data 19 | @code = Code.new 20 | @yaml_frontmatter_remover = YamlFrontmatterRemover.new 21 | end 22 | 23 | # Render the content with Gollum wiki syntax on top of the file's own 24 | # markup language. 25 | # 26 | # @return [String] The formatted data 27 | def render 28 | prepare_data 29 | render_data 30 | post_process_data 31 | 32 | return @data 33 | end 34 | 35 | private 36 | 37 | # Prepare data for rendering 38 | def prepare_data 39 | @data = @yaml_frontmatter_remover.process(@data) if @do_remove_yaml_front_matter 40 | @data = @data.force_encoding('utf-8') if @data.respond_to? :force_encoding 41 | @data = @code.extract(@data) 42 | end 43 | 44 | # Do the markup to html rendering 45 | def render_data 46 | begin 47 | @data = @data.force_encoding('utf-8') if @data.respond_to? :force_encoding 48 | @data = GitHub::Markup.render(@file.filename, @data) 49 | if @data.nil? 50 | raise "There was an error converting #{@file.name} to HTML." 51 | end 52 | rescue Object => e 53 | @data = %{

#{e.message}

} 54 | end 55 | end 56 | 57 | # Do post processing on data 58 | def post_process_data 59 | @data = @code.process(@data) 60 | doc = Nokogiri::HTML::DocumentFragment.parse(@data, 'UTF-8') 61 | @data = doc.to_xhtml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XHTML, :encoding => 'UTF-8') 62 | @data.gsub!(/

<\/p>/, '') 63 | end 64 | end 65 | end 66 | end 67 | 68 | 69 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.9 4 | 5 | * Add sort on files in path to ensure the alphabetical order is maintained. 6 | 7 | ## 0.5.8 8 | 9 | * Use a tempfile for the cover option. Fixes bug where using system ruby on 10 | OSX gets write error. 11 | 12 | ## 0.5.5 13 | 14 | * Add --cover option for creating cover with markdown file https://github.com/walle/gimli/pull/48 Thanks @k0kubun 15 | 16 | ## 0.5.4 17 | 18 | * Use the -q parameter instead of --quiet. https://github.com/walle/gimli/pull/52 Thanks @nelsonsar 19 | * Bugfix with UTF-8 characters. Thanks @toshi-kawanishi 20 | 21 | ## 0.5.3 22 | 23 | * Merge fixes for gemfile and gitignore. Thanks @docwhat 24 | 25 | ## 0.5.0 26 | 27 | * Support for autolinks and tables using PHP-Markdown style. This version monley patches github/markup to use redcarpet directly in order to be able to pass in extensions. 28 | 29 | ## 0.4.2 30 | 31 | * Fix bug with wkhtmltopdf arguments not being passed to said command #38 (@apognu) 32 | * Adds debug flag to output generated html to stdout 33 | * Add css styling to fix issue #36 34 | 35 | ## 0.4.1 36 | 37 | Add utf-8 meta tag to fix display of utf-8 characters 38 | 39 | ## 0.4.0 40 | 41 | * Remove some markup gems as dependencies. Install them outside of gimli to use them. Like all other more "unkommon" markup languages. 42 | 43 | ## 0.3.2 44 | 45 | * Code refactorings 46 | * Removed support for gollum's "tags" eg. [[File]]. Use markup to create links 47 | 48 | ## 0.3.1 49 | 50 | * Use coderay instead of albino and pygments. This fixes issues with 51 | pygments not being installed and simplifies testing. 52 | 53 | ## 0.3.0 54 | 55 | * Remove flags for table of contents and page numbering. Add flag for wkhtmltopdf parameters instead to give more flexibility. 56 | 57 | **This version changes the way you use gimli if you are using the -t or 58 | -p flags. Use the -w flag instead to send the parameters directly to 59 | wkhtmltopdf.** 60 | 61 | ## 0.2.3 62 | 63 | * Bugfix: Transcode to ISO-8859-1 in 1.9 also (@svendahlstrand) 64 | * Add support for table of contents 65 | * Bugfix: Fix incompatible character encodings with code fragments 66 | (@ebeigarts) 67 | * Remove warning of iconv being depricated 68 | 69 | ## 0.2.2 70 | 71 | * Add support for printing out page numbers 72 | 73 | ## 0.2.1 74 | 75 | * Add the ability to remove yaml front matter from documents before they are converted (@clowder) 76 | 77 | 78 | -------------------------------------------------------------------------------- /lib/gimli/converter.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'fileutils' 4 | require 'tempfile' 5 | 6 | require 'gimli/markup' 7 | 8 | module Gimli 9 | 10 | # The class that converts the files 11 | class Converter 12 | 13 | # Initialize the converter with a File 14 | # @param [Array] files The list of Gimli::MarkupFile to convert (passing a single file will still work) 15 | # @param [Gimli::Config] config 16 | def initialize(files, config) 17 | @files, @config = files, config 18 | 19 | @stylesheets = [] 20 | parameters = [@config.wkhtmltopdf_parameters] 21 | if config.cover 22 | @coverfile = Tempfile.new(['coverfile', '.html']) 23 | parameters << '--cover' << @coverfile.path 24 | end 25 | @wkhtmltopdf = Wkhtmltopdf.new parameters.join(' ') 26 | end 27 | 28 | # Convert the file and save it as a PDF file 29 | def convert! 30 | merged_contents = [] 31 | @files.each do |file| 32 | markup = Markup::Renderer.new file, @config.remove_front_matter 33 | html = convert_image_urls markup.render, file.filename 34 | if @config.merge 35 | html = "

#{html}" unless merged_contents.empty? 36 | merged_contents << html 37 | else 38 | output_pdf(html, file) 39 | end 40 | puts html if @config.debug 41 | end 42 | 43 | unless merged_contents.empty? 44 | html = merged_contents.join 45 | output_pdf(html, nil) 46 | end 47 | end 48 | 49 | # Rewrite relative image urls to absolute 50 | # @param [String] html some html to parse 51 | # @return [String] the html with all image urls replaced to absolute 52 | def convert_image_urls(html, filename) 53 | dir_string = ::File.dirname(::File.expand_path(filename)) 54 | html.scan(/]+src="([^"]+)"/).each do |url| 55 | html.gsub!(url[0], ::File.expand_path(url[0], dir_string)) unless url[0] =~ /^https?/ 56 | end 57 | 58 | html 59 | end 60 | 61 | # Create the pdf 62 | # @param [String] html the html input 63 | # @param [String] filename the name of the output file 64 | def output_pdf(html, filename) 65 | html = add_head html 66 | load_stylesheets 67 | generate_cover! 68 | append_stylesheets html 69 | puts @wkhtmltopdf.command(output_file(filename)).join(' ') if @config.debug 70 | @wkhtmltopdf.output_pdf html, output_file(filename) 71 | end 72 | 73 | def add_head(html) 74 | html = "\n\n\n\n#{html}" 75 | end 76 | 77 | # Load the stylesheets to pdfkit loads the default and the user selected if any 78 | def load_stylesheets 79 | # Load standard stylesheet 80 | style = ::File.expand_path("../../../config/style.css", __FILE__) 81 | @stylesheets << style 82 | @stylesheets << stylesheet if ::File.exists?(stylesheet) 83 | end 84 | 85 | def append_stylesheets(html) 86 | @stylesheets.each do |stylesheet| 87 | html.gsub!(/<\/head>/, style_tag_for(stylesheet) + '') 88 | end 89 | end 90 | 91 | def style_tag_for(stylesheet) 92 | "" 93 | end 94 | 95 | # Returns the selected stylesheet. Defaults to ./gimli.css 96 | # @return [String] 97 | def stylesheet 98 | @config.stylesheet.nil? ? 'gimli.css' : @config.stylesheet 99 | end 100 | 101 | # Returns the directory where to save the output. Defaults to ./ 102 | # @return [String] 103 | def output_dir 104 | output_dir = @config.output_dir.nil? ? Dir.getwd : @config.output_dir 105 | FileUtils.mkdir_p(output_dir) unless ::File.directory?(output_dir) 106 | output_dir 107 | end 108 | 109 | # Generate the name of the output file 110 | # @return [String] 111 | # @param [Gimli::MarkupFile] file optionally, specify a file, otherwise use output filename 112 | def output_file(file = nil) 113 | if file 114 | output_filename = file.name 115 | if !@config.output_filename.nil? && @files.length == 1 116 | output_filename = @config.output_filename 117 | end 118 | else 119 | output_filename = Time.now.to_s.split(' ').join('_') 120 | output_filename = @files.last.name if @files.length == 1 || @config.merge 121 | output_filename = @config.output_filename unless @config.output_filename.nil? 122 | end 123 | 124 | ::File.join(output_dir, "#{output_filename}.pdf") 125 | end 126 | 127 | # Generate cover file if optional cover was given 128 | def generate_cover! 129 | return unless @config.cover 130 | cover_file = MarkupFile.new @config.cover 131 | markup = Markup::Renderer.new cover_file 132 | html = "
\n#{markup.render}\n
" 133 | append_stylesheets(html) 134 | html = add_head(html) 135 | @coverfile.write(html) 136 | @coverfile.close 137 | end 138 | end 139 | end 140 | 141 | -------------------------------------------------------------------------------- /spec/gimli/converter_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require './spec/spec_helper' 4 | 5 | require './lib/gimli' 6 | 7 | describe Gimli::Converter do 8 | it 'should give the correct output_file with none given' do 9 | file = Gimli::MarkupFile.new 'fake' 10 | name = 'my_file' 11 | mock(file).name { name } 12 | 13 | converter = Gimli::Converter.new [file], Gimli::Config.new 14 | mock(converter).output_dir { Dir.getwd } 15 | 16 | converter.output_file.should == File.join(Dir.getwd, "#{name}.pdf") 17 | end 18 | 19 | it 'should give the correct output_file with one given' do 20 | file = Gimli::MarkupFile.new 'fake' 21 | name = 'my_file' 22 | mock(file).name { name } 23 | 24 | converter = Gimli::Converter.new [file], Gimli::Config.new 25 | mock(converter).output_dir { '/tmp/out' } 26 | 27 | converter.output_file(file).should == "/tmp/out/#{name}.pdf" 28 | end 29 | 30 | it 'should give the correct output_file when one is set' do 31 | file = Gimli::MarkupFile.new 'fake' 32 | output_filename = 'my_file' 33 | 34 | config = Gimli.configure do |c| 35 | c.output_filename = output_filename 36 | end 37 | 38 | converter = Gimli::Converter.new [file], config 39 | mock(converter).output_dir { Dir.getwd } 40 | 41 | converter.output_file(file).should == File.join(Dir.getwd, "#{output_filename}.pdf") 42 | end 43 | 44 | it 'should give the correct output_dir when none given' do 45 | dir = Dir.getwd 46 | 47 | file = Gimli::MarkupFile.new 'fake' 48 | converter = Gimli::Converter.new file, Gimli::Config.new 49 | 50 | converter.output_dir.should == dir 51 | end 52 | 53 | it 'should give the correct output_dir when given' do 54 | dir = '/tmp/out' 55 | 56 | file = Gimli::MarkupFile.new 'fake' 57 | 58 | config = Gimli.configure do |c| 59 | c.output_dir = dir 60 | end 61 | 62 | converter = Gimli::Converter.new file, config 63 | 64 | mock(File).directory?(dir) { true } 65 | 66 | converter.output_dir.should == dir 67 | end 68 | 69 | it 'should use default stylesheet if none given' do 70 | file = Gimli::MarkupFile.new 'fake' 71 | converter = Gimli::Converter.new file, Gimli::Config.new 72 | 73 | converter.stylesheet.should == 'gimli.css' 74 | end 75 | 76 | it 'should use stylesheet if given' do 77 | file = Gimli::MarkupFile.new 'fake' 78 | stylesheet = '/home/me/gimli/my-style.css' 79 | 80 | config = Gimli.configure do |c| 81 | c.stylesheet = stylesheet 82 | end 83 | 84 | converter = Gimli::Converter.new file, config 85 | 86 | converter.stylesheet.should == stylesheet 87 | end 88 | 89 | it 'should convert relative image urls to absolute' do 90 | file = Gimli::MarkupFile.new 'fake' 91 | filename = 'fixtures/fake.textile' 92 | dir_string = ::File.dirname(::File.expand_path(filename)) 93 | converter = Gimli::Converter.new file, Gimli::Config.new 94 | 95 | html = '

foo

bar

' 96 | valid_html = "

foo

\"\"

bar

\"\"" 97 | 98 | converter.convert_image_urls(html, filename).should == valid_html 99 | end 100 | 101 | it 'should not rewrite non relative urls' do 102 | file = Gimli::MarkupFile.new 'fake' 103 | filename = '../../fixtures/fake.textile' 104 | dir_string = ::File.dirname(::File.expand_path(filename)) 105 | converter = Gimli::Converter.new file, Gimli::Config.new 106 | 107 | html = '

foo

bar

' 108 | 109 | converter.convert_image_urls(html, filename).should == html 110 | end 111 | 112 | it 'should work on both absolute and relative images' do 113 | file = Gimli::MarkupFile.new 'fake' 114 | filename = '../../fixtures/fake.textile' 115 | dir_string = ::File.dirname(::File.expand_path(filename)) 116 | converter = Gimli::Converter.new file, Gimli::Config.new 117 | 118 | html = '

foo

bar

' 119 | valid_html = "

foo

\"\"

bar

\"\" \"\"" 120 | 121 | converter.convert_image_urls(html, filename).should == valid_html 122 | end 123 | 124 | it 'should create a cover file if given cover option' do 125 | file = Gimli::MarkupFile.new 'fake' 126 | config = Gimli.configure do |config| 127 | config.cover = 'fake' 128 | end 129 | converter = Gimli::Converter.new file, config 130 | any_instance_of(Gimli::Markup::Renderer) do |renderer| 131 | stub(renderer).render { 'fake' } 132 | end 133 | 134 | converter.generate_cover! 135 | coverfile = converter.instance_variable_get(:@coverfile) 136 | File.exists?(coverfile.path).should == true 137 | end 138 | end 139 | 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gimli – utility for converting markup to pdf 2 | 3 | [![](https://secure.travis-ci.org/walle/gimli.png)](http://travis-ci.org/walle/gimli) 4 | 5 | ## Description 6 | 7 | Gimli is a utility for converting markup to pdf files. Useful for reports and such things. 8 | It’s a developed version of [textile2pdf](https://github.com/walle/textile2pdf) to support multiple markup styles and to get syntax highlighting. 9 | 10 | It’s inspired by the markup convertion in [gollum](https://github.com/github/gollum). The markup code is adapted from gollum. 11 | It works by converting the markup to pdf using [wkhtmltopdf](https://github.com/antialize/wkhtmltopdf) 12 | The markup is converted to html using [github/markup](https://github.com/github/markup) 13 | 14 | ### Markup 15 | 16 | Markup files may be written in any format supported by GitHub-Markup (except roff). 17 | 18 | ### Images 19 | 20 | Images can be included by absolute url on your hard drive or a absolute url on the Internet. You can also refer to an image relative from the markup file. Example in textile. 21 | 22 |
 23 | !/tmp/test.jpg!
 24 | !http://upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png!
 25 | !../images/test.jpg!
 26 | 
27 | 28 | ## Installation 29 | 30 | The best way to install Gimli is with RubyGems: 31 | 32 | $ [sudo] gem install gimli 33 | 34 | You can install from source: 35 | 36 | ``` 37 | $ cd gimli/ 38 | $ bundle 39 | $ rake install 40 | ``` 41 | 42 | ### On Windows 43 | 44 | If you want to use gimli on windows you will have to make the following actions. 45 | 46 | * Install `wkhtmltopdf` separatly on your OS. Find the package [here](http://wkhtmltopdf.org/downloads.html). 47 | > Make sure that the path to the `wkhtmltopdf.exe` binary is in your `$PATH` environement variable. Also make sure that it's palced before the path to the ruby directory. 48 | 49 | * Install the `which` command of the GnuWin32 on your OS. Find the package [here](http://gnuwin32.sourceforge.net/packages/which.htm). 50 | This command allow gimli to locate the correct binary or `wkhtmltopdf` on your hard drive to perform the html->PDF conversion. 51 | > Make sure that the path to `which.exe` the binary is in your `$PATH` environement variable 52 | 53 | ## Running 54 | 55 | The standard way to run gimli is to go to a folder with markup files and running 56 | 57 | $ gimli 58 | 59 | To apply some style to the pdf or override the standard style add a css file in the directory named `gimli.css` or use the `-s` flag to point out another css file. 60 | 61 | Standard behavior is for gimli to output the files in the current directory. To override this use the `-o` flag to point out another output directory. Gimli tries to create it if it doesn’t exist. 62 | 63 | Gimli also plays nice with Jekyll style markup files. You can pass gimli the `-y` flag to have it remove Jekyll’s YAML front matter from the top of your markup files. Allowing you to use gimli & Jekyll together on your Blog/Resume/Catalogue to create nicely formatted versions for online and offline viewing. 64 | 65 | Gimli can add a cover to your document based on another markdown file. To do this use the `-cover` flag. eg. 66 | 67 | $ gimli -f doc-file.md -cover cover-file.md 68 | 69 | To pass parameters directly to wkhtmltopdf, use the `-w` flag. eg. 70 | 71 | $ gimli -f test.md -w '--toc --footer-right "[page]/[toPage]"' 72 | 73 | This gives a pdf with a table of contents and page numbers in the footer. 74 | 75 | See the [man page](http://wkhtmltopdf.org/usage/wkhtmltopdf.txt) for wkhtmltopdf for all possible parameters. 76 | 77 | Run `gimli -h` for a full list of options available 78 | 79 | ### Gimli Docker Container 80 | 81 | There is a Docker container to use for converting files using gimli without installing gimli to your computer, you can see more about it at https://registry.hub.docker.com/u/walle/gimli/ 82 | 83 | #### Running the image 84 | 85 | $ docker run -v : walle/gimli -f /my-file.md -o 86 | 87 | Where `` is the directory with the files you want to convert and `` is the directory in the image where the files will be put. This will write the pdf files to the same directory as where your source files is. If you want to make a new directory for the pdfs you can call the image like this: 88 | 89 | $ docker run -v : walle/gimli -f /my-file.md -o /pdfs 90 | 91 | This will put the pdfs in a folder named pdfs in your ``. 92 | 93 | ##### A real example 94 | 95 | $ docker run -v /home/walle/gimlidocuments:/tmp/gimli walle/gimli -f /tmp/gimli/my-file.md -o /tmp/gimli 96 | 97 | This will generate a pdf from the file in `/home/walle/gimlidocuments/my-file.md` in the `/home/walle/gimlidocuments` directory. 98 | 99 | ##### Arguments 100 | 101 | All arguments you supply to running the image will end up in gimli, so for an example you could supply the `-s` flag to use a custom stylesheet. But the stylesheet must be in `` to be readable by gimli. 102 | 103 | ## Syntax highlighting 104 | 105 | In page files you can get automatic syntax highlighting for a wide range of languages by using the following syntax: 106 | 107 | ```ruby 108 | def foo 109 | puts 'bar' 110 | end 111 | ``` 112 | 113 | The block must start with three backticks (as the first characters on the line). After that comes the name of the language that is contained by the block. The language must be one of the short name lexer strings supported by coderay. See the list of lexers for valid options. 114 | 115 | If the block contents are indented two spaces or one tab, then that whitespace will be ignored (this makes the blocks easier to read in plaintext). 116 | 117 | The block must end with three backticks as the first characters on a line. 118 | 119 | The syntax highlightning is powered by [coderay](https://github.com/rubychan/coderay) and is using a [github theme](https://github.com/pie4dan/CodeRay-GitHub-Theme). 120 | -------------------------------------------------------------------------------- /config/style.css: -------------------------------------------------------------------------------- 1 | body 2 | { 3 | font-family: "TrebuchetMS", "Trebuchet MS", "LucidaSansUnicode", "Lucida Sans Unicode", "LucidaSans", "Lucida Sans", Arial, sans-serif; 4 | font-size: 12px; 5 | } 6 | 7 | .page-break 8 | { 9 | page-break-before: always; 10 | } 11 | 12 | .CodeRay { 13 | background-color: #FFF; 14 | border: 1px solid #CCC; 15 | font-family: Monaco, "Courier New", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", monospace; 16 | color: #000; 17 | padding: 1em 0px 1em 1em; 18 | } 19 | 20 | .CodeRay pre { 21 | margin: 0px; 22 | } 23 | 24 | div.CodeRay { } 25 | span.CodeRay { white-space: pre; border: 0px; padding: 2px } 26 | 27 | table.CodeRay { border-collapse: collapse; width: 100%; padding: 2px } 28 | table.CodeRay td { 29 | padding: 1em 0.5em; 30 | vertical-align: top; 31 | } 32 | 33 | .CodeRay .line-numbers, .CodeRay .no { 34 | background-color: #ECECEC; 35 | color: #AAA; 36 | text-align: right; 37 | } 38 | 39 | .CodeRay .line-numbers a { 40 | color: #AAA; 41 | } 42 | 43 | .CodeRay .line-numbers tt { font-weight: bold } 44 | .CodeRay .line-numbers .highlighted { color: red } 45 | .CodeRay .line { display: block; float: left; width: 100%; } 46 | .CodeRay span.line-numbers { padding: 0px 4px } 47 | .CodeRay .code { width: 100% } 48 | 49 | ol.CodeRay { font-size: 10pt } 50 | ol.CodeRay li { white-space: pre } 51 | 52 | .CodeRay .code pre { overflow: auto } 53 | .CodeRay .debug { color:white ! important; background:blue ! important; } 54 | 55 | .CodeRay .annotation { color:#007 } 56 | .CodeRay .attribute-name { color:#f08 } 57 | .CodeRay .attribute-value { color:#700 } 58 | .CodeRay .binary { color:#509; font-weight:bold } 59 | .CodeRay .comment { color:#998; font-style: italic;} 60 | .CodeRay .char { color:#04D } 61 | .CodeRay .char .content { color:#04D } 62 | .CodeRay .char .delimiter { color:#039 } 63 | .CodeRay .class { color:#458; font-weight:bold } 64 | .CodeRay .complex { color:#A08; font-weight:bold } 65 | .CodeRay .constant { color:teal; } 66 | .CodeRay .color { color:#0A0 } 67 | .CodeRay .class-variable { color:#369 } 68 | .CodeRay .decorator { color:#B0B; } 69 | .CodeRay .definition { color:#099; font-weight:bold } 70 | .CodeRay .directive { color:#088; font-weight:bold } 71 | .CodeRay .delimiter { color:black } 72 | .CodeRay .doc { color:#970 } 73 | .CodeRay .doctype { color:#34b } 74 | .CodeRay .doc-string { color:#D42; font-weight:bold } 75 | .CodeRay .escape { color:#666; font-weight:bold } 76 | .CodeRay .entity { color:#800; font-weight:bold } 77 | .CodeRay .error { color:#F00; background-color:#FAA } 78 | .CodeRay .exception { color:#C00; font-weight:bold } 79 | .CodeRay .filename { color:#099; } 80 | .CodeRay .function { color:#900; font-weight:bold } 81 | .CodeRay .global-variable { color:teal; font-weight:bold } 82 | .CodeRay .hex { color:#058; font-weight:bold } 83 | .CodeRay .integer { color:#099; } 84 | .CodeRay .include { color:#B44; font-weight:bold } 85 | .CodeRay .inline { color: black } 86 | .CodeRay .inline .inline { background: #ccc } 87 | .CodeRay .inline .inline .inline { background: #bbb } 88 | .CodeRay .inline .inline-delimiter { color: #D14; } 89 | .CodeRay .inline-delimiter { color: #D14; } 90 | .CodeRay .important { color:#f00; } 91 | .CodeRay .interpreted { color:#B2B; font-weight:bold } 92 | .CodeRay .instance-variable { color:teal } 93 | .CodeRay .label { color:#970; font-weight:bold } 94 | .CodeRay .local-variable { color:#963 } 95 | .CodeRay .octal { color:#40E; font-weight:bold } 96 | .CodeRay .operator { } 97 | .CodeRay .predefined-constant { font-weight:bold } 98 | .CodeRay .predefined { color:#369; font-weight:bold } 99 | .CodeRay .preprocessor { color:#579; } 100 | .CodeRay .pseudo-class { color:#00C; font-weight:bold } 101 | .CodeRay .predefined-type { color:#074; font-weight:bold } 102 | .CodeRay .reserved, .keyword { color:#000; font-weight:bold } 103 | 104 | .CodeRay .key { color: #808; } 105 | .CodeRay .key .delimiter { color: #606; } 106 | .CodeRay .key .char { color: #80f; } 107 | .CodeRay .value { color: #088; } 108 | 109 | .CodeRay .regexp { background-color:#fff0ff } 110 | .CodeRay .regexp .content { color:#808 } 111 | .CodeRay .regexp .delimiter { color:#404 } 112 | .CodeRay .regexp .modifier { color:#C2C } 113 | .CodeRay .regexp .function { color:#404; font-weight: bold } 114 | 115 | .CodeRay .string { color: #D20; } 116 | .CodeRay .string .string { } 117 | .CodeRay .string .string .string { background-color:#ffd0d0 } 118 | .CodeRay .string .content { color: #D14; } 119 | .CodeRay .string .char { color: #D14; } 120 | .CodeRay .string .delimiter { color: #D14; } 121 | 122 | .CodeRay .shell { color:#D14 } 123 | .CodeRay .shell .content { } 124 | .CodeRay .shell .delimiter { color:#D14 } 125 | 126 | .CodeRay .symbol { color:#990073 } 127 | .CodeRay .symbol .content { color:#A60 } 128 | .CodeRay .symbol .delimiter { color:#630 } 129 | 130 | .CodeRay .tag { color:#070 } 131 | .CodeRay .tag-special { color:#D70; font-weight:bold } 132 | .CodeRay .type { color:#339; font-weight:bold } 133 | .CodeRay .variable { color:#036 } 134 | 135 | .CodeRay .insert { background: #afa; } 136 | .CodeRay .delete { background: #faa; } 137 | .CodeRay .change { color: #aaf; background: #007; } 138 | .CodeRay .head { color: #f8f; background: #505 } 139 | 140 | .CodeRay .insert .insert { color: #080; font-weight:bold } 141 | .CodeRay .delete .delete { color: #800; font-weight:bold } 142 | .CodeRay .change .change { color: #66f; } 143 | .CodeRay .head .head { color: #f4f; } 144 | 145 | .code pre { 146 | white-space: pre; 147 | white-space: pre-wrap; 148 | word-break: break-all; 149 | word-wrap: break-word; 150 | } 151 | 152 | table { 153 | width: 100%; 154 | overflow: auto; 155 | display: block; 156 | } 157 | 158 | table th { 159 | font-weight: bold; 160 | } 161 | 162 | table th, table td { 163 | border: 1px solid #ddd; 164 | padding: 6px 13px; 165 | } 166 | 167 | table tr { 168 | border-top: 1px solid #ccc; 169 | background-color: #fff; 170 | } 171 | 172 | table tr:nth-child(2n) { 173 | background-color: #f8f8f8; 174 | } 175 | 176 | .cover { 177 | text-align: center; 178 | margin-top: 100px; 179 | } 180 | --------------------------------------------------------------------------------