LaTeX 中的时间旅行

LaTeX 中的时间旅行

是否可以定义一个previouspage环境,以便其中的任何内容都会输出到上一页?

动机:在双列文档中,将全宽浮动元素放置在给定页面上的唯一方法是将浮动元素放在上一页。这充其量是尴尬的,因为它将浮动元素的源代码与逻辑上应该在 tex 代码中的位置分开。此外,我有一个应用程序,其中我的 tex 文件是机器生成的,因此这样做是不可能的——生成 tex 代码的程序无法查看 pdf 输出并确定什么内容落在了哪一页上。

可能的实现:我认为这可以通过以下方法实现。使用everyshi在每一页上编写某种钩子代码。在第 17 页,代码可能会显示类似这样的内容\input{page17}previouspage在第 18 页调用环境时,它会生成一个标签,在第一次编译文档后,aux 文件会显示此标签位于第 18 页。当第二次运行 latex 时,我们读取 aux 文件,我们确定 previouspage 环境位于第 18 页,因此我们将 previouspage 环境中的内容写入 page17.tex。在第三次编译时,此代码从 page17.tex 读回。

此实现中的一个复杂之处在于我们希望分页能够收敛到我们想要的结果。为了在初始阶段就接近最终分页,我们需要确保浮动确实显示在文档中,尽管可能晚了一页。无法保证整个过程在三次迭代后甚至在多次迭代后都会收敛到所需的结果。

我的 tex fu 可能不足以完成任何像在纯 latex 中实现上述所有功能一样花哨的事情,所以如果我要自己做这件事,我可能会用其他编程语言编写一个外部脚本来完成部分工作。或者也许这样的东西已经存在了……?

有关的:

IEEEtran:在所需页面上放置两列图形

如何在 LaTeX 中设置方程的位置?

答案1

编辑 -- 在发布下面的代码后,我对其进行了进一步的处理,并消除了一些不美观之处。我没有继续更新这个答案,而是在 github 上创建了一个项目:https://github.com/bcrowell/timetravel

下面是该想法的概念验证实现的代码。

好消息:

  1. 它实现了我想要为这个应用程序做的事情。在下面的示例中,我们有一个两列文档。源代码中的第 2 页有一个浮动的全页宽表格,排版在第 2 页的顶部。

  2. 通常,编译文档三次后,它应该会收敛到一个确定的结果。第四次或后续编译不应导致浮动元素移动到不同的页面。

坏消息是:

  1. 它作为一个单独的 ruby​​ 脚本实现,用于预处理 tex 源代码。

  2. 它不适用于文档的第一页。

  3. 浮动元素插入到目标页面的第一个段落的开头。为了实现这一点,我不得不everyhook在每个段落的开头放置一个钩子。如果目标页面上的第一个段落不是外部段落模式,这会导致错误。为了解决这个问题,任何不是普通段落的材料都必须位于其\prevpagedisable上方和\prevpageenable下方。

坏消息 3 相当糟糕,这也是我认为这只不过是一个概念验证的最重要原因。Donald Aseneau 的 usenet 帖子这表明 latex 代码没有可靠的方法来检测它是否处于外部段落模式。我写这个问题时的最初想法是使用everyshi或来获取必要的钩子eso-pic,但那行不通,因为这些包排版的材料不是处于外部段落模式。

示例 LaTeX 文件:

\documentclass[twocolumn]{article}

\usepackage{prevpage}
\usepackage{lipsum}

\begin{document}

\lipsum[1-13]

% begin-prev-page
\begin{table*}
  \begin{tabular}{p{30mm}p{30mm}p{30mm}p{30mm}}
    John & Paul & George & Ringo
  \end{tabular}
\end{table*}
% end-prev-page

\end{document}

样式文件:

\RequirePackage{everyhook}

