Class: TeXzip::Project

Inherits:
HighLine
  • Object
show all
Defined in:
lib/texzip/Project.rb

Defined Under Namespace

Classes: FilePath, Quit

Constant Summary collapse

PACKAGE_EXTENSIONS =
%w(.sty .cls).freeze
IMAGE_EXTENSIONS =
%w(.png .jpg .pdf .eps .pstex).freeze
TEXIMAGE_EXTENSIONS =
%w(.pspdftex .pdf_t .pstex_t).freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(master_file) ⇒ Project

Returns a new instance of Project.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/texzip/Project.rb', line 65

def initialize(master_file)
  super()
  @tex_master_file = Pathname.new(master_file).expand_path

  # All possible include-paths for TeX
  @tex_dirs = [@tex_master_file.dirname]
  @tex_dirs.concat((ENV['TEXINPUTS'] || '').split(':').map {|d| Pathname.new(d)})
  @tex_dirs.map!(&:expand_path)
  @tex_dirs.uniq!

  @overwrite_all = false

  parse_files
end

Instance Attribute Details

#overwrite_allObject

Returns the value of attribute overwrite_all.



17
18
19
# File 'lib/texzip/Project.rb', line 17

def overwrite_all
  @overwrite_all
end

Instance Method Details

#add_bib(bib_file_name) ⇒ Object

Adds a BibTeX-file to the list of included BibTeX-files.

Parameters:

  • image_file_name (String)

    The path of the BibTeX-file.



286
287
288
289
290
291
292
293
294
# File 'lib/texzip/Project.rb', line 286

def add_bib(bib_file_name)
  bib_file = find_file(bib_file_name, ['.bib'])

  if bib_file.nil?
    puts "WARNING: Can't find included BibTeX file #{bib_file_name}"
  else
    @bib_files.add bib_file
  end
end

#add_image(image_file_name) ⇒ Object

Adds an image to the list of included images.

Parameters:

  • image_file_name (String)

    The path of the image-file



269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/texzip/Project.rb', line 269

def add_image(image_file_name)
  ext = File.extname(image_file_name)
  image_files = if ext == ''
                  find_files(image_file_name, IMAGE_EXTENSIONS)
                else
                  [find_file(image_file_name)].compact
                end

  if image_files.empty?
    puts "WARNING: Can't find included image #{image_file_name}"
  else
    @image_files.merge image_files
  end
end

#ask_overwrite(file) ⇒ Object



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/texzip/Project.rb', line 505

def ask_overwrite(file)
  if !@overwrite_all && File.exist?(file)
    ask("File #{file.relative_path_from(Pathname.getwd)} exists. Overwrite? [Ynaq]") do |q|
      q.character = true
      q.validate = /[ynaq\r ]/
      q.case = :down
      q.overwrite = false
      q.answer_type = lambda do |c|
        case c
        when 'q' then raise Quit
        when 'y' then true
        when 'n' then false
        when 'a' then
          @overwrite_all = true
          true
        end
      end
    end
  else
    true
  end
end

#bib_filesArray<Pathname>

Returns a list of included BibTeX-files.

Returns:

  • (Array<Pathname>)

    Included BibTeX files.



137
138
139
# File 'lib/texzip/Project.rb', line 137

def bib_files
  @bib_files.to_a
end

#citesArray<String>

Returns a list of citations.

Returns:

  • (Array<String>)

    Citations.



143
144
145
# File 'lib/texzip/Project.rb', line 143

def cites
  @cites.to_a
end

#filter_bibtexObject



393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/texzip/Project.rb', line 393

def filter_bibtex
  if @bib
    cites = @cites.to_a.map(&:to_s)
    seen_cites = cites.to_set
    until cites.empty?
      cite = cites.pop
      entries = @bib[cite]
      if entries.nil?
        puts "WARNING: Can't find BibTeX-entry #{cite}"
      else
        entries = [entries] unless entries.is_a?(Array)
        entries.each do |entry|
          crossref = entry['crossref']
          if crossref
            crossref.split(',').map(&:strip).each do |ref|
              ref = ref.to_s
              cites << ref if seen_cites.add? ref
            end
          end
        end
      end
    end

    @bib = BibTeX::Bibliography.new.add(@bib.data.select {|entry| seen_cites.include? entry.key.to_s})
  end
