![理论](https://linux22.com/image/262867/%E7%90%86%E8%AE%BA.png)
回答时随机开始行\raggedright
,我发现了和之间奇怪的互动\parshape
。
如果右侧参差不齐的段落以短行结束,则该行将是“最参差不齐的”。
\documentclass{article}
\begin{document}
\raggedright
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
\end{document}
现在看这个:我加了一个假人\parshape
(它实际上什么也没做),突然间,它就变成了第一的线很短!
\documentclass{article}
\begin{document}
\raggedright
\parshape 4
0cm \textwidth
0cm \textwidth
0cm \textwidth
0cm \textwidth
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
\end{document}
让我们进一步探索。结果段落有三行。如果\parshape
规范包含三项会发生什么?我们恢复正常!
\documentclass{article}
\begin{document}
\raggedright
\parshape 3
0cm \textwidth
0cm \textwidth
0cm \textwidth
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
\end{document}
一般来说,当且仅当\parshape
规范至少比结果段落多一项时,我们才会得到意外结果。我猜这就是为什么这一切很难被注意到:通常,长度\parshape
为零。
进一步探索后,我发现纯 TeX 的表现还不错。(为了便于比较,我将纯 TeX 的文本宽度设置为与 LaTeX 默认值相同。)
\def\is{%
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i
i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i}
\hsize 345pt
\parskip 2ex
\raggedright
\is
\parshape 3
0cm \hsize
0cm \hsize
0cm \hsize
\is
\parshape 4
0cm \hsize
0cm \hsize
0cm \hsize
0cm \hsize
\is
\bye
普通 TeX 和 LaTeX 之间的差异表明 LaTeX 的定义\raggedright
应该受到指责。
特克斯:
> \raggedright=macro:
->\rightskip \z@ plus2em \spaceskip .3333em \xspaceskip .5em\relax .
乳胶:
> \raggedright=macro:
->\let \\\@centercr \@rightskip \@flushglue \rightskip \@rightskip \leftskip \z@skip \parindent \z@ .
事实上,LaTeX 的定义设置\rightskip
为\@flushglue
,也就是 0.0pt plus 1.0fil
,因此很可能是所描述的问题的原因。定义(在纯 TeX 中)
\def\raggedright{\rightskip 0pt plus1.0fil \spaceskip .3333em \xspaceskip .5em\relax}
使普通的 TeX 表现得像 LaTeX。(几乎:为了完美匹配(在本例中)写入\def\raggedright{\parindent 0pt\rightskip 0pt plus1.0fil\relax}
。)
我也用原始的(非 pdf、非 e)TeX 对此进行了检查。行为是一样的。顺便说一句,我使用的是 TeX 版本 3.1415926-2.4-1.40.13(TeX Live 2012/Arch Linux)。
我的问题(最后)实际上是多方面的:
这是怎么发生的?即,我想了解 TeX 如何计算段落形状并导致第一行较短的详细信息。
这怎么可能呢? 我已经阅读了
\parshape
TeXbook 和 TeX by Topic 中能找到的所有内容,并搜索了网络,但没有任何地方说\parshape
如果重复最后一项(一次太多)会产生不同的效果。这是 TeX 中的一个错误吗?!?! ... ... 我想不是:我只是不相信我足够幸运,能够偶然发现一个,并在一次袭击中获得财富和永恒的荣耀:-)))那么,纯 TeX 和 LaTeX 的预期效果究竟是什么
\raggedright
?显然是右边缘参差不齐 :-) 还有别的吗?3a. 定义的各个组成部分如何有助于实现目标?(针对纯 TeX 和 LaTeX 的单独问题。)
顺便说一下,这个问题给楼主带来的问题是随机开始行\raggedright
通过将他的重新定义为类似 TeX 的普通版本来解决。(所以我的问题是一个学术问题。)
编辑:起初我认为 LaTeX 的参差不齐的右边缘看起来比普通 TeX 的要好看。但 Stephan 的评论让我意识到我只是没有看正确的例子。所以我收回了那个观点,并从问题中删除了所有对它的引用。
更新
通过查看 TeX 源代码,我找到了另外两种触发该现象的方法\raggedright
:
\hangafter
如果 abs( ) >= 段落中的行数,则悬挂缩进(考虑到\parshape
问题,这并不出乎意料)非零
\looseness
,如下图所示。事实上,(正)松散程度越大,结果越糟糕。:-)\documentclass{article} \begin{document} \parindent 0pt \looseness=1 \raggedright i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i \end{document}
如果我没记错的话,\parshape
和\looseness
悬挂缩进应该是一个完整的列表。
答案1
虽然我非常感谢 Stephan 和 Joseph 的回答(尤其是 Joseph 提供的有关 LaTeX3 团队发现相同问题的信息),但我必须承认我有一种无法满足的好奇心:我不得不进一步挖掘。所以我深入研究了 TeX 源代码...
理论
让我重申一下对我来说问题的关键部分。令我困惑的是,以不同的方式传达相同的指令(例如通过书写\parshape 3 0cm \hsize 0cm \hsize 0cm \hsize
或什么都不做)会产生不同的效果。
但现在我知道了!TeX 的换行算法可以以两种模式运行:我将它们称为简单的和难的显然,困难模式的计算成本更高,因此尽可能使用简单模式。但是,至少对我(换行理论的完全新手)来说,第一个大问题是:TeX 到底为什么需要困难模式?在简单模式下不能做什么?
容易又困难:为什么?
首先看一下左上段。请注意,左侧的单词multilingual typesetting
排版非常糟糕。如果TeX's
将单词向下推一行,排版会不会更好?显然不会(参见右上段),因为后面所有行的长度都相同。
当断点之后的行数不均等长时,情况就大不相同了。一旦将单词向下推一行(右下角),中间和中间TeX's
的间隙(左下角)就会消失!multilingual
typesetting
\documentclass{article}
\usepackage{fullpage}
\pagestyle{empty}
% text taken from Wikipedia's article 'TeX'
\def\history{However, since the source code of \TeX\ is essentially in the public domain\paren{}, other programmers are allowed\paren{ (and explicitly encouraged)} to improve the system, but are required to use another name to distribute the modified \TeX, meaning that the source code can still evolve. For example, the Omega project was developed after 1991, primarily to enhance \fbox{\TeX's}\break \emph{multilingual typesetting} abilities. Donald Knuth himself created "unofficial" modified versions, such as \TeX-XeT, which allows a user to mix texts written in left-to-right and right-to-left writing systems in the same document.} \def\paren#1{}
\def\pushdown{\let\paren\relax}
\def\Long{0pt \hsize}
\def\Short{0pt 130pt}
\def\parshapeP{\parshape 8 \Long\Long\Long\Long\Long\Long\Long\Short}
\def\parshapeC{\parshape 9 \Long\Long\Long\Long\Long\Long\Long\Short\Long}
\begin{document}
\tracingparagraphs=1
\hyphenpenalty=10000
\parindent 0pt
\parskip 3ex
\begin{minipage}[t]{222pt}\parshapeP\history\end{minipage}
\quad\quad
\begin{minipage}[t]{222pt}\parshapeP\pushdown\history\end{minipage}
\begin{minipage}[t]{222pt}\parshapeC\history\end{minipage}
\quad\quad
\begin{minipage}[t]{222pt}\parshapeC\pushdown\history\end{minipage}
\end{document}
(显然,TeX 不会像我一样通过在段落中添加一些文本来将断点向下推。(那可能是 Word:-) TeX 可以使用灵活的单词间距,如果灵活性变成一行长...瞧,一个位置有两种可能性!顺便说一句,虽然可以通过设置\tolerance
/ \spaceskip
/...(或使用 LaTeX 的\raggedright
!)来增加 TeX 的默认间距,但在现实世界中也很容易找到一行额外的空间:只需写一个长而/或窄的段落!)
TeX 的换行算法通过从头到尾遍历段落来运行,一路记住哪些位置可能成为断点。假设 TeX 已经意识到当前位置可以中断多行。如果当前位置之后的所有行长度相等,TeX 知道它对此的决定不会影响后续换行的质量:因此它可以采取简单的方式,只记住做出选择前段落中看起来最好的部分(这里 TeX 可以做出完美的决定,因为它已经遍历过了)而忽略了其他可能性。但是,如果当前位置之后的行不是所有行都等长,TeX 别无选择,只能选择难的方式:它必须记住所有的可能性。
线路级别:一切都简单又正确
TeX 的换行算法实际上是一台大型修剪机。计算段落中最佳断点序列的最愚蠢方法是列出所有可能性并评估每个可能性;然而,在包含n
合法断点的段落中,这将产生一个惊人的2^n
序列。因此,TeX 会尽早消除可能性。我们在上面已经看到 TeX 如何尽可能进入“简单”模式:简单模式成本较低,因为它会修剪更多。
在“行级”上,简单模式和困难模式的行为相同。考虑modified
第四行末尾的单词,即左上角段落。当 TeX 评估在该单词后换行的可能性时,它有两个选项(参见日志摘录@@6
):是在单词之后required
(@via @@4
)还是在to
(@via @@5
)之后创建一个行。与上面描述的简单模式类似,该决定不可能影响后续位置的换行,因此选择两个选项中局部更好的一个(@@4
,即对段落开头产生较少缺点的选项),而另一个则被遗忘。
al-lowed to im-prove the sys-tem, but are re-quired
@ via @@3 b=99 p=0 d=11881
@@4: line 3.1 t=1759534 -> @@3
to
@ via @@3 b=0 p=0 d=10100
@@5: line 3.2 t=1757753 -> @@3
use an-other name to dis-tribute the mod-i-fied
@ via @@5 b=2376 p=0 d=5702996
@ via @@4 b=285 p=0 d=87025
@@6: line 4.0 t=1846559 -> @@4
一个看似无害的问题:如果两个(或更多)可能性产生相同的缺点,会发生什么?答案直接来自源头:选择最正确的那个。
详细解释一下原因。当 TeX 评估当前位置是否可以成为断点时,它会从左到右遍历以下列表:活动节点(活动节点是 TeX 对潜在断点的表示——断点位于当前位置之前,并且被认为是可行的。每个活动节点都包含指向某个其他活动节点(在文本中位于其之前)的链接。因此,每个活动节点都是断点链的头部,该断点链指向段落的开头。)在每个节点处,TeX 首先检查当前位置是否是可行的断点相对于当前活动节点,即从当前活动节点到当前位置的行的坏性是否在用户指定的容差范围内。如果是,TeX 会将当前链的总缺点(第一近似值:各行的坏性总和)与目前在当前位置找到的最小总缺点进行比较。如果当前总缺点小于或等于总缺点数达到最低,则记录该缺点(覆盖前一位候选人)。
简而言之:由于从左到右的处理以及小于或等于的比较,因此选择了最右边的选项。
(知识渊博的读者肯定注意到我忽略了适应度分类。这是故意的,因为它与原始问题无关。实际上,我忽略了很多事情......)
活动节点:逐行
我们正在接近问题的核心。
活动节点如何进入(活动)列表?在这里,两种模式截然不同。在简单模式下,(活动节点的)行号无关紧要,因此 TeX 最多为每个当前位置创建一个活动节点:它将其链接到(最右边的)活动节点,从而产生所有活动节点中最小的总缺点。然后将新的活动节点附加到活动列表中。这很好,因为它保留了线性顺序;换句话说,活动列表中活动节点的顺序与相应位置的相对顺序相匹配。
在困难模式下,情况会稍微复杂一些。TeX 必须按行号类别对活动节点进行分组,即对于每个行号,它必须选择具有该行号的最佳活动节点。问题是,如何才能有效地做到这一点?(想想 '80,而不是 Python!)
Knuth 的解决方案非常巧妙。(虽然,正如我们将看到的,它有点适得其反。)活动列表按行号排序。因此,可以在单个块中找到具有给定行号的所有活动节点。这使得计算相对于行号类别的总缺点最小值和在适当位置插入新活动节点变得微不足道。让我们看看如何操作。假设 TeX 当前正在遍历n
活动列表的行号类别块,并在此过程中更新最小缺点。当 TeX 从下一个类别(行号>n
)(或活动列表的末尾)命中一个活动节点时,(i)TeX 已经看到了类别的所有活动节点n
,因此类别的最小缺点计算n
完成:可以创建新的活动节点;并且 (ii) 该位置(紧接着行类 之后n
)对于插入新的活动节点来说是理想的,其行号为n+1
:通过将新节点插入到当前活动节点(即类 的第一个节点)之前来遵守排序顺序(按行号)>n
。
显然,这个过程的结果是新的活动节点将被插入前其类中的其他节点(如果活动列表中有的话)。但是,新的活动节点在段落中的位置是当前位置,关注“旧”活动节点的位置。因此,困难模式将反转每个行号类中的活动节点的顺序 (例如,请参见上面日志摘录中的@ via @@4
和的顺序)。@ via @@5
实际上,这个无关紧要的问题的答案实际上是错误的,或者至少是不精确的。确实,最右边的节点获胜,但按照活动列表的线性顺序是最右边的,而从段落中的位置意义上来说,它不一定是最右边的。(这里,右边应该读作“更接近末尾”。)
LaTeX 的 \raggedright
现在理解 LaTeX 的问题\raggedright
很简单。它定义了一个\rightskip
。它定义了无限可拉伸性。因此,所有线条都可以收缩而不会受到惩罚。因此,任何断点序列同样好。(更准确地说:在通常的正设置下,任何产生相同行数的序列\linepenalty
。通常,将选择行数最少的序列。)因此,处理顺序在断点链的选择中起着重要作用。
实践
这是一个错误吗?
问题是,如果断线问题有多个最优解,即,如果通过多个断点序列实现最小缺点,应该会发生什么。
据我所知(我知道 [1,2,3]),Knuth 没有提到这种情况,因此没有对结果做出任何承诺。我们真的不应该为此责怪他。想想看:如何描述将选择哪个等效断点序列?!除了要求用户研究算法之外,我看不出还有其他方法……
因此,一方面,不保证没有错误。
但是,用户(尤其是 TeX 用户,我们被宠坏了!;-) 希望他们的程序具有一致的行为。关键是,我敢肯定任何人(没有研究过 TeX 源代码的人)都会将简单模式简单地视为困难模式的一个特例:“哦,我不必\parshape=1 0cm \hsize
在每个段落前写字!多方便啊!!!”。或者,当 TeXbook 中明确指出最后一行的规范将“无限重复”时,怎么会有人期望\parshape=1 x y
不产生与完全相同的效果?(Knuth 风格的练习:\parshape=2 x y x y
\parshape
为什么最后两个\parshape
s 能有不同的效果吗?
从这个意义上来说,这确实是一个 Bug。
avaniTeX
我们现在在“实践”部分,但实际上我完全是出于学术兴趣而编写程序的——作为概念证明。看,如果我不能解决这个“问题”,即统一简单模式和困难模式的行为,我就不可能(几乎)绝对有信心地写出这个答案。因此,我制作了一个补丁,甚至给它命名——我不想违反 TeX 的许可证!它被称为 avaniTeX(avani 是我以前在网上使用的昵称),这完全缺乏想象力。您可以从这里下载它我的网页. 它的工作原理如下。
一个邪恶的想法是“修复”简单模式。(邪恶是因为它当然会破坏 LaTeX 中的所有 flushleft 环境 :-) 补丁由一条指令组成:用 a 替换<=
a <
:-)。然而,这实际上不起作用,因为我后来发现活动节点不是简单地反转,而是按行号类反转。
正确的做法是修复活动节点的插入:我们想插入一个新节点n+1
后类的所有其他节点n+1
--- 这将使活动列表顺序与段落顺序保持同步。原则上,这实际上相当容易实现(没有任何严重的计算开销)。不是将新节点直接插入活动列表,而是将其附加到等待列表中,当行号类n+1
结束时,等待列表将插入活动列表。这很好,因为等待列表是在类之间转换时精确填充的。(顺便说一句,等待列表必须是列表因为它可能需要存储多个适应度的新活动节点。)
然而,正确处理增量节点是一场噩梦。
它能用吗?或多或少……我用它重新编译我的文档没有遇到任何问题。TRIP 测试(非常有用,它发现了我的许多错误)在第二次运行时失败(前几“页”trip.typ
没问题,但其余的是垃圾)。---就在发布这篇文章之前,我发现当活动列表为空时,紧急通道中会出现问题:这就是 TeX 排版溢出框的地方。我现在不打算调查这个问题:Knuth 警告说,关于那段特定的代码,“因此,那些试图“改进”TeX 的读者在敢于在这里做任何改变之前应该三思而后行。” :-)
最后,我有没有说过我讨厌 Pascal?:-))) 但我确实喜欢 WEB,并且已经成为文学编程的忠实粉丝,我保证从现在开始更好地记录我的代码。说真的,阅读 Knuth 的代码是一种乐趣。有时间试试吧。
- TeXbook。
- Donald E. Knuth 和 Michael F. Plass (1981)。将段落拆分为行。软件---实践与经验,第 11 卷,1119--1184。
- TeX82:记录来源。
答案2
在希望约瑟夫能够写出一个答案详细描述 LaTeX3 团队所认为的情况的同时,让我总结一下可能的解决方法:
- 始终确保
\parshape
定义的行数不超过段落所含的行数。 使用不同的定义
\raggedright
。例如,下面的方法似乎有效:\makeatletter \def\raggedright{% \let\\\@centercr\@rightskip \z@ plus\linewidth \rightskip\@rightskip \leftskip\z@skip \parindent\z@} \makeatother
还有其他建议吗?
答案3
我无法解释 TeX 代码中发生了什么,但我至少可以提供一些有关该问题的详细信息。该问题出现在 LaTeX2e 的定义中\raggedright
,但在更深层次上并不依赖于 LaTeX2e。(您可以询问 LaTeX2e 对这个概念的定义,但这个问题只有 Leslie Lamport 才能回答!)
在 LaTeX 团队内部 wiki 中,我们有一个使用纯 TeX 的演示:
\def\text{%
There is no just ground, therefore, for the charge brought }
\rightskip=0pt plus 1fil
\def\oneline{ 0em 250pt}
\parshape 2 \oneline\oneline
\text
\vskip 4ex
\parshape 3 \oneline\oneline\oneline
\text
\bye
这已报告给 Barbara Beeton(可能是一个 TeX 错误):希望她能对此发表评论!
进一步的实验表明,真正糟糕的第一个换行发生在以下情况:
- 的行
\parshape
数多于段落所需的行数(可以包括使用 的情况\hangafter
,这基本上是一种快捷方式\parshape
) - 或者包含“过多”拉伸:任何无限或有限粘合
\leftskip
的\rightskip
长度超过当前线宽的 120%。
正如 Stephan 所言,唯一的解决办法是避免以下情况之一:将\leftskip
和中的拉伸保持在较低水平\rightskip
,并且不要将 设置\parshape
为太长。(后者比较棘手,因为它取决于文本,因此跳过部分可能是需要查看的地方。)
答案4
我想对问题 3 发表一些评论,即 TeX 和 LaTeX 的 定义之间的差异\raggedright
。正如您所解释的,TeX 使用,\rightskip
而0pt plus 2em
LaTeX 使用0pt plus 1fil
。正如约瑟夫写道,只有 Leslie Lamport 可以说为什么他选择了另一种方法。我能说的是 Knuth 可能不会同意他的选择。摘自 TeXbook 第 101 页:
例如,一个人可以设置
\rightskip=0pt plus1fil
,这样每行右侧都会有空格。但这并不是制作右侧边距的特别好的方法,因为无限可拉伸性会为非常短的行分配零不良度。要完成正确的右侧边距设置,诀窍是设置得\rightskip
足够拉伸以使换行成为可能,但不要太多,因为短行应该被视为不好。
对我来说,正确的解决方案应该是类似\rightskip=0pt plus \hsize
,但对于实际使用的每一点可伸缩性,都会产生一些不良影响,就像单词间粘连的拉伸和收缩会导致不良影响一样。但是,这似乎无法通过 TeX 的换行算法实现。
顺便说一句,正如斯蒂芬已经评论了,我并不认为 LaTeX 生成的段落比 TeX 更“整齐”。所以我认为解决这个问题的最佳方法是使用 TeX 的定义,\raggedright
看看这是否会导致其他问题。