% This is a proof-of-concept package that allows us to implement "time travel" in LaTeX
% by causing a float to be invoked on the page before the page on which its source code
% occurs. This can be used in a two-column document to make a full-page-width float
% show up on the same page as the one on which it was invoked.
% http://tex.stackexchange.com/questions/314257/time-travel-in-latex    

\newcounter{prevpageparctr}% a counter that labels each paragraph in the document sequentially

\newcommand{\inputifitexists}[1]{\IfFileExists{#1.tex}{\input{#1}}{}}
\newcommand{\kirk}{\inputifitexists{prev-page/par\theprevpageparctr}}
\newcommand{\spock}{\ifdim\emergencystretch>0pt{}\kirk\fi}
% Use \ifdim\emergencystretch>0pt to attempt to detect whether we're in outer paragraph
% mode. This won't always work, and in fact doesn't actually seem to work.
% http://comp.text.tex.narkive.com/ttqVg20H/test-for-outer-par-mode

\PushPreHook{par}{\stepcounter{prevpageparctr}\label{prevpagepar\theprevpageparctr}}
\newcommand{\prevpageenable}{\PushPreHook{par}{\spock}}
\newcommand{\prevpagedisable}{\PopPreHook{par}}
\prevpageenable

Ruby 代码:

#!/usr/bin/ruby

# usage: prev-page.rb foo.tex bar.tex
# Reads foo.tex, writes the preprocessed version to bar.tex.

require 'fileutils'
require 'digest'
require 'json'

$freeze_at_pass = 3
  # Recompiling more than this many times should not change what pages floats land on.
  # This is normally 3, must be at least 2.

def main()
  debug = false
  in_file = ARGV[0]
  out_file = ARGV[1]
  if in_file.nil? then fatal_error("no input file specified") end
  if out_file.nil? then fatal_error("no output file specified") end
  if !(File.exist?(in_file)) then fatal_error("input file #{in_file} does not exist") end
  aux_file = File.basename(out_file, ".tex") + ".aux"
  $temp_dir = "prev-page" # subdirectory of current working directory
  $pass_file = "#{$temp_dir}/pass" # keep track of which pass we're on

  pass = 1
  if File.exist?(aux_file) then
    if !(File.directory?($temp_dir)) then fatal_error("#{aux_file} exists, but directory #{$temp_dir} doesn't") end
    pass = slurp_or_die($pass_file).to_i
    pass = pass+1
  end
  if pass==1 then # make a clean temporary directory
    FileUtils.rm_rf $temp_dir
    Dir.mkdir($temp_dir)
  end
  File.open($pass_file,'w') { |f|  f.print pass}
  if debug then $stderr.print "pass=#{pass}\n" end

  page_numbers = {}
  if pass>=2 then
    if pass<=$freeze_at_pass then 
      get_page_numbers_from_aux_file(aux_file)
      save_page_numbers
    else
      # Try to make sure it converges rather than oscillating indefinitely.
      $aux_invoked,$aux_par = remember_page_numbers()
    end
  end
  File.open(out_file,'w') { |f_out|
    inside = false # are we currently inside or outside of a % begin-prev-page ... % end-prev-page block?
    line_num = 0
    code = '' # if inside a block, start accumulating a copy of the code here
    File.readlines(in_file).each { |line|
      line_num = line_num+1
      if line=~/\s*%\s*begin-prev-page/ then
        if inside then fatal_error("begin-prev-page twice in a row at line #{line_num}") end
        inside = true
        code = "\\prevpagedisable" # Don't place a hook inside the floating content itself.
      end
      if inside then code = code+line end
      if line=~/\s*%\s*end-prev-page/ then
        if !inside then fatal_error("end-prev-page occurs when not inside a prev-page block at line #{line_num}") end
        inside = false
        key = Digest::MD5.hexdigest(code)
        #$stderr.print "hash=#{key}, code=#{code}=\n"
        if pass==1 then
          code_file = "#{$temp_dir}/#{key}.tex"
          File.open(code_file,'w') { |code_f| code_f.print code+"\n\\prevpageenable" }
        end
        if pass>=2 then
          if !$aux_invoked.key?(key) then fatal_error("aux file #{aux_file} doesn't contain key #{key}") end
          page = $aux_invoked[key]
          if page>1 then page=page-1 end
          if pass==2 then
            par = $aux_par[page]
            File.open("#{$temp_dir}/par#{par}.tex",'a') { |f_page| f_page.print "\\input{prev-page/#{key}}"}
          end
        end
        f_out.print "\\label{prevpageinvoked#{key}}" # This will be immediately followed by the % end-prev-page.
      end
      if pass<2 || !inside then f_out.print line end
           # If pass is 2 or greater, don't duplicate the content of the block.
    }
    if inside then fatal_error("begin-prev-page ended at end of file") end
  }
end

def save_page_numbers
  File.open("#{$temp_dir}/freeze_aux_invoked.json",'w') { |f|
    f.print JSON.generate($aux_invoked)
  }
  File.open("#{$temp_dir}/freeze_aux_par.json",'w') { |f|
    f.print JSON.generate($aux_par)
  }
end

def remember_page_numbers
  return [
    get_json_data_from_file_or_die("#{$temp_dir}/freeze_aux_invoked.json"),
    get_json_data_from_file_or_die("#{$temp_dir}/freeze_aux_par.json")
  ]
end

# Initializes $aux_invoked and $aux_par.
# Lines in aux file look like this: 
#   \newlabel{prevpageinvoked226d375a2efab58c0ff60b659a2b5e70}{{}{2}}
#   \newlabel{prevpagepar14}{{}{2}}
def get_page_numbers_from_aux_file(aux_file)
  $aux_invoked = {} # key=hash, value=page
  $aux_par = {}     # key=page, value=paragraph number
  File.readlines(aux_file).each { |line|
    if line=~/\\newlabel{([^}]+)}{{([^}]*)}{([^}]+)}}/ then
      label,number,page = $1,$2,$3.to_i
      if label=~/\Aprevpage(invoked|par)([^}]*)/ then
        type,key=$1,$2
        if type=="invoked" then $aux_invoked[key]=page end
        if type=="par" then
          if $aux_par.key?(page) then
            if key<$aux_par[page] then $aux_par[page]=key end
          else
            $aux_par[page] = key
          end
        end
      end
    end
  }