end

#find_file(file, extensions = []) ⇒ Pathname?

Returns the full path for a certain file.

The file is searched in the current directory as well as all directories given by the environment variable TEXINPUTS

Parameters:

  • file (String)

    The (relative) path of the file.

  • extensions (Array<String>) (defaults to: [])

    The (possible) file extensions.

Returns:

  • (Pathname, nil)

    The path to the file if exist.



155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/texzip/Project.rb', line 155

def find_file(file, extensions = [])
  extensions = [''] + extensions # the empty extension
  extensions.uniq!

  @tex_dirs.each do |d|
    extensions.each do |ext|
      file_path = d.join(file + ext).expand_path
      return FilePath.new(d, file + ext) if File.file?(file_path)
    end
  end
  nil
end

#find_files(file, extensions) ⇒ Array<Pathname>

Returns the full paths for all variants of a certain file.

The files are searched in the current directory as well as all directories given by the environment variable TEXINPUTS

Parameters:

  • file (String)

    The base file-name.

  • extensions (Array<String>)

    The possible file-extensions.

Returns:

  • (Array<Pathname>)

    All found files.



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/texzip/Project.rb', line 176

def find_files(file, extensions)
  extensions = extensions.uniq

  files = []

  extensions.each do |ext|
    @tex_dirs.each do |d|
      file_path = d.join(file + ext).expand_path
      if file_path.file?
        files << FilePath.new(d, file + ext)
        break
      end
    end
  end

  files
end

#handle_command(command, argument) ⇒ Object

Handles parsed commands.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/texzip/Project.rb', line 230

def handle_command(command, argument)
  case command
  when 'includegraphics'
    add_image argument
  when 'bibliography'
    argument.split(',').uniq.each {|f| add_bib f.strip}
  when 'usepackage'
    return [argument + '.sty']
  when 'documentclass'
    return [argument + '.cls']
  when 'cite'
    @cites.merge argument.split(',').map(&:strip)
  else
    ext = File.extname(argument)
    if TEXIMAGE_EXTENSIONS.include?(ext)
      file = find_file(argument)
      unless file
        puts "WARNING: Can't find tex-image file #{argument}"
        return nil
      end
      dir = File.dirname(argument)
      parse_file file do |command, arg|
        if command == 'includegraphics'
          add_image File.join(dir, arg)
        else
          raise TeXzip::Error, "Unexpected command '\\#{command}' in tex-image file: \\#{argument}"
        end
        nil
      end
    elsif ext != '.tex'
      argument += '.tex'
    end
    return [argument]
  end
  nil
end

#image_filesArray<Pathname>

Returns a list of included image-files.

Returns:

  • (Array<Pathname>)

    Included image files.



131
132
133
# File 'lib/texzip/Project.rb', line 131

def image_files
  @image_files.to_a
end

#master_fileObject

Returns the master-file’s path.



119
120
121
# File 'lib/texzip/Project.rb', line 119

def master_file
  @tex_master_file
end

#modify_files(outdir, image_dir, plain_img, bibtex_file) ⇒ Object



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/texzip/Project.rb', line 296

