erbtex 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ \documentclass{article}
2
+
3
+ \begin{document}
4
+
5
+ % This demonstrates that a local variable defined in one chunk of Ruby code
6
+ % can be picked up and used in a later chunk.
7
+
8
+ {: x = 99999 :}
9
+
10
+ \section{Some Pointed Questions}
11
+
12
+ {: 25.times do :}
13
+ \noindent Did you know that
14
+ {: new_x = Math.sqrt(x) :}
15
+ $\sqrt{{:= "%0.10f" % x :}} \approx {:= "%0.10f" % new_x :}$?\par
16
+ {: x = new_x :}
17
+ {: end :}
18
+
19
+ Maybe if you're Rain Man.
20
+
21
+ % The following shows that local variables don't persist across a 'require'
22
+ % statement. Local variables defined in required file disappear on return.
23
+ % Use a global variable $var to communicate between files.
24
+
25
+ {: require './testbind' :}
26
+
27
+ The `required' file set x to 1957, but it still reads {:= x :} in the \LaTeX\
28
+ doc. However, the global variable \$x set to 1957 in the required file is {:=
29
+ $x :} %$
30
+ in the \LaTeX\ file.
31
+ \end{document}
@@ -0,0 +1,160 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ module ErbTeX
4
+ class NoInputFile < StandardError; end
5
+
6
+ class CommandLine
7
+ attr_reader :command_line, :marked_command_line, :input_file
8
+ attr_reader :progname, :input_path, :output_dir, :run_dir
9
+
10
+ def initialize(command_line)
11
+ @command_line = command_line
12
+ @input_file = @marked_command_line = nil
13
+ @run_dir = Dir.pwd
14
+ find_output_dir
15
+ find_progname
16
+ find_input_file
17
+ find_input_path
18
+ mark_command_line
19
+ end
20
+
21
+ def find_progname
22
+ @progname = @command_line.split(' ')[0]
23
+ end
24
+
25
+ def find_output_dir
26
+ args = @command_line.split(' ')
27
+ # There is an -output-comment option, so -output-d is the shortest
28
+ # unambiguous way to write the -output-directory option. It can use
29
+ # one or two dashes at the beginning, and the argument can be
30
+ # seaparated from it with an '=' or white space.
31
+ have_out_dir = false
32
+ out_dir = nil
33
+ args.each do |a|
34
+ if have_out_dir
35
+ # Found -output-directory on last pass without an equals sign
36
+ out_dir = a
37
+ end
38
+ if a =~ /^--?output-d(irectory)?=(\S+)/
39
+ out_dir = $2
40
+ elsif a =~ /^--?output-d(irectory)?$/
41
+ # Next arg is the out_dir
42
+ have_out_dir = true
43
+ end
44
+ end
45
+ if out_dir.nil?
46
+ if File.writable?(Dir.pwd)
47
+ @output_dir = Dir.pwd
48
+ else
49
+ @output_dir = File.expand_path(ENV['TEXMFOUTPUT'])
50
+ end
51
+ else
52
+ @output_dir = File.expand_path(out_dir)
53
+ end
54
+ end
55
+
56
+ def find_input_file
57
+ # Remove the initial command from the command line
58
+ cmd = @command_line.split(/\s+/)[1..-1].join(' ')
59
+ cmd = cmd.gsub(/\s+--?[-a-zA-Z]+(=\S+)?/, ' ')
60
+ infile_re = %r{(\\input\s+)?(([-.~_/A-Za-z0-9]+)(\.[a-z]+)?)\s*$}
61
+ if cmd =~ infile_re
62
+ @input_file = "#{$2}"
63
+ if @input_file =~ /\.tex(\.erb)?$/
64
+ @input_file = @input_file
65
+ else
66
+ @input_file += ".tex"
67
+ end
68
+ elsif cmd =~ %r{(\\input\s+)?(["'])((?:\\?.)*?)\2} #"
69
+ # The re above captures single- or double-quoted strings with
70
+ # the insides in $3
71
+ @input_file = "#{$3}"
72
+ if @input_file !~ /\.tex$/
73
+ @input_file += ".tex#{$1}"
74
+ end
75
+ else
76
+ @input_file = nil
77
+ end
78
+ end
79
+
80
+ def find_input_path
81
+ # If input_file is absolute, don't look further
82
+ if @input_file =~ /^\//
83
+ @input_path = @input_file
84
+ elsif @input_file.nil?
85
+ @input_path = nil
86
+ else
87
+ # The following cribbed from kpathsea.rb
88
+ @progname.untaint
89
+ @input_file.untaint
90
+ kpsewhich = "kpsewhich -progname=\"#{@progname}\" -format=\"tex\" \"#{@input_file}\""
91
+ lines = ""
92
+ IO.popen(kpsewhich) do |io|
93
+ lines = io.readlines
94
+ end
95
+ if $? == 0
96
+ @input_path = lines[0].chomp.untaint
97
+ else
98
+ raise NoInputFile, "Can't find #{@input_file} in TeX search path; try kpsewhich -format=tex #{@input_file}."
99
+ end
100
+ end
101
+ end
102
+
103
+ def new_command_line(new_progname, new_infile)
104
+ ncl = @marked_command_line.sub('^p^', new_progname)
105
+ # Quote the new_infile in case it has spaces
106
+ if new_infile
107
+ ncl = ncl.sub('^f^', "'#{new_infile}'")
108
+ end
109
+ ncl
110
+ end
111
+
112
+ def mark_command_line
113
+ # Replace input file with '^f^'
114
+ infile_re = %r{(\\input\s+)?(([-.~_/A-Za-z0-9]+)(\.[a-z]+)?)\s*$}
115
+ quoted_infile_re = %r{(\\input\s+)?(["'])((?:\\?.)*?)\2} #"
116
+ if @input_file.nil?
117
+ @marked_command_line = @command_line
118
+ elsif @command_line =~ infile_re
119
+ @marked_command_line = @command_line.sub(infile_re, "#{$1}^f^")
120
+ elsif @command_line =~ quoted_infile_re
121
+ @marked_command_line = @command_line.sub(quoted_infile_re, "#{$1}^f^")
122
+ else
123
+ @marked_command_line = @command_line
124
+ end
125
+ # Replace progname with '^p^'
126
+ @marked_command_line = @marked_command_line.lstrip
127
+ @marked_command_line = @marked_command_line.sub(/\S+/, '^p^')
128
+ end
129
+ end
130
+ end
131
+
132
+ # NOTES:
133
+
134
+ # The following text is from the Web2C documentation at
135
+ # http://tug.org/texinfohtml/web2c.html#Output-file-location
136
+ #
137
+ # 3.4 Output file location
138
+ #
139
+ # All the programs generally follow the usual convention for output
140
+ # files. Namely, they are placed in the directory current when the
141
+ # program is run, regardless of any input file location; or, in a few
142
+ # cases, output is to standard output.
143
+
144
+ # For example, if you run ‘tex /tmp/foo’, for example, the output will
145
+ # be in ./foo.dvi and ./foo.log, not /tmp/foo.dvi and /tmp/foo.log.
146
+
147
+ # You can use the ‘-output-directory’ option to cause all output files
148
+ # that would normally be written in the current directory to be written
149
+ # in the specified directory instead. See Common options.
150
+
151
+ # If the current directory is not writable, and ‘-output-directory’ is
152
+ # not specified, the main programs (TeX, Metafont, MetaPost, and BibTeX)
153
+ # make an exception: if the config file or environment variable value
154
+ # TEXMFOUTPUT is set (it is not by default), output files are written to
155
+ # the directory specified.
156
+
157
+ # TEXMFOUTPUT is also checked for input files, as TeX often generates
158
+ # files that need to be subsequently read; for input, no suffixes (such
159
+ # as ‘.tex’) are added by default and no exhaustive path searching is
160
+ # done, the input name is simply checked as given.
@@ -0,0 +1,41 @@
1
+ module ErbTeX
2
+ # Find the first executable file in the PATH that is the same
3
+ # basename, but not the same absolute name as calling_prog. If this
4
+ # program has been linked to the name pdflatex, for example, and is
5
+ # located in ~/bin/pdflatex, this function will take '~/bin/pdflatex'
6
+ # as it parameter, expand it to /home/ded/pdflatex, then walk through
7
+ # the PATH looking for an executable with the same basename, pdflatex,
8
+ # but not the same absolute name /home/ded/bin/pdflatex.
9
+ #
10
+ # This allows us to make several symlinks to our erbtex program with
11
+ # the name of the actual program we want to invoke. So our link
12
+ # version of pdflatex will know to invoke the *real* pdflatex in
13
+ # /usr/bin/pdflatex after we've done the pre-processing. Also, other
14
+ # programs that want to invoke pdflatex will still work, except that
15
+ # we'll sneak in and do ruby pre-processing before invoking the real
16
+ # program.
17
+
18
+ # If the calling program is 'erbtex', treat it as 'pdflatex' just as
19
+ # if it were a pdflatex link to erbtex
20
+
21
+ def ErbTeX.find_executable(calling_prog)
22
+ calling_prog = File.absolute_path(calling_prog)
23
+ call_path = File.dirname(calling_prog)
24
+ call_base = File.basename(calling_prog).sub(/^erbtex$/, 'pdflatex')
25
+ executable = nil
26
+ ENV['PATH'].split(':').each do |p|
27
+ next unless File.directory?(p)
28
+ next if File.absolute_path(p) == call_path
29
+ Dir.chdir(p) do
30
+ Dir.glob(call_base).each do |f|
31
+ if system("file -L #{f} | grep -q ELF")
32
+ executable = File.join(p, f)
33
+ break
34
+ end
35
+ end
36
+ end
37
+ break if executable
38
+ end
39
+ executable
40
+ end
41
+ end
@@ -0,0 +1,108 @@
1
+ require 'tempfile'
2
+ require 'pathname'
3
+
4
+ module ErbTeX
5
+ # When we are handed a command line, it will be one that was
6
+ # originally intended for the real tex processor, e.g., pdflatex.
7
+ #
8
+ # We want to find the intended input file and the intended output
9
+ # directory using the ErbTeX::CommandLine object.
10
+ #
11
+ # We want to process the intended input file with Erubis and save the
12
+ # output in a temporary file with an .etx extension.
13
+ #
14
+ # Write the .etx file to the current directory unless it is not
15
+ # writable, in which case write it to /tmp.
16
+ #
17
+ # Perhaps change the Erubis pattern to something like .{ }. so that
18
+ # AucTeX does not get confused with the comment character used in
19
+ # Erubis by default (<%= %>). Erubis -p commandline would use the
20
+ # switch -p '\.{ }\.' But adapt if old pattern style is found in the
21
+ # input.
22
+ #
23
+ # If there are no Erubis patterns in the file, skip the Erubis phase
24
+ # and just pass the original command on to the system.
25
+ #
26
+ # But wait. What if there are \include{file} or \input file
27
+ # statements in the input and those have Erubis patterns in them? We
28
+ # have to invoke erbtex recursively on those, replacing the
29
+ # orginal with a processed temporary and patching up the
30
+ # \include{tmp-file}, and so on.
31
+ #
32
+ # If there is an error in the Erubis phase, we want the error message
33
+ # to make it clear what happened and exit without invoking the tex
34
+ # processor.
35
+ #
36
+ # We want to find the real tex processor with find_executable and run it
37
+ # on our processed .etx file and otherwise leave the commandline
38
+ # intact.
39
+ #
40
+ def ErbTeX.run(command)
41
+ cl = CommandLine.new(command)
42
+ Dir.chdir(cl.run_dir) do
43
+ if cl.input_file
44
+ new_infile = process(cl.input_file, cl.input_path)
45
+ else
46
+ new_infile = nil
47
+ end
48
+ if new_infile
49
+ new_infile = Pathname.new(new_infile).
50
+ relative_path_from(Pathname.new(cl.run_dir))
51
+ new_progname = ErbTeX.find_executable(command.lstrip.split(' ')[0])
52
+ cmd = cl.new_command_line(new_progname, new_infile)
53
+ cmd.sub!('\\', '\\\\\\')
54
+ puts "Executing: #{cmd}"
55
+ system(cmd)
56
+ end
57
+ end
58
+ end
59
+
60
+ # Run erbtex on the content of file_name, a String, and return the
61
+ # name of the file where the processed content can be found. This
62
+ # could be the orignal file name if no processing was needed, or a
63
+ # temporary file if the erubis pattern is found anywhere in the file.
64
+ def ErbTeX.process(file_name, dir)
65
+ puts "Input path: #{dir}"
66
+ contents = nil
67
+ File.open(file_name) do |f|
68
+ contents = f.read
69
+ end
70
+ # TODO: recurse through any \input or \include commands
71
+
72
+ # Add current directory to LOAD_PATH
73
+ $: << '.' unless $:.include?('.')
74
+
75
+ if ENV['ERBTEX_PATTERN']
76
+ pat = ENV['ERBTEX_PATTERN']
77
+ else
78
+ pat = '{: :}'
79
+ end
80
+
81
+ # Otherwise process the contents
82
+ # Find a writable directory, prefering the one the input file came
83
+ # from, or the current directory, and a temp file as a last resort.
84
+ file_absolute = File.absolute_path(File.expand_path(file_name))
85
+ file_dir = File.dirname(file_absolute)
86
+ if file_absolute =~ /\.tex\.erb$/
87
+ file_base = File.basename(file_absolute, '.tex.erb')
88
+ else
89
+ file_base = File.basename(file_absolute, '.tex')
90
+ end
91
+ of = nil
92
+ if File.writable?(file_dir)
93
+ out_file = file_dir + '/' + file_base + '.etx'
94
+ elsif File.writable?('.')
95
+ out_file = './' + file_base + '.etx'
96
+ else
97
+ of = Tempfile.new([File.basename(file_name), '.etx'])
98
+ out_file = of.path
99
+ end
100
+ unless of
101
+ of = File.open(out_file, 'w+')
102
+ end
103
+ er = Erubis::Eruby.new(contents, :pattern => pat)
104
+ of.write(er.result)
105
+ of.close
106
+ out_file
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ module ErbTeX
2
+ VERSION = "0.2.0"
3
+ end
data/lib/erbtex.rb ADDED
@@ -0,0 +1,10 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require 'erubis'
4
+
5
+ require 'erbtex/version'
6
+ require 'erbtex/command_line'
7
+ require 'erbtex/find_binary'
8
+ require 'erbtex/runner'
9
+
10
+ require 'byebug'
@@ -0,0 +1,181 @@
1
+ require 'test_helper'
2
+
3
+ class CommandLineTest < Test::Unit::TestCase
4
+ include ErbTeX
5
+
6
+ def setup
7
+ @test_dir = File.dirname(File.absolute_path(__FILE__))
8
+ @junk_tex = @test_dir + '/junk.tex'
9
+ FileUtils.touch(@junk_tex)
10
+
11
+ @tex_dir = @test_dir + '/tex_dir'
12
+ FileUtils.mkdir(@tex_dir) unless File.exists?(@tex_dir)
13
+ @junk2_tex = @tex_dir + '/junk2.tex'
14
+ FileUtils.touch(@junk2_tex)
15
+
16
+ @tex_dir_nw = @test_dir + '/tex_dir_nw'
17
+ FileUtils.mkdir(@tex_dir_nw) unless File.exists?(@tex_dir_nw)
18
+ @junk3_tex = @tex_dir_nw + '/junk3.tex'
19
+ FileUtils.touch(@junk3_tex)
20
+ FileUtils.chmod(0500, @tex_dir_nw)
21
+
22
+ @junk4_tex = File.expand_path('~/junk.tex')
23
+ FileUtils.touch(@junk4_tex)
24
+ end
25
+
26
+ def teardown
27
+ FileUtils.rm(@junk_tex)
28
+ FileUtils.rm(@junk4_tex)
29
+ FileUtils.rm_rf(@tex_dir)
30
+ FileUtils.chmod(0700, @tex_dir_nw)
31
+ FileUtils.rm_rf(@tex_dir_nw)
32
+ end
33
+
34
+ def test_find_ordinary_input_file
35
+ cl = 'pdflatex -ini --halt-on-error junk'
36
+ assert_equal("junk.tex",
37
+ CommandLine.new(cl).input_file)
38
+ end
39
+
40
+ def test_find_input_file_relative
41
+ cl = 'pdflatex -ini --halt-on-error ./junk.tex'
42
+ assert_equal("./junk.tex",
43
+ CommandLine.new(cl).input_file)
44
+ end
45
+
46
+ def test_find_input_file_relative_no_ext
47
+ cl = 'pdflatex -ini --halt-on-error ./junk'
48
+ assert_equal("./junk.tex",
49
+ CommandLine.new(cl).input_file)
50
+ end
51
+
52
+ def test_find_ordinary_input_file_with_ext
53
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
54
+ assert_equal("junk.tex",
55
+ CommandLine.new(cl).input_file)
56
+ end
57
+
58
+ def test_find_ordinary_input_file_with_spaces
59
+ fn = 'A junk.tex'
60
+ FileUtils.touch(fn)
61
+ cl = "pdflatex -ini --halt-on-error \'#{fn}\'"
62
+ assert_equal("A junk.tex",
63
+ CommandLine.new(cl).input_file)
64
+ FileUtils.rm(fn)
65
+ end
66
+
67
+ def test_no_input_file
68
+ assert_raise ErbTeX::NoInputFile do
69
+ cl = 'pdflatex -ini'
70
+ CommandLine.new(cl)
71
+ end
72
+ end
73
+
74
+ def test_no_input_file_with_eq
75
+ assert_raise ErbTeX::NoInputFile do
76
+ cl = 'pdflatex -ini -output-directory=/tmp'
77
+ CommandLine.new(cl).input_file
78
+ end
79
+ end
80
+
81
+ def test_find_progname
82
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
83
+ assert_equal("pdflatex",
84
+ CommandLine.new(cl).progname)
85
+ end
86
+
87
+ def test_mark_command_line
88
+ cl = 'pdflatex -ini --halt-on-error junk'
89
+ clm = '^p^ -ini --halt-on-error ^f^'
90
+ assert_equal(clm,
91
+ CommandLine.new(cl).marked_command_line)
92
+ end
93
+
94
+ def test_mark_command_line_with_ext
95
+ cl = 'pdflatex -ini --halt-on-error junk.tex'
96
+ clm = '^p^ -ini --halt-on-error ^f^'
97
+ assert_equal(clm,
98
+ CommandLine.new(cl).marked_command_line)
99
+ end
100
+
101
+ def test_mark_command_line_with_dir
102
+ cl = 'pdflatex -ini --halt-on-error ~/junk.tex'
103
+ clm = '^p^ -ini --halt-on-error ^f^'
104
+ assert_equal(clm,
105
+ CommandLine.new(cl).marked_command_line)
106
+ end
107
+
108
+ def test_mark_command_line_with_spaces
109
+ cl = 'pdflatex -ini --halt-on-error \'/home/ded/A junk.tex\''
110
+ clm = '^p^ -ini --halt-on-error ^f^'
111
+ assert_equal(clm,
112
+ CommandLine.new(cl).marked_command_line)
113
+ end
114
+
115
+ def test_find_embedded_input_file
116
+ cl = 'pdflatex -ini --halt-on-error \input junk'
117
+ assert_equal("junk.tex",
118
+ CommandLine.new(cl).input_file)
119
+ end
120
+
121
+ def test_find_embedded_input_file_with_ext
122
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
123
+ assert_equal("junk.tex",
124
+ CommandLine.new(cl).input_file)
125
+ end
126
+
127
+ def test_find_input_file_with_relative
128
+ cl = 'pdflatex -ini --halt-on-error \input tex_dir/junk2.tex'
129
+ assert_equal("tex_dir/junk2.tex",
130
+ CommandLine.new(cl).input_file)
131
+ end
132
+
133
+ def test_find_input_file_with_spaces
134
+ fn = "my junk2.tex"
135
+ FileUtils.touch(fn)
136
+ cl = "pdflatex -ini --halt-on-error \input \"#{fn}\""
137
+ assert_equal(fn,
138
+ CommandLine.new(cl).input_file)
139
+ FileUtils.rm(fn)
140
+ end
141
+
142
+ def test_find_input_path_existing
143
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
144
+ assert_equal("./junk.tex",
145
+ CommandLine.new(cl).input_path)
146
+ end
147
+
148
+ def test_dont_find_input_path_non_existing
149
+ cl = 'pdflatex -ini --halt-on-error \input junk3.tex'
150
+ assert_raise NoInputFile do
151
+ CommandLine.new(cl).input_path
152
+ end
153
+ end
154
+
155
+ def test_find_full_path_with_env
156
+ save_env = ENV['TEXINPUTS']
157
+ ENV['TEXINPUTS'] = File.dirname(__FILE__) + '/tex_dir'
158
+ cl = 'pdflatex -ini --halt-on-error \input junk2.tex'
159
+ assert_equal("./tex_dir/junk2.tex",
160
+ CommandLine.new(cl).input_path)
161
+ ENV['TEXINPUTS'] = save_env
162
+ end
163
+
164
+ def test_find_output_dir
165
+ cl = 'pdflatex -ini --halt-on-error \input junk.tex'
166
+ assert_equal(File.expand_path('./'), CommandLine.new(cl).output_dir)
167
+ end
168
+
169
+ def test_find_output_dir_with_options
170
+ cl = 'pdflatex -ini -output-d=~/tmp \input junk.tex'
171
+ assert_equal(File.expand_path('~/tmp'), CommandLine.new(cl).output_dir)
172
+ end
173
+
174
+ def test_find_output_dir_with_non_writable_pwd
175
+ cl = 'pdflatex -ini \input junk3.tex'
176
+ ENV['TEXMFOUTPUT'] = File.expand_path(File.dirname(__FILE__) + '/tex_dir')
177
+ Dir.chdir('tex_dir_nw') do
178
+ assert_equal(File.expand_path('../tex_dir'), CommandLine.new(cl).output_dir)
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,44 @@
1
+ require 'test_helper'
2
+
3
+ class FindBinaryTest < Test::Unit::TestCase
4
+ include ErbTeX
5
+
6
+ # Here we set up the situation as we expect it to be after
7
+ # installation. There is a "real" pdflatex executable binary and
8
+ # there is one that is just a link to our script, the "fake" binary.
9
+ # The fake binary is earlier in PATH than the real binary, and we want
10
+ # this function, when fed the name of the fake binary to deduce the
11
+ # name of the real binary.
12
+ def setup
13
+ # Create a "fake" ruby script named pdflatex
14
+ @fake_dir = File.dirname(File.absolute_path(__FILE__)) + '/fake_bin'
15
+ FileUtils.mkdir(@fake_dir) unless File.exist?(@fake_dir)
16
+ @fake_binary = @fake_dir + '/pdflatex'
17
+ @erbtex = @fake_dir + '/erbtex'
18
+ FileUtils.touch(@erbtex)
19
+ FileUtils.chmod(0700, @erbtex)
20
+ FileUtils.rm_rf(@fake_binary) if File.exists?(@fake_binary)
21
+ FileUtils.ln_s(@erbtex, @fake_binary)
22
+
23
+ # Point to "real" pdflatex to find
24
+ @real_binary = '/usr/bin/pdflatex'
25
+ @real_dir = '/usr/bin'
26
+
27
+ # Put the fake dir on the PATH before the real dir
28
+ ENV['PATH'] = @fake_dir + ':' + @real_dir + ':' + ENV['PATH']
29
+ end
30
+
31
+ def teardown
32
+ FileUtils.rm_rf(@fake_dir)
33
+ end
34
+
35
+ def test_find_pdflatex
36
+ assert_equal(@real_binary,
37
+ ErbTeX.find_executable(@fake_binary))
38
+ end
39
+
40
+ def test_find_pdflatex_with_erbtex
41
+ assert_equal(@real_binary,
42
+ ErbTeX.find_executable(@erbtex))
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+
2
+ # Set up load path for tests.
3
+
4
+ lib_dir = File.dirname(__FILE__) + '/../lib'
5
+ $:.unshift lib_dir unless $:.include?(lib_dir)
6
+
7
+ require 'test/unit'
8
+ require 'erbtex'
9
+ require 'fileutils'
10
+