├── test ├── files │ ├── Test File 2.md │ ├── format.plain │ ├── reference.doc │ ├── reference.docx │ ├── Test File 1.md │ ├── format.asciidoc │ ├── format.markdown │ ├── format.textile │ ├── format.man │ ├── format.rst │ ├── format.latex │ ├── format.mediawiki │ ├── format.html │ ├── format.html5 │ ├── format.org │ ├── format.beamer │ ├── format.context │ ├── format.s5 │ ├── format.slidy │ ├── format.dzslides │ ├── format.rtf │ ├── format.texinfo │ ├── format.docbook │ ├── format.opendocument │ ├── bomb.tex │ └── benchmark.txt ├── helper.rb ├── benchmark.rb ├── test_conversions.rb └── test_pandoc_ruby.rb ├── .github ├── FUNDING.yml └── workflows │ └── tests.yml ├── .document ├── .gitignore ├── Gemfile ├── pandoc-ruby.gemspec ├── LICENSE ├── Gemfile.lock ├── Rakefile ├── .rubocop.yml ├── .rubocop_todo.yml ├── README.md └── lib └── pandoc-ruby.rb /test/files/Test File 2.md: -------------------------------------------------------------------------------- 1 | # A Second Title 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: xwmx 2 | custom: https://paypal.me/WilliamMelody 3 | -------------------------------------------------------------------------------- /test/files/format.plain: -------------------------------------------------------------------------------- 1 | This is a Title 2 | 3 | Some emphasized text and a link 4 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | README.markdown 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | .rvmrc 7 | .bundle 8 | *.gem 9 | -------------------------------------------------------------------------------- /test/files/reference.doc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwmx/pandoc-ruby/HEAD/test/files/reference.doc -------------------------------------------------------------------------------- /test/files/reference.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xwmx/pandoc-ruby/HEAD/test/files/reference.docx -------------------------------------------------------------------------------- /test/files/Test File 1.md: -------------------------------------------------------------------------------- 1 | # This is a Title 2 | 3 | Some *emphasized text* and 4 | [a link](http://daringfireball.net/projects/markdown/) 5 | -------------------------------------------------------------------------------- /test/files/format.asciidoc: -------------------------------------------------------------------------------- 1 | == This is a Title 2 | 3 | Some _emphasized text_ and 4 | http://daringfireball.net/projects/markdown/[a link] 5 | -------------------------------------------------------------------------------- /test/files/format.markdown: -------------------------------------------------------------------------------- 1 | # This is a Title 2 | 3 | Some *emphasized text* and [a 4 | link](http://daringfireball.net/projects/markdown/) 5 | -------------------------------------------------------------------------------- /test/files/format.textile: -------------------------------------------------------------------------------- 1 | h1(#this-is-a-title). This is a Title 2 | 3 | Some _emphasized text_ and "a link":http://daringfireball.net/projects/markdown/ 4 | -------------------------------------------------------------------------------- /test/files/format.man: -------------------------------------------------------------------------------- 1 | .SH This is a Title 2 | Some \f[I]emphasized text\f[R] and \c 3 | .UR http://daringfireball.net/projects/markdown/ 4 | a link 5 | .UE \c 6 | -------------------------------------------------------------------------------- /test/files/format.rst: -------------------------------------------------------------------------------- 1 | This is a Title 2 | =============== 3 | 4 | Some *emphasized text* and `a 5 | link `__ 6 | -------------------------------------------------------------------------------- /test/files/format.latex: -------------------------------------------------------------------------------- 1 | \section{This is a Title}\label{this-is-a-title} 2 | 3 | Some \emph{emphasized text} and 4 | \href{http://daringfireball.net/projects/markdown/}{a link} 5 | -------------------------------------------------------------------------------- /test/files/format.mediawiki: -------------------------------------------------------------------------------- 1 | 2 | = This is a Title = 3 | 4 | Some ''emphasized text'' and [http://daringfireball.net/projects/markdown/ a link] 5 | -------------------------------------------------------------------------------- /test/files/format.html: -------------------------------------------------------------------------------- 1 |

This is a Title

2 |

Some emphasized text and a link

4 | -------------------------------------------------------------------------------- /test/files/format.html5: -------------------------------------------------------------------------------- 1 |

This is a Title

2 |

Some emphasized text and a link

