Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion floatplace.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const FloatPlacementSubstrate = `
type floatNode struct {
box *boxNode
place string // placement bits (h/t/b/p), lower-cased; "" means default "tbp"
kind string // caption type: "figure" or "table" — floats of one type keep their order
}

func (*floatNode) isNode() {}
Expand Down Expand Up @@ -98,9 +99,20 @@ func (e *Engine) doFloatBegin() {
}
e.define("@captype", &meaning{kind: mMacro, body: stringToToks(kind)}, true)
body := e.collectEnvBody(env)
// The body is a BOX being built, so what it assigns must stay inside it: \@xfloat
// sets the float in \vbox\bgroup…\egroup (latex.ltx:12950), and the engine's own
// inline \@float opens a \begingroup that \end@float closes. Capturing the body
// and running it with no group of its own let an assignment escape into the
// document. One real paper shows the cost: a figure holding
// \put(-0.33\textwidth,0.5\textwidth){…} — \put undefined, so \textwidth reads as
// the start of an assignment and takes the missing number as zero — left \hsize
// at 0pt for the remaining 230 pages, which then set ONE WORD PER LINE and ran to
// 1439 pages against a reference of 333.
e.beginGroup()
box := e.typesetGroupToVbox(append([]tok{csTok("centering")}, body...))
e.endGroup()
box.width = e.hsize
e.contribute(&floatNode{box: box, place: place})
e.contribute(&floatNode{box: box, place: place, kind: kind})
}

// isStandardFloatEnv reports whether name is one of the standard float environments,
Expand Down Expand Up @@ -131,6 +143,18 @@ func (e *Engine) currentEnvName() string {
return "figure"
}

// kindDeferred reports whether a float of this caption type is still waiting, which
// forbids a later one of the same type from being placed first (\@bitor\@currtype
// \@deferlist in \@addtocurcol): figure 3 must never appear before figure 2.
func kindDeferred(deferred []anchoredFloat, kind string) bool {
for _, af := range deferred {
if af.f.kind == kind {
return true
}
}
return false
}

// mvlHasFloats reports whether the main vertical list carries any captured floatNode.
func (e *Engine) mvlHasFloats() bool {
for _, n := range e.mvl {
Expand Down Expand Up @@ -376,6 +400,46 @@ func (e *Engine) pagesWithFloats() []*boxNode {
e.vsize = pageVsize
end := e.findPageBreak(text, start)
e.vsize = savedVsize

// A float written INSIDE this page is a candidate for this page's own top.
// LaTeX contributes a float at its anchor — \@xfloat fires the output routine
// there with \@floatpenalty — and \@addtocurcol (latex.ltx:15636) then tests it
// against the room LEFT in the column: it goes to the top of the page being
// built whenever \@colroom exceeds the height already set plus \textfraction
// plus the float. That is why a figure declared halfway down a page comes out
// at the top of THAT page. Taking only the floats anchored before the page
// began pushed every one of them a page later, and at the end of the document
// onto float pages of their own.
for fi < len(floats) && floats[fi].at < end {
af := floats[fi]
fi++
// \@bitor\@currtype\@deferlist: a float may not overtake an earlier one
// of its own type that is still waiting.
if len(top) < 2 && af.c.allowTop && !kindDeferred(deferred, af.f.kind) {
need := fh(af)
if len(top) > 0 {
need += e.floatSep()
}
want := topH + need
wantReserve := want + botH + e.textFloatSep()
if botH > 0 {
wantReserve += e.textFloatSep()
}
if want <= topCap && wantReserve <= vsize-textMin {
top = append(top, af)
topH, reserve = want, wantReserve
pageVsize = vsize - reserve
if pageVsize < textMin {
pageVsize = textMin
}
e.vsize = pageVsize
end = e.findPageBreak(text, start) // less room now: the text stops earlier
e.vsize = savedVsize
continue
}
}
deferred = append(deferred, af)
}
pageText := trimTrailingGlue(text[start:end])

// Emit only a non-empty page (as paginateSingleList does): an empty text break with
Expand Down
50 changes: 50 additions & 0 deletions floatplace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,53 @@ func TestFloatTopPlacement(t *testing.T) {
t.Error("[t] float box was not found at the top of any page")
}
}

// The captured body is a box being built, so an assignment inside it must not
// escape into the document. The real case is a figure holding
// \put(-0.33\textwidth,…): \put is undefined, so \textwidth reads as the start of
// an assignment, takes the missing number as zero, and — before the body was given
// a group of its own — left \hsize at 0pt for the rest of the paper, which then set
// one word per line (1439 pages against a reference of 333).
func TestCapturedFloatBodyCannotChangeTheTextWidth(t *testing.T) {
t.Setenv("GOTEX_FLOATS", "1")
e := New()
if err := e.LoadLaTeX(); err != nil {
t.Fatal(err)
}
e.SetFont(spMock{})
if _, err := e.Run(`\documentclass{article}\begin{document}` +
`\begin{figure}\textwidth=0pt\caption{Plot}\end{figure}` +
strings.Repeat(`Body text paragraph. `, 20) + `\par`); err != nil {
t.Fatal(err)
}
if e.hsize <= 0 {
t.Fatalf("\\hsize = %d sp after the float: an assignment escaped the captured body", e.hsize)
}
}

// A float written halfway down a page belongs at the top of THAT page: LaTeX
// contributes it at its anchor and \@addtocurcol (latex.ltx:15636) tests it against
// the room left in the column. Taking only floats anchored before the page began
// pushed every one of them at least a page later.
func TestFloatAnchoredInsideThePageRidesIt(t *testing.T) {
t.Setenv("GOTEX_FLOATS", "1")
e := New()
if err := e.LoadLaTeX(); err != nil {
t.Fatal(err)
}
e.SetFont(spMock{})
// Text, then a figure, then more text: the figure is anchored inside page 1.
if _, err := e.Run(`\documentclass{article}\begin{document}` +
strings.Repeat(`Opening paragraph. `, 10) + `\par` +
`\begin{figure}\caption{Plot}\end{figure}` +
strings.Repeat(`Body text paragraph. `, 10) + `\par`); err != nil {
t.Fatal(err)
}
pages := e.Pages()
if len(pages) != 1 {
t.Fatalf("the whole document fits one page, got %d", len(pages))
}
if txt := mvlText(pages[0].list); !strings.Contains(txt, "Figure1:Plot") {
t.Errorf("the float did not ride the page it was written on: %q", txt)
}
}
Loading