是否可以定义一个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 中实现上述所有功能一样花哨的事情,所以如果我要自己做这件事,我可能会用其他编程语言编写一个外部脚本来完成部分工作。或者也许这样的东西已经存在了……?
有关的:
答案1
编辑 -- 在发布下面的代码后,我对其进行了进一步的处理,并消除了一些不美观之处。我没有继续更新这个答案,而是在 github 上创建了一个项目:https://github.com/bcrowell/timetravel
下面是该想法的概念验证实现的代码。
好消息:
它实现了我想要为这个应用程序做的事情。在下面的示例中,我们有一个两列文档。源代码中的第 2 页有一个浮动的全页宽表格,排版在第 2 页的顶部。
通常,编译文档三次后,它应该会收敛到一个确定的结果。第四次或后续编译不应导致浮动元素移动到不同的页面。
坏消息是:
它作为一个单独的 ruby 脚本实现,用于预处理 tex 源代码。
它不适用于文档的第一页。
浮动元素插入到目标页面的第一个段落的开头。为了实现这一点,我不得不
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