diff --git a/.rubocop.yml b/.rubocop.yml index fb5e000cd..32de86645 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -427,7 +427,7 @@ Metrics/AbcSize: ### shoud be < 25 Metrics/BlockLength: CountComments: false # count full line comments? - Max: 50 + Max: 62 Exclude: - 'Rakefile' - '**/*.rake' diff --git a/doc/format.ja.md b/doc/format.ja.md index f2f98a376..d9f7c7a0d 100644 --- a/doc/format.ja.md +++ b/doc/format.ja.md @@ -573,9 +573,103 @@ LaTeX の式を挿入するには、`//texequation{ 〜 //}` を使います。 //} ``` -インライン命令では `@{〜}` を使います。インライン命令の式中に「}」を含む場合、`\}` とエスケープする必要があることに注意してください(`{` はエスケープ不要)。 +インライン命令では `@{〜}` を使います。インライン命令の式中に「}」を含む場合、`\}` とエスケープする必要があることに注意してください(`{` はエスケープ不要)。「インライン命令のフェンス記法」も参照してください。 -LaTeX の数式が正常に整形されるかどうかは処理系に依存します。たとえば TeX PDF であれば問題なく利用できるでしょうが、EPUB では MathML 変換を有効にしても妥当な結果にならないことがあります。確実を期すならば、画像で表現するほうが適切です。 +LaTeX の数式が正常に整形されるかどうかは処理系に依存します。LaTeX を利用する PDFMaker では問題なく利用できます。 + +EPUBMaker および WEBMaker では、MathML に変換する方法と、画像化する方法のどちらかを選べます。 + +### MathML の場合 +MathML ライブラリをインストールしておきます(`gem install math_ml`)。 + +さらに config.yml に以下のように指定します。 + +``` +mathml: true +``` + +なお、MathML で正常に表現されるかどうかは、ビューアやブラウザに依存します。 + +### 画像化の場合 + +LaTeX を内部で呼び出し、外部ツールを使って画像化する方法です。画像化された数式は、`images/_review_math` フォルダに配置されます。 + +TeXLive などの LaTeX 環境が必要です。必要に応じて config.yml の `texcommand`、`texoptions`、`dvicommand`、`dvioptions` のパラメータを調整します。 + +さらに、画像化するための外部ツールも用意します。現在、以下の2つのやり方をサポートしています。 + +- `pdfcrop`:TeXLive に収録されている `pdfcrop` コマンドを使用して数式部分を切り出し、さらに PDF から画像化します。デフォルトでは画像化には Poppler ライブラリに収録されている `pdftocairo` コマンドを使用します(コマンドラインで利用可能であれば、別のツールに変更することもできます)。 +- `dvipng`:[dvipng](https://ctan.org/pkg/dvipng) を使用します。OS のパッケージまたは `tlmgr install dvipng` でインストールできます。数式中に日本語は使えません。 + +config.yml で以下のように設定すると、 + +``` +imgmath: true +``` + +デフォルト値として以下が使われます。 + +``` +imgmath_options: + # 使用する画像拡張子。通常は「png」か「svg」(svgの場合は、pdfcrop_pixelize_cmdの-pngも-svgにする) + format: png + # 変換手法。pdfcrop または dvipng + converter: pdfcrop + # プリアンブルの内容を上書きするファイルを指定する(デフォルトはupLaTeX+jsarticle.clsを前提とした、lib/review/makerhelper.rbのdefault_imgmath_preambleメソッドの内容) + preamble_file: null + # 基準のフォントサイズ + fontsize: 10 + # 基準の行間 + lineheight: 12 + # pdfcropコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル + pdfcrop_cmd: "pdfcrop --hires %i %o" + # PDFから画像化するコマンドのコマンドライン。プレースホルダは + # %i: 入力ファイル、%o: 出力ファイル、%O: 出力ファイルから拡張子を除いたもの + # %p: 対象ページ番号 + pdfcrop_pixelize_cmd: "pdftocairo -png -r 90 -f %p -l %p -singlefile %i %O" + # pdfcrop_pixelize_cmdが複数ページの処理に対応していない場合に単ページ化するか + extract_singlepage: null + # 単ページ化するコマンドのコマンドライン + pdfextract_cmd: "pdfjam -q --outfile %o %i %p" + # dvipngコマンドのコマンドライン + dvipng_cmd: "dvipng -T tight -z 9 -p %p -l %p -o %o %i" +``` + +たとえば SVG を利用するには、次のようにします。 + +``` +imgmath: true +imgmath_options: + format: svg + pdfcrop_pixelize_cmd: "pdftocairo -svg -r 90 -f %p -l %p -singlefile %i %o" +``` + +デフォルトでは、pdfcrop_pixelize_cmd に指定するコマンドは、1ページあたり1数式からなる複数ページの PDF のファイル名を `%i` プレースホルダで受け取り、`%p` プレースホルダのページ数に基づいて `%o`(拡張子あり)または `%O`(拡張子なし)の画像ファイルに書き出す、という仕組みになっています。 + +単一のページの処理を前提とする `sips` コマンドや `magick` コマンドを使う場合、入力 PDF から指定のページを抽出するように `extract_singlepage: true` として挙動を変更します。単一ページの抽出はデフォルトで TeXLive の `pdfjam` コマンドが使われます。 + +``` +imgmath: true +imgmath_options: + extract_singlepage: true + # pdfjamの代わりに外部ツールのpdftkを使う場合(Windowsなど) + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + # ImageMagickを利用する例 + pdfcrop_pixelize_cmd: "magick -density 200x200 %i %o" + # sipsを利用する例 + pdfcrop_pixelize_cmd: "sips -s format png --out %o %i" +``` + +Re:VIEW 2 以前の dvipng の設定に合わせるには、次のようにします。 + +``` +imgmath: true +imgmath_options: + converter: dvipng + fontsize: 12 + lineheight: 14.3 +``` ## 字下げの制御 diff --git a/doc/format.md b/doc/format.md index 965ccd54e..d081a1e85 100644 --- a/doc/format.md +++ b/doc/format.md @@ -498,7 +498,6 @@ The rule of finding images is same as image command. //} ``` - ## Quoting Text You can use `//quote{ ... //}` as quotations. @@ -618,6 +617,104 @@ Usage: //} ``` +There is `@{ ... }` for inline (see "Fence notation for inline commands" section also). + +Whether LaTeX formula is correctly displayed or not depends on the processing system. PDFMaker uses LaTeX internally, so there is no problem. + +EPUBMaker and WEBMaker use either MathML transformation or imaging. + +### MathML case +Install MathML library (`gem install math_ml`). + +Specify in config.yml as follows: + +``` +mathml: true +``` + +Whether it is displayed properly in MathML depends on your viewer or browser. + +### imaging case + +This way calls LaTeX internally and images it with an external tool. Image files will be placed in `images/_review_math` folder. + +You need TeXLive or other LaTeX environment. Modify the parameters of `texcommand`,` texoptions`, `dvicommand`,` dvioptions` in config.yml as necessary. + +In addition, external tools for image conversion are also needed. Currently, it supports the following two methods. + +- `pdfcrop`: cut out the formula using `pdfcrop` command (included in TeXLive) and image it. By default, `pdftocairo` command is used (included in Poppler library). You can change it to another tool if available on the command line. +- `dvipng`: it uses [dvipng](https://ctan.org/pkg/dvipng) to cut out and to image. You can install with OS package or `tlmgr install dvipng`. + +By setting in config.yml, + +``` +imgmath: true +``` + +it is set as follows: + +``` +imgmath_options: + # format. png|svg + format: png + # conversion method. pdfcrop|dvipng + converter: pdfcrop + # custom preamble file (default: for upLaTeX+jsarticle.cls, see lib/review/makerhelper.rb#default_imgmath_preamble) + preamble_file: null + # default font size + fontsize: 10 + # default line height + lineheight: 12 + # pdfcrop command. + # %i: filename for input %o: filename for output + pdfcrop_cmd: "pdfcrop --hires %i %o" + # imaging command. + # %i: filename for input %o: filename for output %O: filename for output without the extension + # %p: page number + pdfcrop_pixelize_cmd: "pdftocairo -png -r 90 -f %p -l %p -singlefile %i %O" + # whether to generate a single PDF page for pdfcrop_pixelize_cmd. + extract_singlepage: null + # command line to generate a single PDF page file. + pdfextract_cmd: "pdfjam -q --outfile %o %i %p" + # dvipng command. + dvipng_cmd: "dvipng -T tight -z 9 -p %p -l %p -o %o %i" +``` + +For example, to make SVG: + +``` +imgmath: true +imgmath_options: + format: svg + pdfcrop_pixelize_cmd: "pdftocairo -svg -r 90 -f %p -l %p -singlefile %i %o" +``` + +By default, the command specified in `pdfcrop_pixelize_cmd` takes the filename of multi-page PDF consisting of one formula per page. + +If you want to use the `sips` command or the` magick` command, they can only process a single page, so you need to set `extract_singlepage: true` to extract the specified page from the input PDF. `pdfjam` command (in TeXLive) is used to extract pages. + +``` +imgmath: true +imgmath_options: + extract_singlepage: true + # use pdftk instead of default pdfjam (for Windows) + pdfextract_cmd: "pdftk A=%i cat A%p output %o" + # use ImageMagick + pdfcrop_pixelize_cmd: "magick -density 200x200 %i %o" + # use sips + pdfcrop_pixelize_cmd: "sips -s format png --out %o %i" +``` + +To set the same setting as Re:VIEW 2: + +``` +imgmath: true +imgmath_options: + converter: dvipng + fontsize: 12 + lineheight: 14.3 +``` + ## Spacing `//noindent` is a tag for spacing. diff --git a/lib/review/configure.rb b/lib/review/configure.rb index e1413c8e9..44f4c3ef8 100644 --- a/lib/review/configure.rb +++ b/lib/review/configure.rb @@ -89,6 +89,18 @@ def self.values 'makeindex_dic' => nil, 'makeindex_mecab' => true, 'makeindex_mecab_opts' => '-Oyomi' + }, + 'imgmath_options' => { + 'format' => 'png', + 'converter' => 'pdfcrop', # dvipng | pdfcrop + 'pdfcrop_cmd' => 'pdfcrop --hires %i %o', + 'extract_singlepage' => nil, + 'pdfextract_cmd' => 'pdfjam -q --outfile %o %i %p', + 'preamble_file' => nil, + 'fontsize' => 10, + 'lineheight' => 10 * 1.2, + 'pdfcrop_pixelize_cmd' => 'pdftocairo -png -r 90 -f %p -l %p -singlefile %i %O', + 'dvipng_cmd' => 'dvipng -T tight -z 9 -p %p -l %p -o %o %i' } ] conf.maker = nil diff --git a/lib/review/epubmaker.rb b/lib/review/epubmaker.rb index 632c0d47a..00f843dbd 100644 --- a/lib/review/epubmaker.rb +++ b/lib/review/epubmaker.rb @@ -23,11 +23,13 @@ require 'rexml/streamlistener' require 'epubmaker' require 'review/epubmaker/reviewheaderlistener' +require 'review/makerhelper' module ReVIEW class EPUBMaker include ::EPUBMaker include REXML + include MakerHelper def initialize @producer = nil @@ -103,10 +105,8 @@ def produce(yamlfile, bookname = nil) if @config['debug'] FileUtils.rm_rf(booktmpname) end - math_dir = "./#{@config['imagedir']}/_review_math" - if @config['imgmath'] && Dir.exist?(math_dir) - FileUtils.rm_rf(math_dir) - end + + cleanup_mathimg basetmpdir = build_path begin @@ -125,6 +125,11 @@ def produce(yamlfile, bookname = nil) call_hook('hook_afterbody', basetmpdir) copy_backmatter(basetmpdir) + + math_dir = "./#{@config['imagedir']}/_review_math" + if @config['imgmath'] && File.exist?("#{math_dir}/__IMGMATH_BODY__.tex") + make_math_images(math_dir) + end call_hook('hook_afterbackmatter', basetmpdir) ## push contents in basetmpdir into @producer @@ -267,6 +272,7 @@ def build_body(basetmpdir, yamlfile) book.config = @config @converter = ReVIEW::Converter.new(book, ReVIEW::HTMLBuilder.new) @compile_errors = nil + book.parts.each do |part| if part.name.present? if part.file? diff --git a/lib/review/htmlbuilder.rb b/lib/review/htmlbuilder.rb index 1a29f92cc..222031a79 100644 --- a/lib/review/htmlbuilder.rb +++ b/lib/review/htmlbuilder.rb @@ -549,14 +549,19 @@ def texequation(lines) elsif @book.config['imgmath'] math_str = "\\begin{equation*}\n" + unescape(lines.join("\n")) + "\n\\end{equation*}\n" key = Digest::SHA256.hexdigest(math_str) - math_dir = "./#{@book.config['imagedir']}/_review_math" + math_dir = File.join(@book.config['imagedir'], '_review_math') Dir.mkdir(math_dir) unless Dir.exist?(math_dir) - img_path = "./#{math_dir}/_gen_#{key}.png" - make_math_image(math_str, img_path) - puts %Q() + img_path = File.join(math_dir, "_gen_#{key}.#{@book.config['imgmath_options']['format']}") + if @book.config.check_version('2', exception: false) + make_math_image(math_str, img_path) + puts %Q() + else + defer_math_image(math_str, img_path, key) + puts %Q(#{escape(lines.join(' '))}) + end else print '
'
-        puts lines.join("\n")
+        puts escape(lines.join("\n"))
         puts '