4 | -------------------------------------------------------------------------------- /test/files/format.org: -------------------------------------------------------------------------------- 1 | * This is a Title 2 | :PROPERTIES: 3 | :CUSTOM_ID: this-is-a-title 4 | :END: 5 | Some /emphasized text/ and 6 | [[http://daringfireball.net/projects/markdown/][a link]] 7 | -------------------------------------------------------------------------------- /test/files/format.beamer: -------------------------------------------------------------------------------- 1 | \begin{frame}{This is a Title} 2 | \phantomsection\label{this-is-a-title} 3 | Some \emph{emphasized text} and 4 | \href{http://daringfireball.net/projects/markdown/}{a link} 5 | \end{frame} 6 | -------------------------------------------------------------------------------- /test/files/format.context: -------------------------------------------------------------------------------- 1 | \startsectionlevel[title={This is a Title},reference={this-is-a-title}] 2 | 3 | Some {\em emphasized text} and \goto{a 4 | link}[url(http://daringfireball.net/projects/markdown/)] 5 | 6 | \stopsectionlevel 7 | -------------------------------------------------------------------------------- /test/files/format.s5: -------------------------------------------------------------------------------- 1 |
2 |

This is a Title

3 |

Some emphasized text and a link

5 |
6 | -------------------------------------------------------------------------------- /test/files/format.slidy: -------------------------------------------------------------------------------- 1 |
2 |

This is a Title

3 |

Some emphasized text and a link

5 |
6 | -------------------------------------------------------------------------------- /test/files/format.dzslides: -------------------------------------------------------------------------------- 1 |
2 |

This is a Title

3 |

Some emphasized text and a link

5 |
6 | -------------------------------------------------------------------------------- /test/files/format.rtf: -------------------------------------------------------------------------------- 1 | {\pard \ql \f0 \sa180 \li0 \fi0 \outlinelevel0 \b \fs36 This is a Title\par} 2 | {\pard \ql \f0 \sa180 \li0 \fi0 Some {\i emphasized text} and {\field{\*\fldinst{HYPERLINK "http://daringfireball.net/projects/markdown/"}}{\fldrslt{\ul 3 | a link 4 | }}} 5 | \par} 6 | -------------------------------------------------------------------------------- /test/files/format.texinfo: -------------------------------------------------------------------------------- 1 | @node Top 2 | @top Top 3 | 4 | @menu 5 | * This is a Title:: 6 | @end menu 7 | 8 | @node This is a Title 9 | @chapter This is a Title 10 | @anchor{#this-is-a-title} 11 | Some @emph{emphasized text} and 12 | @uref{http://daringfireball.net/projects/markdown/,a link} 13 | -------------------------------------------------------------------------------- /test/files/format.docbook: -------------------------------------------------------------------------------- 1 |
2 | This is a Title 3 | 4 | Some emphasized text and 5 | a 6 | link 7 | 8 |
9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | # Add dependencies required to use your gem here. 3 | # Example: 4 | # gem "activesupport", ">= 2.3.5" 5 | 6 | # Add dependencies to develop your gem here. 7 | # Include everything needed to run rake, tests, features, etc. 8 | group :development, :test do 9 | gem 'minitest' 10 | gem 'mocha' 11 | gem 'rake' 12 | gem 'rdoc' 13 | gem 'rubocop' 14 | end 15 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | warn e.message 7 | warn 'Run `bundle install` to install missing gems' 8 | exit e.status_code 9 | end 10 | require 'minitest/autorun' 11 | require 'minitest/pride' 12 | require 'mocha/minitest' 13 | 14 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 15 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 16 | require 'pandoc-ruby' 17 | -------------------------------------------------------------------------------- /test/files/format.opendocument: -------------------------------------------------------------------------------- 1 | This 2 | is a Title 3 | Some 4 | emphasized text and 5 | a 6 | link 7 | -------------------------------------------------------------------------------- /test/files/bomb.tex: -------------------------------------------------------------------------------- 1 | \documentclass{article} 2 | \renewcommand*\familydefault{\sfdefault} 3 | 4 | \makeatletter 5 | 6 | \newcommand{\bomb@anglex}{0} 7 | \newcommand{\bomb@scalex}{1} 8 | \newcommand{\bomb@shaded}{N} 9 | \newcommand{\bomb@shadecolor}{black} 10 | \newcommand{\bomb@shadeperc}{25} 11 | \newcommand{\bomb@emphedge}{N} 12 | \newcommand{\bomb@emphstyle}{thick} 13 | \newcommand{\bomb}{ 14 | \bomb@shadecolor \bomb@shaded 15 | } 16 | 17 | 18 | \begin{document} 19 | 20 | \bomb 21 | 22 | \end{document} 23 | 24 | -------------------------------------------------------------------------------- /pandoc-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'pandoc-ruby' 8 | s.version = '2.1.10' 9 | s.authors = ['William Melody'] 10 | s.date = '2023-11-24' 11 | s.description = 'Ruby wrapper for Pandoc' 12 | s.email = 'hi@williammelody.com' 13 | s.extra_rdoc_files = [ 14 | 'LICENSE', 15 | 'README.md' 16 | ] 17 | s.files = %w[ 18 | .document 19 | Gemfile 20 | Gemfile.lock 21 | LICENSE 22 | README.md 23 | Rakefile 24 | lib/pandoc-ruby.rb 25 | pandoc-ruby.gemspec 26 | test/benchmark.rb 27 | test/files/benchmark.txt 28 | test/files/Test\ File\ 1.md 29 | test/files/Test\ File\ 2.md 30 | test/helper.rb 31 | test/test_conversions.rb 32 | test/test_pandoc_ruby.rb 33 | ] 34 | s.homepage = 'http://github.com/xwmx/pandoc-ruby' 35 | s.licenses = ['MIT'] 36 | s.require_paths = ['lib'] 37 | s.required_ruby_version = '>= 2.2' 38 | s.summary = 'PandocRuby' 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 William Melody 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # .github/workflows/tests.yml 3 | ############################################################################### 4 | 5 | name: "pandoc-ruby · Test Suite" 6 | 7 | on: 8 | pull_request: 9 | branches: [ master ] 10 | push: 11 | branches: [ master ] 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test-ubuntu-latest: 16 | name: "Test: Ubuntu Latest" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Setup Ruby 21 | # https://github.com/ruby/setup-ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: 3.2.1 25 | bundler-cache: true 26 | - name: "Setup Environment" 27 | run: | 28 | sudo apt-get update 29 | sudo apt-get -yqq install libpq-dev build-essential libcurl4-openssl-dev 30 | curl -LO https://github.com/jgm/pandoc/releases/download/3.1.9/pandoc-3.1.9-1-amd64.deb 31 | sudo dpkg -i pandoc-3.1.9-1-amd64.deb 32 | gem install bundler 33 | bundle install --jobs 4 --retry 3 34 | - name: "Run tests" 35 | run: | 36 | bundle exec rake 37 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | json (2.6.3) 6 | language_server-protocol (3.17.0.3) 7 | minitest (5.20.0) 8 | mocha (2.1.0) 9 | ruby2_keywords (>= 0.0.5) 10 | parallel (1.23.0) 11 | parser (3.2.2.4) 12 | ast (~> 2.4.1) 13 | racc 14 | psych (5.1.1.1) 15 | stringio 16 | racc (1.7.3) 17 | rainbow (3.1.1) 18 | rake (13.1.0) 19 | rdoc (6.6.0) 20 | psych (>= 4.0.0) 21 | regexp_parser (2.8.2) 22 | rexml (3.2.6) 23 | rubocop (1.57.2) 24 | json (~> 2.3) 25 | language_server-protocol (>= 3.17.0) 26 | parallel (~> 1.10) 27 | parser (>= 3.2.2.4) 28 | rainbow (>= 2.2.2, < 4.0) 29 | regexp_parser (>= 1.8, < 3.0) 30 | rexml (>= 3.2.5, < 4.0) 31 | rubocop-ast (>= 1.28.1, < 2.0) 32 | ruby-progressbar (~> 1.7) 33 | unicode-display_width (>= 2.4.0, < 3.0) 34 | rubocop-ast (1.30.0) 35 | parser (>= 3.2.1.0) 36 | ruby-progressbar (1.13.0) 37 | ruby2_keywords (0.0.5) 38 | stringio (3.0.9) 39 | unicode-display_width (2.5.0) 40 | 41 | PLATFORMS 42 | ruby 43 | 44 | DEPENDENCIES 45 | minitest 46 | mocha 47 | rake 48 | rdoc 49 | rubocop 50 | 51 | BUNDLED WITH 52 | 2.2.28 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | warn e.message 7 | warn 'Run `bundle install` to install missing gems' 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'rake/testtask' 13 | Rake::TestTask.new(:test) do |test| 14 | test.libs << 'lib' << 'test' 15 | test.pattern = 'test/**/test_*.rb' 16 | test.verbose = true 17 | end 18 | 19 | task :default => :test 20 | 21 | require 'rdoc/task' 22 | Rake::RDocTask.new do |rdoc| 23 | version = File.exist?('VERSION') ? File.read('VERSION') : '' 24 | 25 | rdoc.rdoc_dir = 'rdoc' 26 | rdoc.title = "pandoc-ruby #{version}" 27 | rdoc.rdoc_files.include('README*') 28 | rdoc.rdoc_files.include('lib/**/*.rb') 29 | end 30 | 31 | require './lib/pandoc-ruby.rb' 32 | desc < 'markdown', 53 | :to => to 54 | ) 55 | 56 | File.open(to_file, 'w') do |file| 57 | file.write(converted_content) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /test/benchmark.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # From on Ryan Tomayako's benchmark script from: 4 | # http://tomayko.com/writings/ruby-markdown-libraries-real-cheap-for-you-two-for-price-of-one 5 | 6 | iterations = 100 7 | test_file = File.join(File.dirname(__FILE__), 'files', 'benchmark.txt') 8 | impl_gems = { 9 | 'BlueCloth' => 'bluecloth', 10 | 'RDiscount' => 'rdiscount', 11 | 'Maruku' => 'maruku', 12 | 'PandocRuby' => 'pandoc-ruby' 13 | } 14 | 15 | implementations = impl_gems.keys 16 | 17 | # Attempt to require each implementation and remove any that are not 18 | # installed. 19 | implementations.reject! do |class_name| 20 | begin 21 | require impl_gems[class_name] 22 | false 23 | rescue LoadError => boom 24 | puts "#{class_name} excluded. Try: gem install #{impl_gems[class_name]}" 25 | true 26 | end 27 | end 28 | 29 | # Grab actual class objects. 30 | implementations.map! { |class_name| Object.const_get(class_name) } 31 | 32 | def benchmark(implementation, text, iterations) 33 | start = Time.now 34 | iterations.times do |_i| 35 | implementation.new(text).to_html 36 | end 37 | Time.now - start 38 | end 39 | 40 | test_data = File.read(test_file) 41 | 42 | puts 'Spinning up ...' 43 | implementations.each { |impl| benchmark(impl, test_data, 1) } 44 | 45 | puts 'Running benchmarks ...' 46 | results = 47 | implementations.inject([]) do |r, impl| 48 | GC.start 49 | r << [impl, benchmark(impl, test_data, iterations)] 50 | end 51 | 52 | puts "Results for #{iterations} iterations:" 53 | results.each do |impl, time| 54 | printf " %10s %09.06fs total time, %09.06fs average\n", 55 | "#{impl}:", 56 | time, 57 | time / iterations 58 | end 59 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # .rubocop.yml 2 | # 3 | # Configuration for rubocop, a static code analyzer for Ruby. 4 | # 5 | # https://github.com/bbatsov/rubocop#configuration 6 | 7 | inherit_from: .rubocop_todo.yml 8 | 9 | # Layout/AccessModifierIndentation 10 | # 11 | # Indent access modifiers like `protected` and `private`. 12 | Layout/AccessModifierIndentation: 13 | EnforcedStyle: indent 14 | 15 | Layout/FirstHashElementIndentation: 16 | EnforcedStyle: consistent 17 | 18 | Layout/HashAlignment: 19 | EnforcedHashRocketStyle: table 20 | 21 | # Layout/IndentationConsistency 22 | # 23 | # Use Rails-style access modifier indentation. 24 | # 25 | # Example: 26 | # ``` 27 | # class Foo 28 | # def bar 29 | # puts 'bar' 30 | # end 31 | # 32 | # private 33 | # 34 | # def baz 35 | # puts 'baz' 36 | # end 37 | # end 38 | # ``` 39 | Layout/IndentationConsistency: 40 | EnforcedStyle: indented_internal_methods 41 | 42 | Layout/SpaceAroundOperators: 43 | Enabled: false 44 | 45 | Metrics/AbcSize: 46 | Enabled: false 47 | 48 | Metrics/BlockLength: 49 | Enabled: false 50 | 51 | Metrics/ClassLength: 52 | Enabled: false 53 | 54 | # Naming/FileName 55 | # 56 | # Long, long ago, this was named with a dash rather than an underscore. Now 57 | # it's unconventional, but let's consider it retro and leave it for now so we 58 | # don't have to rename the gem. TODO: Rename using an underscore. 59 | Naming/FileName: 60 | Exclude: 61 | - 'lib/pandoc-ruby.rb' 62 | 63 | Style/AccessorGrouping: 64 | Enabled: false 65 | 66 | Style/HashSyntax: 67 | EnforcedStyle: hash_rockets 68 | 69 | # Style/RedundantBegin 70 | # 71 | # Permit redundant `begin` block to support Ruby 2.4 and earlier. 72 | # 73 | # See also: 74 | # https://github.com/xwmx/pandoc-ruby/issues/47 75 | # https://github.com/xwmx/pandoc-ruby/issues/41 76 | Style/RedundantBegin: 77 | Enabled: false 78 | 79 | # Style/SymbolArray 80 | # 81 | # Avoid `%i` syntax so symbols look like symbols. 82 | Style/SymbolArray: 83 | EnforcedStyle: brackets 84 | 85 | # Style/RedundantSelf 86 | # 87 | # Explicit `self` is currently preferred in this project in order to 88 | # better distinguish between accessors and local variables. 89 | Style/RedundantSelf: 90 | Exclude: 91 | - 'lib/pandoc-ruby.rb' 92 | -------------------------------------------------------------------------------- /test/test_conversions.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | # Generate tests for converting to and from various formats. Use two nested 4 | # loops to iterate over each source and destination format, using files with 5 | # names of the following structure: "format.#{format_name}" 6 | describe 'Conversions' do 7 | @extensions = [] 8 | Dir.glob(File.join(File.dirname(__FILE__), 'files', 'format*')) do |f| 9 | @extensions << f.match(/format\.(\w+)\Z/)[1] 10 | end 11 | 12 | [:markdown, :html, :rst, :latex].each do |from| 13 | @extensions.each do |to| 14 | next if from == to 15 | 16 | it "converts #{from} to #{to}" do 17 | files_dir = File.join(File.dirname(__FILE__), 'files') 18 | from_content = File.read(File.join(files_dir, "format.#{from}")) 19 | to_content = File.read(File.join(files_dir, "format.#{to}")) 20 | 21 | converted_content = PandocRuby.convert( 22 | from_content, 23 | :from => from, 24 | :to => to 25 | ) 26 | 27 | assert_equal( 28 | to_content.strip, 29 | converted_content.strip, 30 | <<-HEREDOC 31 | --------- 32 | EXPECTED: 33 | --------- 34 | #{to_content.strip} 35 | --------- 36 | ------- 37 | ACTUAL: 38 | ------- 39 | #{converted_content.strip} 40 | ------- 41 | HEREDOC 42 | ) 43 | end 44 | end 45 | end 46 | 47 | describe '.docx' do 48 | it "converts from docx to html" do 49 | converted_content = PandocRuby.convert( 50 | ['./test/files/reference.docx'], 51 | :from => 'docx', 52 | :to => 'html' 53 | ) 54 | assert_equal("

Hello World.

", converted_content.strip) 55 | end 56 | 57 | it "raises an error when attempting to convert doc with docx format" do 58 | error = assert_raises(RuntimeError) do 59 | PandocRuby.convert( 60 | ['./test/files/reference.doc'], 61 | :from => 'docx', 62 | :to => 'html' 63 | ) 64 | end 65 | 66 | assert_match(/couldn't unpack docx container/, error.message) 67 | end 68 | 69 | it "raises an error when attempting to convert doc with doc format" do 70 | error = assert_raises(RuntimeError) do 71 | PandocRuby.convert( 72 | ['./test/files/reference.doc'], 73 | :from => 'doc', 74 | :to => 'html' 75 | ) 76 | end 77 | 78 | assert_match(/Pandoc can convert from DOCX, but not from DOC./, error.message) 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-11-25 00:17:47 UTC using RuboCop version 1.57.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: Severity, Include. 11 | # Include: **/*.gemspec 12 | Gemspec/RequiredRubyVersion: 13 | Exclude: 14 | - 'pandoc-ruby.gemspec' 15 | 16 | # Offense count: 1 17 | # This cop supports safe autocorrection (--autocorrect). 18 | Layout/HeredocIndentation: 19 | Exclude: 20 | - 'test/test_conversions.rb' 21 | 22 | # Offense count: 1 23 | # This cop supports unsafe autocorrection (--autocorrect-all). 24 | Lint/UselessAssignment: 25 | Exclude: 26 | - 'test/benchmark.rb' 27 | 28 | # Offense count: 1 29 | # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. 30 | Metrics/MethodLength: 31 | Max: 19 32 | 33 | # Offense count: 2 34 | # This cop supports safe autocorrection (--autocorrect). 35 | # Configuration parameters: PreferredName. 36 | Naming/RescuedExceptionsVariableName: 37 | Exclude: 38 | - 'lib/pandoc-ruby.rb' 39 | - 'test/benchmark.rb' 40 | 41 | # Offense count: 1 42 | # Configuration parameters: AllowedConstants. 43 | Style/Documentation: 44 | Exclude: 45 | - 'spec/**/*' 46 | - 'test/**/*' 47 | - 'lib/pandoc-ruby.rb' 48 | 49 | # Offense count: 3 50 | # This cop supports safe autocorrection (--autocorrect). 51 | # Configuration parameters: MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns. 52 | # SupportedStyles: annotated, template, unannotated 53 | Style/FormatStringToken: 54 | EnforcedStyle: unannotated 55 | 56 | # Offense count: 8 57 | # This cop supports unsafe autocorrection (--autocorrect-all). 58 | # Configuration parameters: EnforcedStyle. 59 | # SupportedStyles: always, always_true, never 60 | Style/FrozenStringLiteralComment: 61 | Exclude: 62 | - 'Gemfile' 63 | - 'Rakefile' 64 | - 'lib/pandoc-ruby.rb' 65 | - 'pandoc-ruby.gemspec' 66 | - 'test/benchmark.rb' 67 | - 'test/helper.rb' 68 | - 'test/test_conversions.rb' 69 | - 'test/test_pandoc_ruby.rb' 70 | 71 | # Offense count: 1 72 | # This cop supports safe autocorrection (--autocorrect). 73 | Style/RedundantFileExtensionInRequire: 74 | Exclude: 75 | - 'Rakefile' 76 | 77 | # Offense count: 1 78 | # This cop supports safe autocorrection (--autocorrect). 79 | # Configuration parameters: AllowMultipleReturnValues. 80 | Style/RedundantReturn: 81 | Exclude: 82 | - 'lib/pandoc-ruby.rb' 83 | 84 | # Offense count: 1 85 | # This cop supports unsafe autocorrection (--autocorrect-all). 86 | # Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. 87 | # AllowedMethods: present?, blank?, presence, try, try! 88 | Style/SafeNavigation: 89 | Exclude: 90 | - 'lib/pandoc-ruby.rb' 91 | 92 | # Offense count: 4 93 | # This cop supports safe autocorrection (--autocorrect). 94 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 95 | # SupportedStyles: single_quotes, double_quotes 96 | Style/StringLiterals: 97 | Exclude: 98 | - 'test/test_conversions.rb' 99 | 100 | # Offense count: 1 101 | # This cop supports safe autocorrection (--autocorrect). 102 | # Configuration parameters: EnforcedStyle, MinSize, WordRegex. 103 | # SupportedStyles: percent, brackets 104 | Style/WordArray: 105 | Exclude: 106 | - 'pandoc-ruby.gemspec' 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PandocRuby 2 | 3 | [![Build Status](https://img.shields.io/github/actions/workflow/status/xwmx/pandoc-ruby/tests.yml?branch=master)](https://github.com/xwmx/pandoc-ruby/actions) 4 | [![Gem Version](https://img.shields.io/gem/v/pandoc-ruby)](http://rubygems.org/gems/pandoc-ruby) 5 | [![Gem Downloads](https://img.shields.io/gem/dt/pandoc-ruby)](http://rubygems.org/gems/pandoc-ruby) 6 | 7 | PandocRuby is a wrapper for [Pandoc](http://pandoc.org), a 8 | Haskell library with command line tools for converting one markup format to 9 | another. 10 | 11 | Pandoc can convert documents from a variety of formats including markdown, 12 | reStructuredText, textile, HTML, DocBook, LaTeX, and MediaWiki markup to a 13 | variety of other formats, including markdown, reStructuredText, HTML, LaTeX, 14 | ConTeXt, PDF, RTF, DocBook XML, OpenDocument XML, ODT, GNU Texinfo, MediaWiki 15 | markup, groff man pages, HTML slide shows, EPUB, Microsoft Word docx, and more. 16 | 17 | ## Installation 18 | 19 | First, [install Pandoc](https://pandoc.org/installing.html). 20 | 21 | PandocRuby is available on [RubyGems](http://rubygems.org/gems/pandoc-ruby): 22 | 23 | ```bash 24 | gem install pandoc-ruby 25 | ``` 26 | 27 | To install with [Bundler](https://bundler.io/), add the following to your 28 | Gemfile: 29 | 30 | ```ruby 31 | gem 'pandoc-ruby' 32 | ``` 33 | 34 | Then run `bundle install` 35 | 36 | ## Usage 37 | 38 | ```ruby 39 | require 'pandoc-ruby' 40 | @converter = PandocRuby.new('# Markdown Title', from: :markdown, to: :rst) 41 | puts @converter.convert 42 | ``` 43 | 44 | This takes the Markdown formatted string and converts it to reStructuredText. 45 | 46 | You can also use the `#convert` class method: 47 | 48 | ```ruby 49 | puts PandocRuby.convert('# Markdown Title', from: :markdown, to: :html) 50 | ``` 51 | 52 | Other arguments are simply converted into command line options, accepting 53 | symbols and strings for options and hashes for options with arguments. 54 | 55 | ```ruby 56 | PandocRuby.convert('# Markdown Title', :s, {f: :markdown, to: :rst}, '--wrap=none', :table_of_contents) 57 | ``` 58 | 59 | is equivalent to 60 | 61 | ```bash 62 | echo "# Markdown Title" | pandoc -s -f markdown --to=rst --wrap=none --table-of-contents 63 | ``` 64 | 65 | Also provided are `#to_[writer]` instance methods for each of the writers, 66 | and these can also accept options: 67 | 68 | ```ruby 69 | PandocRuby.new('# Example').to_html(:ascii) 70 | # => "

Example

" 71 | # or 72 | PandocRuby.new("# Example").to_rst 73 | # => "Example 74 | # =======" 75 | ``` 76 | 77 | Similarly, there are class methods for each of the readers, so readers 78 | and writers can be specified like this: 79 | 80 | ```ruby 81 | PandocRuby.html("

hello

").to_latex 82 | # => "\\section{hello}" 83 | ``` 84 | 85 | Available readers and writers are can be found in the following 86 | variables: 87 | - [`PandocRuby::READERS`](lib/pandoc-ruby.rb#L10) 88 | - [`PandocRuby::STRING_WRITERS`](lib/pandoc-ruby.rb#L48) 89 | - [`PandocRuby::BINARY_WRITERS`](lib/pandoc-ruby.rb#L104) 90 | - [`PandocRuby::WRITERS`](lib/pandoc-ruby.rb#L113) 91 | 92 | PandocRuby assumes the `pandoc` executable is in your environment's `$PATH` 93 | variable. If you'd like to set an explicit path to the `pandoc` executable, 94 | you can do so with `PandocRuby.pandoc_path = '/path/to/pandoc'` 95 | 96 | ### Converting Files 97 | 98 | PandocRuby can also take an array of one or more file paths as the first 99 | argument. The files will be concatenated together with a blank line between 100 | each and used as input. 101 | 102 | ```ruby 103 | # One file path as a single-element array. 104 | PandocRuby.new(['/path/to/file1.docx'], from: 'docx').to_html 105 | # Multiple file paths as an array. 106 | PandocRuby.new(['/path/to/file1.docx', '/path/to/file1.docx'], from: 'docx').to_html 107 | ``` 108 | 109 | If you are trying to generate a standalone file with full file headers rather 110 | than just a marked up fragment, remember to pass the `:standalone` option so 111 | the correct header and footer are added. 112 | 113 | ```ruby 114 | PandocRuby.new("# Some title", :standalone).to_rtf 115 | ``` 116 | 117 | ### Extensions 118 | 119 | Pandoc [extensions](https://pandoc.org/MANUAL.html#extensions) can be 120 | used to modify the behavior of readers and writers. To use an extension, 121 | add the extension with a `+` or `-` after the reader or writer name: 122 | 123 | ```ruby 124 | # Without extension: 125 | PandocRuby.new("Line 1\n# Heading", from: 'markdown_strict').to_html 126 | # => "

Line 1

\n

Heading

\n" 127 | 128 | # With `+blank_before_header` extension: 129 | PandocRuby.new("Line 1\n# Heading", from: 'markdown_strict+blank_before_header').to_html 130 | # => "

Line 1 # Heading

\n 131 | ``` 132 | 133 | ### More Information 134 | 135 | For more information on Pandoc, see the 136 | [Pandoc documentation](http://pandoc.org) 137 | or run `man pandoc` 138 | ([also available here](https://pandoc.org/MANUAL.html)). 139 | 140 | If you'd prefer a pure-Ruby extended markdown interpreter that can output a 141 | few different formats, take a look at 142 | [kramdown](https://kramdown.gettalong.org/). If you want to use the full 143 | reStructuredText syntax from within Ruby, check out 144 | [RbST](https://github.com/xwmx/rbst), a docutils wrapper. 145 | 146 | This gem was inspired by [Albino](http://github.com/github/albino). For a 147 | slightly different approach to using Pandoc with Ruby, see 148 | [Pandoku](http://github.com/dahlia/pandoku). 149 | 150 | ## Note on Patches/Pull Requests 151 | 152 | * Fork the project. 153 | * Make your feature addition or bug fix. 154 | * Add tests for it. This is important so I don't break it in a 155 | future version unintentionally. 156 | * Commit, do not mess with rakefile, version, or history. 157 | (if you want to have your own version, that is fine but 158 | bump version in a commit by itself I can ignore when I pull) 159 | * Send me a pull request. Bonus points for topic branches. 160 | -------------------------------------------------------------------------------- /test/files/benchmark.txt: -------------------------------------------------------------------------------- 1 | Markdown: Basics 2 | ================ 3 | 4 | 11 | 12 | 13 | Getting the Gist of Markdown's Formatting Syntax 14 | ------------------------------------------------ 15 | 16 | This page offers a brief overview of what it's like to use Markdown. 17 | The [syntax page] [s] provides complete, detailed documentation for 18 | every feature, but Markdown should be very easy to pick up simply by 19 | looking at a few examples of it in action. The examples on this page 20 | are written in a before/after style, showing example syntax and the 21 | HTML output produced by Markdown. 22 | 23 | It's also helpful to simply try Markdown out; the [Dingus] [d] is a 24 | web application that allows you type your own Markdown-formatted text 25 | and translate it to XHTML. 26 | 27 | **Note:** This document is itself written using Markdown; you 28 | can [see the source for it by adding '.text' to the URL] [src]. 29 | 30 | [s]: /projects/markdown/syntax "Markdown Syntax" 31 | [d]: /projects/markdown/dingus "Markdown Dingus" 32 | [src]: /projects/markdown/basics.text 33 | 34 | 35 | ## Paragraphs, Headers, Blockquotes ## 36 | 37 | A paragraph is simply one or more consecutive lines of text, separated 38 | by one or more blank lines. (A blank line is any line that looks like a 39 | blank line -- a line containing nothing spaces or tabs is considered 40 | blank.) Normal paragraphs should not be intended with spaces or tabs. 41 | 42 | Markdown offers two styles of headers: *Setext* and *atx*. 43 | Setext-style headers for `

` and `

` are created by 44 | "underlining" with equal signs (`=`) and hyphens (`-`), respectively. 45 | To create an atx-style header, you put 1-6 hash marks (`#`) at the 46 | beginning of the line -- the number of hashes equals the resulting 47 | HTML header level. 48 | 49 | Blockquotes are indicated using email-style '`>`' angle brackets. 50 | 51 | Markdown: 52 | 53 | A First Level Header 54 | ==================== 55 | 56 | A Second Level Header 57 | --------------------- 58 | 59 | Now is the time for all good men to come to 60 | the aid of their country. This is just a 61 | regular paragraph. 62 | 63 | The quick brown fox jumped over the lazy 64 | dog's back. 65 | 66 | ### Header 3 67 | 68 | > This is a blockquote. 69 | > 70 | > This is the second paragraph in the blockquote. 71 | > 72 | > ## This is an H2 in a blockquote 73 | 74 | 75 | Output: 76 | 77 |

A First Level Header

78 | 79 |

A Second Level Header

80 | 81 |

Now is the time for all good men to come to 82 | the aid of their country. This is just a 83 | regular paragraph.

84 | 85 |

The quick brown fox jumped over the lazy 86 | dog's back.

87 | 88 |

Header 3

89 | 90 |
91 |

This is a blockquote.

92 | 93 |

This is the second paragraph in the blockquote.

94 | 95 |

This is an H2 in a blockquote

96 |
97 | 98 | 99 | 100 | ### Phrase Emphasis ### 101 | 102 | Markdown uses asterisks and underscores to indicate spans of emphasis. 103 | 104 | Markdown: 105 | 106 | Some of these words *are emphasized*. 107 | Some of these words _are emphasized also_. 108 | 109 | Use two asterisks for **strong emphasis**. 110 | Or, if you prefer, __use two underscores instead__. 111 | 112 | Output: 113 | 114 |

Some of these words are emphasized. 115 | Some of these words are emphasized also.

116 | 117 |

Use two asterisks for strong emphasis. 118 | Or, if you prefer, use two underscores instead.

119 | 120 | 121 | 122 | ## Lists ## 123 | 124 | Unordered (bulleted) lists use asterisks, pluses, and hyphens (`*`, 125 | `+`, and `-`) as list markers. These three markers are 126 | interchangable; this: 127 | 128 | * Candy. 129 | * Gum. 130 | * Booze. 131 | 132 | this: 133 | 134 | + Candy. 135 | + Gum. 136 | + Booze. 137 | 138 | and this: 139 | 140 | - Candy. 141 | - Gum. 142 | - Booze. 143 | 144 | all produce the same output: 145 | 146 | 151 | 152 | Ordered (numbered) lists use regular numbers, followed by periods, as 153 | list markers: 154 | 155 | 1. Red 156 | 2. Green 157 | 3. Blue 158 | 159 | Output: 160 | 161 |
    162 |
  1. Red
  2. 163 |
  3. Green
  4. 164 |
  5. Blue
  6. 165 |
166 | 167 | If you put blank lines between items, you'll get `

` tags for the 168 | list item text. You can create multi-paragraph list items by indenting 169 | the paragraphs by 4 spaces or 1 tab: 170 | 171 | * A list item. 172 | 173 | With multiple paragraphs. 174 | 175 | * Another item in the list. 176 | 177 | Output: 178 | 179 |

184 | 185 | 186 | 187 | ### Links ### 188 | 189 | Markdown supports two styles for creating links: *inline* and 190 | *reference*. With both styles, you use square brackets to delimit the 191 | text you want to turn into a link. 192 | 193 | Inline-style links use parentheses immediately after the link text. 194 | For example: 195 | 196 | This is an [example link](http://example.com/). 197 | 198 | Output: 199 | 200 |

This is an 201 | example link.

202 | 203 | Optionally, you may include a title attribute in the parentheses: 204 | 205 | This is an [example link](http://example.com/ "With a Title"). 206 | 207 | Output: 208 | 209 |

This is an 210 | example link.

211 | 212 | Reference-style links allow you to refer to your links by names, which 213 | you define elsewhere in your document: 214 | 215 | I get 10 times more traffic from [Google][1] than from 216 | [Yahoo][2] or [MSN][3]. 217 | 218 | [1]: http://google.com/ "Google" 219 | [2]: http://search.yahoo.com/ "Yahoo Search" 220 | [3]: http://search.msn.com/ "MSN Search" 221 | 222 | Output: 223 | 224 |

I get 10 times more traffic from Google than from Yahoo or MSN.

228 | 229 | The title attribute is optional. Link names may contain letters, 230 | numbers and spaces, but are *not* case sensitive: 231 | 232 | I start my morning with a cup of coffee and 233 | [The New York Times][NY Times]. 234 | 235 | [ny times]: http://www.nytimes.com/ 236 | 237 | Output: 238 | 239 |

I start my morning with a cup of coffee and 240 | The New York Times.

241 | 242 | 243 | ### Images ### 244 | 245 | Image syntax is very much like link syntax. 246 | 247 | Inline (titles are optional): 248 | 249 | ![alt text](/path/to/img.jpg "Title") 250 | 251 | Reference-style: 252 | 253 | ![alt text][id] 254 | 255 | [id]: /path/to/img.jpg "Title" 256 | 257 | Both of the above examples produce the same output: 258 | 259 | alt text 260 | 261 | 262 | 263 | ### Code ### 264 | 265 | In a regular paragraph, you can create code span by wrapping text in 266 | backtick quotes. Any ampersands (`&`) and angle brackets (`<` or 267 | `>`) will automatically be translated into HTML entities. This makes 268 | it easy to use Markdown to write about HTML example code: 269 | 270 | I strongly recommend against using any `` tags. 271 | 272 | I wish SmartyPants used named entities like `—` 273 | instead of decimal-encoded entites like `—`. 274 | 275 | Output: 276 | 277 |

I strongly recommend against using any 278 | <blink> tags.

279 | 280 |

I wish SmartyPants used named entities like 281 | &mdash; instead of decimal-encoded 282 | entites like &#8212;.

283 | 284 | 285 | To specify an entire block of pre-formatted code, indent every line of 286 | the block by 4 spaces or 1 tab. Just like with code spans, `&`, `<`, 287 | and `>` characters will be escaped automatically. 288 | 289 | Markdown: 290 | 291 | If you want your page to validate under XHTML 1.0 Strict, 292 | you've got to put paragraph tags in your blockquotes: 293 | 294 |
295 |

For example.

296 |
297 | 298 | Output: 299 | 300 |

If you want your page to validate under XHTML 1.0 Strict, 301 | you've got to put paragraph tags in your blockquotes:

302 | 303 |
<blockquote>
304 |         <p>For example.</p>
305 |     </blockquote>
306 |     
307 | -------------------------------------------------------------------------------- /lib/pandoc-ruby.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'tempfile' 3 | require 'timeout' 4 | 5 | class PandocRuby 6 | # Use the pandoc command with a custom executable path. 7 | @pandoc_path = 'pandoc' 8 | class << self 9 | attr_accessor :pandoc_path 10 | end 11 | 12 | # The available readers and their corresponding names. The keys are used to 13 | # generate methods and specify options to Pandoc. 14 | READERS = { 15 | 'biblatex' => 'BibLaTeX bibliography', 16 | 'bibtex' => 'BibTeX bibliography', 17 | 'commonmark' => 'CommonMark Markdown', 18 | 'commonmark_x' => 'CommonMark Markdown with extensions', 19 | 'creole' => 'Creole 1.0', 20 | 'csljson' => 'CSL JSON bibliography', 21 | 'csv' => 'CSV table', 22 | 'docbook' => 'DocBook', 23 | 'docx' => 'Word docx', 24 | 'dokuwiki' => 'DokuWiki markup', 25 | 'endnotexml' => 'EndNote XML bibliography', 26 | 'epub' => 'EPUB', 27 | 'fb2' => 'FictionBook2 e-book', 28 | 'gfm' => 'GitHub-Flavored Markdown', 29 | 'haddock' => 'Haddock markup', 30 | 'html' => 'HTML', 31 | 'ipynb' => 'Jupyter notebook', 32 | 'jats' => 'JATS XML', 33 | 'jira' => 'Jira wiki markup', 34 | 'json' => 'JSON version of native AST', 35 | 'latex' => 'LaTex', 36 | 'man' => 'roff man', 37 | 'markdown' => "Pandoc's Markdown", 38 | 'markdown_mmd' => 'MultiMarkdown', 39 | 'markdown_phpextra' => 'PHP Markdown Extra', 40 | 'markdown_strict' => 'original unextended Markdown', 41 | 'mediawiki' => 'MediaWiki markup', 42 | 'muse' => 'Muse', 43 | 'native' => 'native Haskell', 44 | 'odt' => 'ODT', 45 | 'opml' => 'OPML', 46 | 'org' => 'Emacs Org mode', 47 | 'ris' => 'RIS bibliography', 48 | 'rst' => 'reStructuredText', 49 | 'rtf' => 'Rich Text Format', 50 | 't2t' => 'txt2tags', 51 | 'textile' => 'Textile', 52 | 'tikiwiki' => 'TikiWiki markup', 53 | 'tsv' => 'TSV table', 54 | 'twiki' => 'TWiki markup', 55 | 'vimwiki' => 'Vimwiki' 56 | }.freeze 57 | 58 | # The available string writers and their corresponding names. The keys are 59 | # used to generate methods and specify options to Pandoc. 60 | STRING_WRITERS = { 61 | 'asciidoc' => 'AsciiDoc', 62 | 'asciidoctor' => 'AsciiDoctor', 63 | 'beamer' => 'LaTeX beamer slide show', 64 | 'biblatex' => 'BibLaTeX bibliography', 65 | 'bibtex' => 'BibTeX bibliography', 66 | 'chunkedhtml' => 'zip archive of multiple linked HTML files', 67 | 'commonmark' => 'CommonMark Markdown', 68 | 'commonmark_x' => 'CommonMark Markdown with extensions', 69 | 'context' => 'ConTeXt', 70 | 'csljson' => 'CSL JSON bibliography', 71 | 'docbook' => 'DocBook 4', 72 | 'docbook4' => 'DocBook 4', 73 | 'docbook5' => 'DocBook 5', 74 | 'dokuwiki' => 'DokuWiki markup', 75 | 'fb2' => 'FictionBook2 e-book', 76 | 'gfm' => 'GitHub-Flavored Markdown', 77 | 'haddock' => 'Haddock markup', 78 | 'html' => 'HTML, i.e. HTML5/XHTML polyglot markup', 79 | 'html5' => 'HTML, i.e. HTML5/XHTML polyglot markup', 80 | 'html4' => 'XHTML 1.0 Transitional', 81 | 'icml' => 'InDesign ICML', 82 | 'ipynb' => 'Jupyter notebook', 83 | 'jats_archiving' => 'JATS XML, Archiving and Interchange Tag Set', 84 | 'jats_articleauthoring' => 'JATS XML, Article Authoring Tag Set', 85 | 'jats_publishing' => 'JATS XML, Journal Publishing Tag Set', 86 | 'jats' => 'alias for jats_archiving', 87 | 'jira' => 'Jira wiki markup', 88 | 'json' => 'JSON version of native AST', 89 | 'latex' => 'LaTex', 90 | 'man' => 'roff man', 91 | 'markdown' => "Pandoc's Markdown", 92 | 'markdown_mmd' => 'MultiMarkdown', 93 | 'markdown_phpextra' => 'PHP Markdown Extra', 94 | 'markdown_strict' => 'original unextended Markdown', 95 | 'markua' => 'Markua', 96 | 'mediawiki' => 'MediaWiki markup', 97 | 'ms' => 'roff ms', 98 | 'muse' => 'Muse', 99 | 'native' => 'native Haskell', 100 | 'opml' => 'OPML', 101 | 'opendocument' => 'OpenDocument', 102 | 'org' => 'Emacs Org mode', 103 | 'pdf' => 'PDF', 104 | 'plain' => 'plain text', 105 | 'pptx' => 'PowerPoint slide show', 106 | 'rst' => 'reStructuredText', 107 | 'rtf' => 'Rich Text Format', 108 | 'texinfo' => 'GNU Texinfo', 109 | 'textile' => 'Textile', 110 | 'slideous' => 'Slideous HTML and JavaScript slide show', 111 | 'slidy' => 'Slidy HTML and JavaScript slide show', 112 | 'dzslides' => 'DZSlides HTML5 + JavaScript slide show', 113 | 'revealjs' => 'reveal.js HTML5 + JavaScript slide show', 114 | 's5' => 'S5 HTML and JavaScript slide show', 115 | 'tei' => 'TEI Simple', 116 | 'xwiki' => 'XWiki markup', 117 | 'zimwiki' => 'ZimWiki markup' 118 | }.freeze 119 | 120 | # The available binary writers and their corresponding names. The keys are 121 | # used to generate methods and specify options to Pandoc. 122 | BINARY_WRITERS = { 123 | 'odt' => 'OpenOffice text document', 124 | 'docx' => 'Word docx', 125 | 'epub' => 'EPUB v3', 126 | 'epub2' => 'EPUB v2', 127 | 'epub3' => 'EPUB v3' 128 | }.freeze 129 | 130 | # All of the available Writers. 131 | WRITERS = STRING_WRITERS.merge(BINARY_WRITERS) 132 | 133 | # A shortcut method that creates a new PandocRuby object and immediately 134 | # calls `#convert`. Options passed to this method are passed directly to 135 | # `#new` and treated the same as if they were passed directly to the 136 | # initializer. 137 | def self.convert(*args) 138 | new(*args).convert 139 | end 140 | 141 | attr_writer :binary_output 142 | 143 | def binary_output 144 | @binary_output ||= false 145 | end 146 | 147 | attr_writer :options 148 | 149 | def options 150 | @options ||= [] 151 | end 152 | 153 | attr_writer :option_string 154 | 155 | def option_string 156 | @option_string ||= '' 157 | end 158 | 159 | attr_writer :writer 160 | 161 | def writer 162 | @writer ||= 'html' 163 | end 164 | 165 | attr_accessor :input_files 166 | attr_accessor :input_string 167 | 168 | # Create a new PandocRuby converter object. The first argument contains the 169 | # input either as string or as an array of filenames. 170 | # 171 | # Any other arguments will be converted to pandoc options. 172 | # 173 | # Usage: 174 | # new("# A String", :option1 => :value, :option2) 175 | # new(["/path/to/file.md"], :option1 => :value, :option2) 176 | # new(["/to/file1.html", "/to/file2.html"], :option1 => :value) 177 | def initialize(*args) 178 | case args[0] 179 | when String 180 | self.input_string = args.shift 181 | when Array 182 | self.input_files = args.shift.map { |f| "'#{f}'" }.join(' ') 183 | end 184 | 185 | self.options = args 186 | end 187 | 188 | # Run the conversion. The convert method can take any number of arguments, 189 | # which will be converted to pandoc options. If options were already 190 | # specified in an initializer or reader method, they will be combined with 191 | # any that are passed to this method. 192 | # 193 | # Returns a string with the converted content. 194 | # 195 | # Example: 196 | # 197 | # PandocRuby.new("# text").convert 198 | # # => "

text

\n" 199 | def convert(*args) 200 | self.options += args if args 201 | self.option_string = prepare_options(self.options) 202 | 203 | if self.binary_output 204 | convert_binary 205 | else 206 | convert_string 207 | end 208 | end 209 | alias to_s convert 210 | 211 | # Generate class methods for each of the readers in PandocRuby::READERS. 212 | # When one of these methods is called, it simply calls the initializer 213 | # with the `from` option set to the reader key, and returns the object. 214 | # 215 | # Example: 216 | # 217 | # PandocRuby.markdown("# text") 218 | # # => #"markdown"}] 219 | class << self 220 | READERS.each_key do |r| 221 | define_method(r) do |*args| 222 | args += [{ :from => r }] 223 | 224 | new(*args) 225 | end 226 | end 227 | end 228 | 229 | # Generate instance methods for each of the writers in PandocRuby::WRITERS. 230 | # When one of these methods is called, it simply calls the `#convert` method 231 | # with the `to` option set to the writer key, thereby returning the 232 | # converted string. 233 | # 234 | # Example: 235 | # 236 | # PandocRuby.new("# text").to_html 237 | # # => "

text

\n" 238 | WRITERS.each_key do |w| 239 | define_method(:"to_#{w}") do |*args| 240 | args += [{ :to => w.to_sym }] 241 | 242 | convert(*args) 243 | end 244 | end 245 | 246 | private 247 | 248 | # Execute the pandoc command for binary writers. A temp file is created 249 | # and written to, then read back into the program as a string, then the 250 | # temp file is closed and unlinked. 251 | def convert_binary 252 | tmp_file = Tempfile.new('pandoc-conversion') 253 | 254 | begin 255 | self.options += [{ :output => tmp_file.path }] 256 | self.option_string = "#{self.option_string} --output \"#{tmp_file.path}\"" 257 | 258 | execute_pandoc 259 | 260 | return IO.binread(tmp_file) 261 | ensure 262 | tmp_file.close 263 | 264 | tmp_file.unlink 265 | end 266 | end 267 | 268 | # Execute the pandoc command for string writers. 269 | def convert_string 270 | execute_pandoc 271 | end 272 | 273 | # Wrapper to run pandoc in a consistent, DRY way 274 | def execute_pandoc 275 | if !self.input_files.nil? 276 | execute("#{PandocRuby.pandoc_path} #{self.input_files}#{self.option_string}") 277 | else 278 | execute("#{PandocRuby.pandoc_path}#{self.option_string}") 279 | end 280 | end 281 | 282 | # Run the command and returns the output. 283 | def execute(command) 284 | output = error = exit_status = nil 285 | 286 | @timeout ||= 31_557_600 287 | 288 | Open3.popen3(command) do |stdin, stdout, stderr, wait_thr| 289 | begin 290 | Timeout.timeout(@timeout) do 291 | stdin.puts self.input_string 292 | 293 | stdin.close 294 | 295 | output = stdout.read 296 | error = stderr.read 297 | exit_status = wait_thr.value 298 | end 299 | rescue Timeout::Error => ex 300 | Process.kill 9, wait_thr.pid 301 | 302 | maybe_ex = "\n#{ex}" if ex 303 | error = "Pandoc timed out after #{@timeout} seconds.#{maybe_ex}" 304 | end 305 | end 306 | 307 | raise error unless exit_status && exit_status.success? 308 | 309 | output 310 | end 311 | 312 | # Builds the option string to be passed to pandoc by iterating over the 313 | # opts passed in. Recursively calls itself in order to handle hash options. 314 | def prepare_options(opts = []) 315 | opts.inject('') do |string, (option, value)| 316 | string + if value 317 | create_option(option, value) 318 | elsif option.respond_to?(:each_pair) 319 | prepare_options(option) 320 | else 321 | create_option(option) 322 | end 323 | end 324 | end 325 | 326 | # Takes a flag and optional argument, uses it to set any relevant options 327 | # used by the library, and returns string with the option formatted as a 328 | # command line options. If the option has an argument, it is also included. 329 | def create_option(flag, argument = nil) 330 | return '' unless flag 331 | 332 | flag = flag.to_s 333 | set_pandoc_ruby_options(flag, argument) 334 | return '' if flag == 'timeout' # pandoc doesn't accept timeouts yet 335 | 336 | if argument.nil? 337 | format_flag(flag) 338 | else 339 | "#{format_flag(flag)} \"#{argument}\"" 340 | end 341 | end 342 | 343 | # Formats an option flag in order to be used with the pandoc command line 344 | # tool. 345 | def format_flag(flag) 346 | if flag.length == 1 347 | " -#{flag}" 348 | elsif flag =~ /^-|\+/ 349 | " #{flag}" 350 | else 351 | " --#{flag.to_s.tr('_', '-')}" 352 | end 353 | end 354 | 355 | # Takes an option and optional argument and uses them to set any flags 356 | # used by PandocRuby. 357 | def set_pandoc_ruby_options(flag, argument = nil) 358 | case flag 359 | when 't', 'to' 360 | self.writer = argument.to_s 361 | self.binary_output = true if BINARY_WRITERS.key?(self.writer) 362 | when 'timeout' 363 | @timeout = argument 364 | end 365 | end 366 | end 367 | -------------------------------------------------------------------------------- /test/test_pandoc_ruby.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe PandocRuby do 4 | before do 5 | @file = File.join(File.dirname(__FILE__), 'files', 'Test File 1.md') 6 | @file2 = File.join(File.dirname(__FILE__), 'files', 'Test File 2.md') 7 | @string = '# Test String' 8 | @converter = PandocRuby.new(@string, :t => :rst) 9 | end 10 | 11 | after do 12 | PandocRuby.pandoc_path = 'pandoc' 13 | end 14 | 15 | it 'calls bare pandoc when passed no options' do 16 | converter = PandocRuby.new(@string) 17 | 18 | converter.expects(:execute).with('pandoc').returns(true) 19 | 20 | assert converter.convert 21 | end 22 | 23 | it 'converts with altered pandoc_path' do 24 | path = '/usr/bin/env pandoc' 25 | PandocRuby.pandoc_path = path 26 | 27 | converter = PandocRuby.new(@string) 28 | 29 | converter.expects(:execute).with(path).returns(true) 30 | 31 | assert converter.convert 32 | end 33 | 34 | it 'converts input passed as a string' do 35 | assert_equal "

Test String

\n", 36 | PandocRuby.new(@string).to_html 37 | end 38 | 39 | it 'converts single element array input as array of file paths' do 40 | assert PandocRuby.new([@file]).to_html.match(/This is a Title/) 41 | end 42 | 43 | it 'converts multiple element array input as array of file paths' do 44 | assert PandocRuby.new([@file, @file2]).to_html.match(/This is a Title/) 45 | assert PandocRuby.new([@file, @file2]).to_html.match(/A Second Title/) 46 | end 47 | 48 | it 'converts multiple element array input as array of file paths to a binary output format' do 49 | assert PandocRuby.new([@file, @file2]).to_epub.match(/com.apple.ibooks/) 50 | end 51 | 52 | it 'accepts short options' do 53 | @converter.expects(:execute).with('pandoc -t "rst"').returns(true) 54 | 55 | assert @converter.convert 56 | end 57 | 58 | it 'accepts long options' do 59 | converter = PandocRuby.new(@string, :to => :rst) 60 | 61 | converter.expects(:execute).with('pandoc --to "rst"').returns(true) 62 | 63 | assert converter.convert 64 | end 65 | 66 | it 'accepts a variety of options in initializer' do 67 | converter = PandocRuby.new( 68 | @string, 69 | :s, 70 | { :f => :markdown, :to => :rst }, 71 | 'no-wrap' 72 | ) 73 | 74 | converter \ 75 | .expects(:execute) \ 76 | .with('pandoc -s -f "markdown" --to "rst" --no-wrap') \ 77 | .returns(true) 78 | 79 | assert converter.convert 80 | end 81 | 82 | it 'accepts a variety of options in convert' do 83 | converter = PandocRuby.new(@string) 84 | 85 | converter \ 86 | .expects(:execute) \ 87 | .with('pandoc -s -f "markdown" --to "rst" --no-wrap') \ 88 | .returns(true) 89 | 90 | assert converter.convert(:s, { :f => :markdown, :to => :rst }, 'no-wrap') 91 | end 92 | 93 | it 'converts underscore symbol args to hyphenated long options' do 94 | converter = PandocRuby.new( 95 | @string, 96 | { :email_obfuscation => :javascript }, 97 | :table_of_contents 98 | ) 99 | 100 | converter \ 101 | .expects(:execute) \ 102 | .with('pandoc --email-obfuscation "javascript" --table-of-contents') \ 103 | .returns(true) 104 | 105 | assert converter.convert 106 | end 107 | 108 | it 'uses second arg as option' do 109 | converter = PandocRuby.new(@string, 'toc') 110 | 111 | converter.expects(:execute).with('pandoc --toc').returns(true) 112 | 113 | assert converter.convert 114 | end 115 | 116 | it 'passes command line options without modification' do 117 | converter = PandocRuby.new( 118 | @string, 119 | '+RTS', '-M512M', '-RTS', '--to=markdown', '--no-wrap' 120 | ) 121 | 122 | converter.expects(:execute).with( 123 | 'pandoc +RTS -M512M -RTS --to=markdown --no-wrap' 124 | ).returns(true) 125 | 126 | assert converter.convert 127 | end 128 | 129 | it 'supports reader extensions' do 130 | assert_equal( 131 | PandocRuby.convert( 132 | "Line 1\n# Heading", 133 | :from => 'markdown_strict', 134 | :to => 'html' 135 | ), 136 | "

Line 1

\n

Heading

\n" 137 | ) 138 | 139 | assert_equal( 140 | PandocRuby.convert( 141 | "Line 1\n# Heading", 142 | :from => 'markdown_strict+blank_before_header', 143 | :to => 'html' 144 | ), 145 | "

Line 1 # Heading

\n" 146 | ) 147 | end 148 | 149 | it 'supports writer extensions' do 150 | assert_equal( 151 | PandocRuby.convert( 152 | "example\n", 153 | :from => 'html', 154 | :to => 'markdown' 155 | ), 156 | "~example~\n" 157 | ) 158 | 159 | assert_equal( 160 | PandocRuby.convert( 161 | "example\n", 162 | :from => 'html', 163 | :to => 'markdown-subscript' 164 | ), 165 | "example\n" 166 | ) 167 | end 168 | 169 | it 'supports output filenames without spaces' do 170 | Tempfile.create('example') do |file| 171 | PandocRuby.convert( 172 | '# Example', 173 | :from => 'markdown', 174 | :to => 'html', 175 | :output => file.path 176 | ) 177 | 178 | file.rewind 179 | 180 | assert_equal("

Example

\n", file.read) 181 | end 182 | end 183 | 184 | it 'quotes output filenames with spaces' do 185 | Tempfile.create('example with spaces') do |file| 186 | converter = PandocRuby.new( 187 | '# Example', 188 | :from => 'markdown', 189 | :to => 'html', 190 | :output => file.path 191 | ) 192 | 193 | converter \ 194 | .expects(:execute) \ 195 | .with( 196 | "pandoc --from \"markdown\" --to \"html\" --output \"#{file.path}\"" 197 | ).returns(true) 198 | 199 | assert converter.convert 200 | end 201 | end 202 | 203 | it 'outputs to filenames with spaces' do 204 | Tempfile.create('example with spaces') do |file| 205 | PandocRuby.convert( 206 | '# Example', 207 | :from => 'markdown', 208 | :to => 'html', 209 | :output => file.path 210 | ) 211 | 212 | file.rewind 213 | 214 | assert_equal("

Example

\n", file.read) 215 | end 216 | end 217 | 218 | it 'quotes output filenames as Pathname objects' do 219 | Tempfile.create('example with spaces') do |file| 220 | converter = PandocRuby.new( 221 | '# Example', 222 | :from => 'markdown', 223 | :to => 'html', 224 | :output => Pathname.new(file.path) 225 | ) 226 | 227 | converter \ 228 | .expects(:execute) \ 229 | .with( 230 | "pandoc --from \"markdown\" --to \"html\" --output \"#{file.path}\"" 231 | ).returns(true) 232 | 233 | assert converter.convert 234 | end 235 | end 236 | 237 | it 'outputs to filenames as Pathname objects' do 238 | Tempfile.create('example with spaces') do |file| 239 | PandocRuby.convert( 240 | '# Example', 241 | :from => 'markdown', 242 | :to => 'html', 243 | :output => Pathname.new(file.path) 244 | ) 245 | 246 | file.rewind 247 | 248 | assert_equal("

Example

\n", file.read) 249 | end 250 | end 251 | 252 | it 'raises RuntimeError from pandoc executable error' do 253 | assert_raises(RuntimeError) do 254 | PandocRuby.new('# hello', 'badopt').to_html5 255 | end 256 | end 257 | 258 | PandocRuby::READERS.each_key do |r| 259 | it "converts from #{r} with PandocRuby.#{r}" do 260 | converter = PandocRuby.send(r, @string) 261 | 262 | converter.expects(:execute).with("pandoc --from \"#{r}\"").returns(true) 263 | 264 | assert converter.convert 265 | end 266 | end 267 | 268 | PandocRuby::STRING_WRITERS.each_key do |w| 269 | it "converts to #{w} with to_#{w}" do 270 | converter = PandocRuby.new(@string) 271 | 272 | converter \ 273 | .expects(:execute) \ 274 | .with("pandoc --no-wrap --to \"#{w}\"") \ 275 | .returns(true) 276 | 277 | assert converter.send("to_#{w}", :no_wrap) 278 | end 279 | end 280 | 281 | PandocRuby::BINARY_WRITERS.each_key do |w| 282 | it "converts to #{w} with to_#{w}" do 283 | converter = PandocRuby.new(@string) 284 | 285 | converter \ 286 | .expects(:execute) \ 287 | .with(regexp_matches(/^pandoc --no-wrap --to "#{w}" --output /)) \ 288 | .returns(true) 289 | 290 | assert converter.send("to_#{w}", :no_wrap) 291 | end 292 | end 293 | 294 | it 'works with strings' do 295 | converter = PandocRuby.new('## this is a title') 296 | 297 | assert_match(/h2/, converter.convert) 298 | end 299 | 300 | it 'accepts blank strings' do 301 | converter = PandocRuby.new('') 302 | 303 | assert_match("\n", converter.convert) 304 | end 305 | 306 | it 'accepts nil and treats like a blank string' do 307 | converter = PandocRuby.new(nil) 308 | 309 | assert_match("\n", converter.convert) 310 | end 311 | 312 | it 'aliases to_s' do 313 | assert_equal @converter.convert, @converter.to_s 314 | end 315 | 316 | it 'has convert class method' do 317 | assert_equal @converter.convert, PandocRuby.convert(@string, :t => :rst) 318 | end 319 | 320 | it 'runs more than 400 times without error' do 321 | begin 322 | 400.times do 323 | PandocRuby.convert(@string) 324 | end 325 | 326 | assert true 327 | rescue Errno::EMFILE, Errno::EAGAIN => e 328 | flunk e 329 | end 330 | end 331 | 332 | it 'gracefully times out when pandoc hangs due to malformed input' do 333 | skip('Pandoc no longer times out with test file. Determine how to test.') 334 | 335 | file = File.join(File.dirname(__FILE__), 'files', 'bomb.tex') 336 | 337 | contents = File.read(file) 338 | 339 | assert_raises(RuntimeError) do 340 | PandocRuby.convert( 341 | contents, :from => :latex, :to => :html, :timeout => 1 342 | ) 343 | end 344 | end 345 | 346 | it 'has reader and writer constants' do 347 | assert_equal( 348 | PandocRuby::READERS, 349 | 'biblatex' => 'BibLaTeX bibliography', 350 | 'bibtex' => 'BibTeX bibliography', 351 | 'commonmark' => 'CommonMark Markdown', 352 | 'commonmark_x' => 'CommonMark Markdown with extensions', 353 | 'creole' => 'Creole 1.0', 354 | 'csljson' => 'CSL JSON bibliography', 355 | 'csv' => 'CSV table', 356 | 'docbook' => 'DocBook', 357 | 'docx' => 'Word docx', 358 | 'dokuwiki' => 'DokuWiki markup', 359 | 'endnotexml' => 'EndNote XML bibliography', 360 | 'epub' => 'EPUB', 361 | 'fb2' => 'FictionBook2 e-book', 362 | 'gfm' => 'GitHub-Flavored Markdown', 363 | 'haddock' => 'Haddock markup', 364 | 'html' => 'HTML', 365 | 'ipynb' => 'Jupyter notebook', 366 | 'jats' => 'JATS XML', 367 | 'jira' => 'Jira wiki markup', 368 | 'json' => 'JSON version of native AST', 369 | 'latex' => 'LaTex', 370 | 'man' => 'roff man', 371 | 'markdown' => "Pandoc's Markdown", 372 | 'markdown_mmd' => 'MultiMarkdown', 373 | 'markdown_phpextra' => 'PHP Markdown Extra', 374 | 'markdown_strict' => 'original unextended Markdown', 375 | 'mediawiki' => 'MediaWiki markup', 376 | 'muse' => 'Muse', 377 | 'native' => 'native Haskell', 378 | 'odt' => 'ODT', 379 | 'opml' => 'OPML', 380 | 'org' => 'Emacs Org mode', 381 | 'ris' => 'RIS bibliography', 382 | 'rst' => 'reStructuredText', 383 | 'rtf' => 'Rich Text Format', 384 | 't2t' => 'txt2tags', 385 | 'textile' => 'Textile', 386 | 'tikiwiki' => 'TikiWiki markup', 387 | 'tsv' => 'TSV table', 388 | 'twiki' => 'TWiki markup', 389 | 'vimwiki' => 'Vimwiki' 390 | ) 391 | 392 | assert_equal( 393 | PandocRuby::STRING_WRITERS, 394 | 'asciidoc' => 'AsciiDoc', 395 | 'asciidoctor' => 'AsciiDoctor', 396 | 'beamer' => 'LaTeX beamer slide show', 397 | 'biblatex' => 'BibLaTeX bibliography', 398 | 'bibtex' => 'BibTeX bibliography', 399 | 'chunkedhtml' => 'zip archive of multiple linked HTML files', 400 | 'commonmark' => 'CommonMark Markdown', 401 | 'commonmark_x' => 'CommonMark Markdown with extensions', 402 | 'context' => 'ConTeXt', 403 | 'csljson' => 'CSL JSON bibliography', 404 | 'docbook' => 'DocBook 4', 405 | 'docbook4' => 'DocBook 4', 406 | 'docbook5' => 'DocBook 5', 407 | 'dokuwiki' => 'DokuWiki markup', 408 | 'fb2' => 'FictionBook2 e-book', 409 | 'gfm' => 'GitHub-Flavored Markdown', 410 | 'haddock' => 'Haddock markup', 411 | 'html' => 'HTML, i.e. HTML5/XHTML polyglot markup', 412 | 'html5' => 'HTML, i.e. HTML5/XHTML polyglot markup', 413 | 'html4' => 'XHTML 1.0 Transitional', 414 | 'icml' => 'InDesign ICML', 415 | 'ipynb' => 'Jupyter notebook', 416 | 'jats_archiving' => 'JATS XML, Archiving and Interchange Tag Set', 417 | 'jats_articleauthoring' => 'JATS XML, Article Authoring Tag Set', 418 | 'jats_publishing' => 'JATS XML, Journal Publishing Tag Set', 419 | 'jats' => 'alias for jats_archiving', 420 | 'jira' => 'Jira wiki markup', 421 | 'json' => 'JSON version of native AST', 422 | 'latex' => 'LaTex', 423 | 'man' => 'roff man', 424 | 'markdown' => "Pandoc's Markdown", 425 | 'markdown_mmd' => 'MultiMarkdown', 426 | 'markdown_phpextra' => 'PHP Markdown Extra', 427 | 'markdown_strict' => 'original unextended Markdown', 428 | 'markua' => 'Markua', 429 | 'mediawiki' => 'MediaWiki markup', 430 | 'ms' => 'roff ms', 431 | 'muse' => 'Muse', 432 | 'native' => 'native Haskell', 433 | 'opml' => 'OPML', 434 | 'opendocument' => 'OpenDocument', 435 | 'org' => 'Emacs Org mode', 436 | 'pdf' => 'PDF', 437 | 'plain' => 'plain text', 438 | 'pptx' => 'PowerPoint slide show', 439 | 'rst' => 'reStructuredText', 440 | 'rtf' => 'Rich Text Format', 441 | 'texinfo' => 'GNU Texinfo', 442 | 'textile' => 'Textile', 443 | 'slideous' => 'Slideous HTML and JavaScript slide show', 444 | 'slidy' => 'Slidy HTML and JavaScript slide show', 445 | 'dzslides' => 'DZSlides HTML5 + JavaScript slide show', 446 | 'revealjs' => 'reveal.js HTML5 + JavaScript slide show', 447 | 's5' => 'S5 HTML and JavaScript slide show', 448 | 'tei' => 'TEI Simple', 449 | 'xwiki' => 'XWiki markup', 450 | 'zimwiki' => 'ZimWiki markup' 451 | ) 452 | 453 | assert_equal( 454 | PandocRuby::BINARY_WRITERS, 455 | 'odt' => 'OpenOffice text document', 456 | 'docx' => 'Word docx', 457 | 'epub' => 'EPUB v3', 458 | 'epub2' => 'EPUB v2', 459 | 'epub3' => 'EPUB v3' 460 | ) 461 | 462 | assert_equal( 463 | PandocRuby::WRITERS, 464 | PandocRuby::STRING_WRITERS.merge(PandocRuby::BINARY_WRITERS) 465 | ) 466 | end 467 | end 468 | --------------------------------------------------------------------------------