def modify_files(outdir, image_dir, plain_img, bibtex_file)
  @output_directory = Pathname.new(outdir).expand_path
  @output_image_directory = @output_directory.join(Pathname.new(image_dir)).expand_path
  @output_bibtex = @output_directory.join(Pathname.new(bibtex_file)).expand_path
  @modified_files = {}

  plain_img_files = Hash.new {|h,k| h[k] = []}

  @tex_files.each_key do |file|
    if TEXIMAGE_EXTENSIONS.include? file.extname
      if plain_img
        file.set_plain_output_directory @output_image_directory
        plain_img_files[file.file.to_s] << file
      else
        file.set_output_directory @output_image_directory
      end
    else
      file.set_output_directory @output_directory
    end
  end
  @tex_files.each_pair do |file, text|
    @modified_files[file] = update_file file, text, plain_img
  end

  @image_files.each do |file|
    if plain_img
      file.set_plain_output_directory @output_image_directory
      plain_img_files[file.file.to_s] << file
    else
      file.set_output_directory @output_image_directory
    end
  end

  # maybe rename some files
  plain_img_files.each_pair do |fname, files|
    ext = File.extname(fname)
    next if files.size <= 1
    # TODO: not supported when updating references
    raise 'Multiple images with the same name but different directories'
    cnt = 2
    files.each do |file|
      loop do
        new_fname = fname.basename.sub_ext("#{cnt}#{ext}")
        if plain_img_files.include?(new_fname.to_s)
          cnt += 1
        else
          file.out_file = new_fname
          break
        end
      end
    end
  end

  filter_bibtex
end

#parse_file(file_name, &block) ⇒ Array<String>

Load and parse a single tex-file.

The file is parsed for commands including images, BibTeX-files and citations. The command along with the command’s argument is passed to the block. The block is assumed to return a list of further tex-files to be parsed.

Parameters:

  • file_name (Pathname, String)

    The name of the TeX-file to parse

Returns:

  • (Array<String>)

    A list of included TeX-files.



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/texzip/Project.rb', line 203

def parse_file(file_name, &block)
  text = nil
  File.open(file_name.path, 'rb') do |f|
    text = f.read
  end
  @tex_files[file_name] = text

  block = method(:handle_command) unless block

  included_files = []
  text_without_comments = ''
  text.each_line do |line|
    comment_match = line.match(/(?:\\)*%/)
    if comment_match && (comment_match.end(0) - comment_match.begin(0)).odd?
      line = line[0...comment_match.end(0)]
    end
    text_without_comments.concat line
  end

  text_without_comments.scan(/\\(documentclass|usepackage|include|input|includegraphics|bibliography|cite)(?:\[[^\]]+\])?\{([^}]+)\}/) do |cmd, arg|
    new_files = block.call(cmd, arg)
    included_files.concat(new_files) if new_files
  end
  included_files
end

#parse_filesObject



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/texzip/Project.rb', line 80

def parse_files
  # The hash of all files, including the whole text.
  @tex_files = {}
  @image_files = Set.new
  @bib_files = Set.new
  @cites = Set.new

  # Read all files recursively
  unparsed_files = [@tex_master_file]
  until unparsed_files.empty?
    fname = unparsed_files.pop
    file = find_file(fname)
    if file.nil? then
      if PACKAGE_EXTENSIONS.include?(File.extname(fname))
        next
      else
        raise TeXzip::Error, "Can't find file: #{fname}"
      end
    end

    unless @tex_files.has_key?(file)
      included_files = parse_file file
      unparsed_files.concat included_files
    end
  end

  if !@bib_files.empty?
    @bib = nil
  else
    @bib = BibTeX::Bibliography.new
    @bib_files.each do |bib_file|
      bib = BibTeX.open(bib_file.path)
      bib.replace_strings
      @bib.add(bib.data)
    end
  end
end

#tex_filesArray<Pathname>

Returns a list of included tex and sty files.

Returns:

  • (Array<Pathname>)

    Included tex files.



125
126
127
# File 'lib/texzip/Project.rb', line 125

def tex_files
  @tex_files.keys
end

#update_file(tex_file, text, plain) ⇒ Object



352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
# File 'lib/texzip/Project.rb', line 352