' end puts '' @@ -924,11 +929,16 @@ def inline_m(str) elsif @book.config['imgmath'] math_str = '$' + str + '$' key = Digest::SHA256.hexdigest(str) - math_dir = "./#{@book.config['imagedir']}/_review_math" + math_dir = File.join(@book.config['imagedir'], '_review_math') Dir.mkdir(math_dir) unless Dir.exist?(math_dir) - img_path = "./#{math_dir}/_gen_#{key}.png" - make_math_image(math_str, img_path) - %Q() + img_path = File.join(math_dir, "_gen_#{key}.#{@book.config['imgmath_options']['format']}") + if @book.config.check_version('2', exception: false) + make_math_image(math_str, img_path) + %Q() + else + defer_math_image(math_str, img_path, key) + %Q(#{escape(str)}) + end else %Q(#{escape(str)}) end @@ -1192,7 +1202,19 @@ def olnum(num) @ol_num = num.to_i end + def defer_math_image(str, path, key) + # for Re:VIEW >3 + File.open(File.join(File.dirname(path), '__IMGMATH_BODY__.tex'), 'a+') do |f| + f.puts str + f.puts '\\clearpage' + end + File.open(File.join(File.dirname(path), '__IMGMATH_BODY__.map'), 'a+') do |f| + f.puts key + end + end + def make_math_image(str, path, fontsize = 12) + # Re:VIEW 2 compatibility fontsize2 = (fontsize * 1.2).round.to_i texsrc = <<-EOB \\documentclass[12pt]{article} diff --git a/lib/review/makerhelper.rb b/lib/review/makerhelper.rb index 114346811..dfa16c7be 100644 --- a/lib/review/makerhelper.rb +++ b/lib/review/makerhelper.rb @@ -1,4 +1,4 @@ -# Copyright (c) 2012-2017 Yuto HAYAMIZU, Kenshi Muto +# Copyright (c) 2012-2018 Yuto HAYAMIZU, Kenshi Muto # # This program is free software. # You can distribute or modify this program under the terms of @@ -8,6 +8,7 @@ require 'pathname' require 'fileutils' require 'yaml' +require 'shellwords' module ReVIEW module MakerHelper @@ -64,5 +65,183 @@ def copy_images_to_dir(from_dir, to_dir, options = {}) image_files end module_function :copy_images_to_dir + + def cleanup_mathimg + math_dir = "./#{@config['imagedir']}/_review_math" + if @config['imgmath'] && Dir.exist?(math_dir) + FileUtils.rm_rf(math_dir) + end + end + + def default_imgmath_preamble + <<-EOB +\\documentclass[uplatex]{jsarticle} +\\usepackage[deluxe,uplatex]{otf} +\\usepackage[T1]{fontenc} +\\usepackage{textcomp} +\\usepackage{lmodern} +\\usepackage[dvipdfmx]{graphicx} +\\usepackage[dvipdfmx,table]{xcolor} +\\usepackage[utf8]{inputenc} +\\usepackage{ascmac} +\\usepackage{float} +\\usepackage{alltt} +\\usepackage{amsmath} +\\usepackage{amssymb} +\\usepackage{amsfonts} +\\usepackage{anyfontsize} +\\usepackage{bm} +\\pagestyle{empty} + EOB + end + + def make_math_images(math_dir) + fontsize = @config['imgmath_options']['fontsize'].to_f + lineheight = @config['imgmath_options']['lineheight'].to_f + + texsrc = default_imgmath_preamble + if @config['imgmath_options']['preamble_file'] && File.readable?(@config['imgmath_options']['preamble_file']) + texsrc = File.read(@config['imgmath_options']['preamble_file']) + end + + texsrc << <<-EOB +\\begin{document} +\\fontsize{#{fontsize}}{#{lineheight}}\\selectfont +\\input{__IMGMATH_BODY__} +\\end{document} +EOB + + math_dir = File.realpath(math_dir) + Dir.mktmpdir do |tmpdir| + FileUtils.cp([File.join(math_dir, '__IMGMATH_BODY__.tex'), + File.join(math_dir, '__IMGMATH_BODY__.map')], + tmpdir) + tex_path = File.join(tmpdir, '__IMGMATH__.tex') + File.write(tex_path, texsrc) + + begin + case @config['imgmath_options']['converter'] + when 'pdfcrop' + make_math_images_pdfcrop(tmpdir, tex_path, math_dir) + when 'dvipng' + make_math_images_dvipng(tmpdir, tex_path, math_dir) + else + error "unknown math converter error. imgmath_options/converter parameter should be 'pdfcrop' or 'dvipng'." + end + rescue CompileError + FileUtils.cp([tex_path, + File.join(File.dirname(tex_path), '__IMGMATH__.log')], + math_dir) + error "LaTeX math compile error. See #{math_dir}/__IMGMATH__.log for details." + end + end + FileUtils.rm_f([File.join(math_dir, '__IMGMATH_BODY__.tex'), + File.join(math_dir, '__IMGMATH_BODY__.map')]) + end + + def make_math_images_pdfcrop(dir, tex_path, math_dir) + Dir.chdir(dir) do + dvi_path = '__IMGMATH__.dvi' + pdf_path = '__IMGMATH__.pdf' + out, status = Open3.capture2e(*[@config['texcommand'], @config['texoptions'].shellsplit, tex_path].flatten.compact) + if !status.success? || (!File.exist?(dvi_path) && !File.exist?(pdf_path)) + raise CompileError + end + if File.exist?(dvi_path) + out, status = Open3.capture2e(*[@config['dvicommand'], @config['dvioptions'].shellsplit, dvi_path].flatten.compact) + if !status.success? || !File.exist?(pdf_path) + warn "error in #{@config['dvicommand']}. Error log:\n#{out}" + raise CompileError + end + end + + args = @config['imgmath_options']['pdfcrop_cmd'].shellsplit + args.map! do |m| + m.sub('%i', pdf_path). + sub('%o', '__IMGMATH__pdfcrop.pdf') + end + out, status = Open3.capture2e(*args) + unless status.success? + warn "error in pdfcrop. Error log:\n#{out}" + raise CompileError + end + pdf_path = '__IMGMATH__pdfcrop.pdf' + pdf_path2 = pdf_path + + File.open('__IMGMATH_BODY__.map') do |f| + page = 0 + f.each_line do |key| + page += 1 + key.chomp! + if File.exist?(File.join(math_dir, "_gen_#{key}.#{@config['imgmath_options']['format']}")) + # made already + next + end + + if @config['imgmath_options']['extract_singlepage'] + # if extract_singlepage = true, split each page + args = @config['imgmath_options']['pdfextract_cmd'].shellsplit + + args.map! do |m| + m.sub('%i', pdf_path). + sub('%o', "__IMGMATH__pdfcrop_p#{page}.pdf"). + sub('%O', "__IMGMATH__pdfcrop_p#{page}"). + sub('%p', page.to_s) + end + out, status = Open3.capture2e(*args) + unless status.success? + warn "error in pdf extracting. Error log:\n#{out}" + raise CompileError + end + + pdf_path2 = "__IMGMATH__pdfcrop_p#{page}.pdf" + end + + args = @config['imgmath_options']['pdfcrop_pixelize_cmd'].shellsplit + args.map! do |m| + m.sub('%i', pdf_path2). + sub('%o', File.join(math_dir, "_gen_#{key}.#{@config['imgmath_options']['format']}")). + sub('%O', File.join(math_dir, "_gen_#{key}")). + sub('%p', page.to_s) + end + out, status = Open3.capture2e(*args) + unless status.success? + warn "error in pdf pixelizing. Error log:\n#{out}" + raise CompileError + end + end + end + end + end + + def make_math_images_dvipng(dir, tex_path, math_dir) + Dir.chdir(dir) do + dvi_path = '__IMGMATH__.dvi' + out, status = Open3.capture2e(*[@config['texcommand'], @config['texoptions'].shellsplit, tex_path].flatten.compact) + if !status.success? || !File.exist?(dvi_path) + raise CompileError + end + + File.open('__IMGMATH_BODY__.map') do |f| + page = 0 + f.each_line do |key| + page += 1 + key.chomp! + args = @config['imgmath_options']['dvipng_cmd'].shellsplit + args.map! do |m| + m.sub('%i', dvi_path). + sub('%o', File.join(math_dir, "_gen_#{key}.#{@config['imgmath_options']['format']}")). + sub('%O', File.join(math_dir, "_gen_#{key}")). + sub('%p', page.to_s) + end + out, status = Open3.capture2e(*args) + unless status.success? + warn "error in dvipng. Error log:\n#{out}" + raise CompileError + end + end + end + end + end end end diff --git a/lib/review/webmaker.rb b/lib/review/webmaker.rb index 474b415d0..ee7b6afad 100644 --- a/lib/review/webmaker.rb +++ b/lib/review/webmaker.rb @@ -20,10 +20,12 @@ require 'review/tocprinter' require 'review/version' require 'erb' +require 'review/makerhelper' module ReVIEW class WEBMaker include ERB::Util + include MakerHelper attr_accessor :config, :basedir @@ -71,10 +73,7 @@ def build_path end def remove_old_files(path) - math_dir = "./#{@config['imagedir']}/_review_math" - if @config['imgmath'] && Dir.exist?(math_dir) - FileUtils.rm_rf(math_dir) - end + cleanup_mathimg FileUtils.rm_rf(path) end @@ -116,6 +115,11 @@ def generate_html_files(yamlfile) build_body(@path, yamlfile) copy_backmatter(@path) + math_dir = "./#{@config['imagedir']}/_review_math" + if @config['imgmath'] && File.exist?("#{math_dir}/__IMGMATH_BODY__.tex") + make_math_images(math_dir) + end + copy_images(@config['imagedir'], "#{@path}/#{@config['imagedir']}") copy_resources('covers', "#{@path}/#{@config['imagedir']}") @@ -123,6 +127,12 @@ def generate_html_files(yamlfile) copy_resources(@config['fontdir'], "#{@path}/fonts", @config['font_ext']) end + def clean_mathdir + if @config['imgmath'] && File.exist?("#{@config['imagedir']}/_review_math") + FileUtils.rm_rf("#{@config['imagedir']}/_review_math") + end + end + def build_body(basetmpdir, _yamlfile) base_path = Pathname.new(@basedir) builder = ReVIEW::HTMLBuilder.new diff --git a/test/test_htmlbuilder.rb b/test/test_htmlbuilder.rb index 6f5c373bb..efe0f9d5b 100644 --- a/test/test_htmlbuilder.rb +++ b/test/test_htmlbuilder.rb @@ -1080,7 +1080,7 @@ def test_texequation FileUtils.mkdir_p(File.join(dir, 'images')) expected = <<-EOB
- +p \\land \\bm{P} q
EOB tmpio = $stderr @@ -1096,12 +1096,14 @@ def test_texequation end def test_texequation_fail + # Re:VIEW 3 never fail on defer mode. This test is only for Re:VIEW 2. return true if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM return true unless system('latex -version 1>/dev/null 2>/dev/null') mktmpbookdir('catalog.yml' => "CHAPS:\n - ch01.re\n", 'ch01.re' => "= test\n\n//texequation{\np \\land \\bm{P}} q\n//}\n") do |dir, book, _files| @book = book @book.config = @config + @config['review_version'] = 2 @config['imgmath'] = true @chapter = Book::Chapter.new(@book, 1, '-', nil, StringIO.new) location = Location.new(nil, nil)