通过 lualatex 访问精确的分页符

通过 lualatex 访问精确的分页符

我有一个期刊布局,其中第一页(包含文章标题)采用奇数 1.5 列布局。包含元数据的第一列比包含正文前几段的右列要窄。从第二页开始,布局采用均匀的两列布局,两列大小相同。

我通过定义一个宏来实现这一点\@output@first@dblcol,该宏为第一页分叉 LaTeX 内核,并使用包中的钩子\@outputdblcol在标题页之后切换回内核定义,除非作者/编辑手动设置了分页符。\AtBeginShipoutNextatbegshi

MWE 的整个源代码是:

\documentclass[twocolumn]{article}
\RequirePackage{lipsum}
\RequirePackage{atbegshi}

\textwidth124mm
\columnwidth60mm
\columnsep4mm

\author{Max Mustermann*, John Doe}
\title{Test document with a longer title}

\makeatletter
\let\ltx@outputdblcol\@outputdblcol
\def\@output@first@dblcol{%
  \if@firstcolumn
    \global\@firstcolumnfalse
    \global\setbox\@leftcolumn\copy\@outputbox
    \splitmaxdepth\maxdimen
    \vbadness\maxdimen
     \setbox\@outputbox\vbox{\unvbox\@outputbox\unskip}%
     \setbox\@outputbox\vsplit\@outputbox to\maxdimen
    \toks@\expandafter{\topmark}%
    \xdef\@firstcoltopmark{\the\toks@}%
    \toks@\expandafter{\splitfirstmark}%
    \xdef\@firstcolfirstmark{\the\toks@}%
    \ifx\@firstcolfirstmark\@empty
      \global\let\@setmarks\relax
    \else
      \gdef\@setmarks{%
        \let\firstmark\@firstcolfirstmark
        \let\topmark\@firstcoltopmark}%
    \fi
  \else
    \global\@firstcolumntrue
    \setbox\@outputbox\vbox{%
     \hb@xt@\textwidth{%
        \hb@xt@40mm{\box\@leftcolumn \hss}%
        {\normalcolor\vrule \@width\columnseprule}%
        \hfil
        \hb@xt@80mm{\box\@outputbox \hss}}}%
  \@combinedblfloats
    \@setmarks
    \@outputpage
    \begingroup
      \@dblfloatplacement
      \@startdblcolumn
      \@whilesw\if@fcolmade \fi{\@outputpage
     \@startdblcolumn}%
    \endgroup
  \fi}%

\def\maketitle{%
  \let\is@firstpage\relax
  \let\@outputdblcol\@output@first@dblcol
  \twocolumn[{\bfseries\large\@author}\par\vskip10bp{\LARGE\bfseries\sffamily\@title}\vskip18bp]%
  \par
  \bgroup
    \raggedright
    \hsize40mm
    \parindent\z@
    \textbf{Email:} [email protected]\par
    \textbf{Max Mustermann:} University of Life\par
    \textbf{Abtract:} \lipsum*[1][1-3]\par
  \egroup
  \newpage
  \hsize80mm
  }

\def\titlebreak{%
  \ifhmode\vadjust{\par\newpage\noindent}\else\newpage\fi%
  \global\let\@outputdblcol\ltx@outputdblcol
  \global\let\is@firstpage\@undefined
  \global\columnwidth60mm
  \global\hsize\columnwidth
  \global\linewidth\columnwidth
}
\AtBeginShipoutNext{\ifx\is@firstpage\relax\titlebreak\fi}
\makeatother


\begin{document}
\maketitle
\lipsum[1-3]

\lipsum[4-9]
\end{document}

输出为 MWE 的输出

如您所见,除了分页符所在的段落的其余部分外,此方法效果很好。如果第一个分页符出现在段落内,则要求作者/编辑\titlebreak在分页符所在的行中手动设置宏。

但我想知道是否有办法使用 LuaLaTeX 自动执行第一个分页符?

问题:有没有办法(可能是 lua hook)让我能够在\titlebreakLuaLaTeX 中断页面的精确位置(或行)自动注入宏?我如何在现有代码中实现这一点?

答案1

我将在这里使用简化的 MWE,以便更容易看到效果:

\documentclass{article}

\usepackage[english]{babel}
\usepackage{blindtext}
\usepackage{microtype}

\AddToHookNext{shipout}{%
    \global\columnwidth=0.5\columnwidth
    \global\textwidth=\columnwidth
    \global\hsize=\columnwidth
    \AddToHookNext{shipout}{%
        \global\columnwidth=2\columnwidth
        \global\textwidth=\columnwidth
        \global\hsize=\columnwidth
    }
}

\newdimen\bigright \bigright=\dimexpr\oddsidemargin+1in+\textwidth\relax
\newdimen\smallright \smallright=\dimexpr\oddsidemargin+1in+0.5\textwidth\relax

\AddToHook{shipout/foreground}{%
    \put(\dimexpr\oddsidemargin+1in, 0in){%
        \line(0,-1){\paperheight}
    }%
    \put(\bigright, 0in){%
        \line(0,-1){\paperheight}
    }%
    \put(\smallright, 0in){%
        \line(0,-1){\paperheight}
    }%
}

\begin{document}
    \Blindtext[10]
\end{document}

原始输出

您应该能够使用任何其他方式来更改此处的页面布局;关键是您需要设置下一页的列宽前一页已运出。这也意味着此解决方案将仅调整每页第一列的宽度,因此任何给定页面上的列都必须具有相同的宽度。