def update_file(tex_file, text, plain)
  ext = tex_file.path.extname

  new_text = ''
  text.each_line do |line|
    comment_match = line.match(/(?:\\)*%/)
    if comment_match && (comment_match.end(0) - comment_match.begin(0)).odd?
      comment = line[comment_match.end(0)..-1]
      line = line[0...comment_match.end(0)]
    else
      comment = ''
    end
    new_line = line.gsub(/(\\(include|input|includegraphics|bibliography)(?:\[[^\]]+\])?)\{([^}]+)\}/) do |m|
      start = $1
      cmd = $2
      file = $3
      if cmd == 'includegraphics'
        if TEXIMAGE_EXTENSIONS.include? ext
          file = File.join(tex_file.file.dirname, file)
        end
        new_file = @output_image_directory.join(Pathname.new(file)).relative_path_from(@output_directory)
        # TODO: this does not support multiple files with same name
        new_file = @output_image_directory.join(new_file.basename).relative_path_from(@output_directory) if plain
      elsif cmd == 'bibliography'
        new_file = @output_bibtex.basename.to_s.gsub(/\.bib$/, '')
      elsif TEXIMAGE_EXTENSIONS.include?(File.extname(file))
        new_file = @output_image_directory.join(Pathname.new(file)).relative_path_from(@output_directory)
        # TODO: this does not support multiple files with same name
        new_file = @output_image_directory.join(new_file.basename).relative_path_from(@output_directory) if plain
      else
        new_file = @output_directory.join(Pathname.new(file)).relative_path_from(@output_directory)
      end
      "#{start}{#{new_file}}"
    end
    new_text.concat new_line
    new_text.concat comment
  end

  new_text
end

#write_archive(archive_file, force = false) ⇒ Object



435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/texzip/Project.rb', line 435

def write_archive(archive_file, force = false)
  require 'ffi-libarchive'

  archive_file = Pathname.new(archive_file).expand_path
  return unless ask_overwrite(archive_file)

  compression = case File.basename(archive_file.to_s)
                when /\.tgz$/, /\.tar\.gz$/
                  :gzip
                when /\.tbz2$/, /\.tar\.bz2$/
                  :bzip2
                when /\.txz$/, /\.tar\.xz$/
                  :xz
                when /\.tlzma$/, /\.tar\.lzma$/
                  :lzma
                when /\.tZ$/, /\.tar\.Z$/
                  :Z
                when /\.tar$/
                  :none
                else
                  raise TeXzip::Error, "Can't derive archive-type from file name #{archive_file}"
                end

  puts "Write archive #{archive_file.relative_path_from(Pathname.getwd)}"
  Archive.write_open_filename archive_file.to_s, compression, :tar do |ar|
    write_data true do |path, data|
      ar.add_entry do |e|
        e.pathname = path.relative_path_from(@output_directory).to_s
        if data.is_a? Pathname
          e.copy_stat(data.to_s)
          File.open(data, 'rb', &:read)
        else
          e.mode = 0644
          e.atime = Time.now
          e.mtime = Time.now
          e.filetype = :file
          data
        end
      end
    end
  end
end

#write_data(force = false) ⇒ Object

Raises:

  • (ArgumentError)


478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/texzip/Project.rb', line 478

def write_data(force = false)
  raise ArgumentError, 'Block required' unless block_given?

  overwrite_all = force
  commands = []

  @modified_files.each_pair do |file, text|
    if force || ask_overwrite(file.output_path)
      commands << [file.output_path, text]
    end
  end

  @image_files.each do |file|
    if force || ask_overwrite(file.output_path)
      commands << [file.output_path, file.path]
    end
  end

  if @bib && (force || ask_overwrite(@output_bibtex))
    commands << [@output_bibtex, @bib.to_s]
  end

  commands.each do |path, data|
    yield(path, data)
  end
end

#write_files(force = false) ⇒ Object



420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/texzip/Project.rb', line 420

def write_files(force = false)
  cwd = Pathname.getwd.expand_path
  write_data do |path, data|
    puts "Write file #{path.relative_path_from(cwd)}"
    FileUtils.mkdir_p path.dirname unless path.dirname.exist?
    if data.is_a? Pathname
      FileUtils.copy data, path
    else
      File.open(path, 'wb') do |f|
        f.write data
      end
    end
  end
end