end

def fatal_error(message)
  $stderr.print "generate_problems.rb: #{$verb} fatal error: #{message}\n"
  exit(-1)
end

def warning(message)
  $stderr.print "generate_problems.rb: #{$verb} warning: #{message}\n"
end

def get_json_data_from_file_or_die(file)
  r = slurp_file_with_detailed_error_reporting(file)
  if !(r[1].nil?) then fatal_error(r[1]) end
  return parse_json_or_die(r[0])
end

def parse_json_or_die(json)
  begin
    return JSON.parse(json) # use minifier to get rid of comments
  rescue JSON::ParserError
    fatal_error("syntax error in JSON string '#{json}'")
  end
end

# returns contents or nil on error; for more detailed error reporting, see slurp_file_with_detailed_error_reporting()
def slurp_file(file)
  x = slurp_file_with_detailed_error_reporting(file)
  return x[0]
end

def slurp_or_die(file)
  x = slurp_file_with_detailed_error_reporting(file)
  x = x[0]
  if x.nil? then fatal_error("file #{file} not found") end
  return x
end

# returns [contents,nil] normally [nil,error message] otherwise
def slurp_file_with_detailed_error_reporting(file)
  begin
    File.open(file,'r') { |f|
      t = f.gets(nil) # nil means read whole file
      if t.nil? then t='' end # gets returns nil at EOF, which means it returns nil if file is empty
      return [t,nil]
    }
  rescue
    return [nil,"Error opening file #{file} for input: #{$!}."]
  end
end

main()

生成文件:

default:
    make clean
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2

clean:
    rm -f *.aux
    rm -Rf prev-page

相关内容