有没有一种方法(可能是 lua hook)可以让我\titlebreak在 LuaLaTeX 中断页面的精确点(或行)处自动注入宏?

并非如此。分段严格发生在分页之前(或与分页同时发生),因此在创建段落时,它无法“知道”段落将在何处拆分或是否会跨页面拆分。

使用传统的 TeX 方法,您可以定义一个宏,让段落.aux在第一次运行时将其分割点写入文件,然后使用\parshape技巧在第二次运行时正确调整段落的大小。不过这会相当繁琐,而且我不太确定它的效果如何。

另一个选择是使用一些 Lua 技巧。在每个段落被拆分之前,我们可以用属性标记每个节点(字形 + 胶水 + 等)并保存一份副本。然后,当触发输出例程时,我们可以查看contrib_head(任何保留到下一页的段落),将拆分的节点与拆分前的节点进行匹配,并用新拆分的正确大小的段落替换该段落。这里唯一棘手的事情是没有post_output_filter,所以我们需要在 shipout 之前而不是在输出例程之后实际运行它。

\documentclass{article}

\usepackage[english]{babel}
\usepackage{blindtext}
\usepackage{microtype}

\csname @ifl@t@r\endcsname\fmtversion{2020-10-01}{}{
    % TL2019 compatibility
    \usepackage{atbegshi}

    \directlua{luatexbase.create_callback("pre_shipout_filter", "list", false)}
    \AtBeginShipout{%
        \csname columns@run\endcsname
        \directlua{luatexbase.call_callback("pre_shipout_filter", tex.box.AtBeginShipoutBox)}%
    }

    \usepackage{picture}
    \def\AddToHookNext#1#2{
        \global\expandafter\def\csname columns@run\endcsname{#2}
    }
    \def\AddToHook#1#2{\AtBeginShipout{\AtBeginShipoutUpperLeftForeground{#2}}}
}

\AddToHookNext{shipout/firstpage}{%
    \global\columnwidth=0.5\columnwidth
    \global\textwidth=\columnwidth
    \global\hsize=\columnwidth
    \AddToHookNext{shipout/before}{%
        \global\columnwidth=2\columnwidth
        \global\textwidth=\columnwidth
        \global\hsize=\columnwidth
    }
}

\newdimen\bigright \bigright=\dimexpr\oddsidemargin+1in+\textwidth\relax
\newdimen\smallright \smallright=\dimexpr\oddsidemargin+1in+0.5\textwidth\relax

\AddToHook{shipout/foreground}{%
    \put(\dimexpr\oddsidemargin+1in, 0in){%
        \line(0,-1){\paperheight}
    }%
    \put(\bigright, 0in){%
        \line(0,-1){\paperheight}
    }%
    \put(\smallright, 0in){%
        \line(0,-1){\paperheight}
    }%
}


\usepackage{luacode}
\begin{luacode*}
-- Save some LuaTeX internal node functions
local copy_list = node.copy_list
local find_attribute = node.find_attribute
local get_attribute = node.get_attribute
local has_attribute = node.has_attribute
local set_attribute = node.set_attribute
local slide = node.slide
local traverse = node.traverse
local traverse_list = node.traverse_list


-- Define some module-global variables
local attr = luatexbase.new_attribute("mark_paragraph")
local count = 1
local saved = node.new("glue")
local columnwidth = 0


-- Mark each node in a paragraph and save the pre-broken tagged contents
local function save_paragraph(head)
    for n in traverse(head) do
        set_attribute(n, attr, count)
        count = count + 1
    end

    slide(saved).next = copy_list(head)
    columnwidth = tex.dimen.columnwidth

    return true
end

luatexbase.add_to_callback("pre_linebreak_filter", save_paragraph, "save_paragraph")


-- Fix the width of any partial paragraphs carried over from the previous page
local function fix_paragraph(head)
    -- do nothing if columnwidth has not been changed
    if columnwidth == tex.dimen.columnwidth then return true end

    -- do nothing if the last contribution is a penalty, meaning that the page break was forced manually 
    if tex.lists.contrib_head.id == 14 then return true end

    local first_index, first_n, first_list
    local last_index, last_n, last_list

    for list, _, _, inner in traverse_list(tex.lists.contrib_head) do
        if not first_index then
            first_index = find_attribute(inner, attr)
            first_list = list
        end

        local prev_last_index
        local m = inner
        repeat
            last_index = prev_last_index
            prev_last_index, m = find_attribute(m.next, attr)
        until (not prev_last_index) or (not m.next)
        last_list = list
    end

    for n in traverse(saved) do
        if not first_n and
           (get_attribute(n, attr) or 0) >= first_index
        then
            first_n = n
        end

        if has_attribute(n, attr, last_index) then
            last_n = n
            break
        end
    end

    first_n.prev.next = nil
    node.flush_list(saved)
    saved = node.new("glue")

    local new = copy_list(first_n, last_n.next)
    local broken = tex.linebreak(new)
    first_list.prev.next = broken
    slide(broken).next = last_list.next

    last_list.next = nil
    node.flush_list(first_list)

    return true
end

luatexbase.add_to_callback("pre_shipout_filter", fix_paragraph, "fix_paragraph")
\end{luacode*}

\begin{document}
    \Blindtext[10]
\end{document}

解决方案输出

可能仍存在一些错误,但在我的测试中这似乎运行良好。

相关内容