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
70 changes: 70 additions & 0 deletions beamercolumns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package engine

import (
"os"
"path/filepath"
"strings"
"testing"
)

// Two \column's in a frame must not cost a page break. beamer's \beamer@colclose
// carries \end{minipage}\hfill\end{actionenv}\ignorespaces
// (beamerbaseframecomponents.sty:283); honouring only the \end{minipage} and
// emptying the whole macro dropped the \end{actionenv} pairing the
// \begin{actionenv} the column opened, so two columns left an environment group
// open and the \end{frame} that followed closed THAT — the next frame never began
// its own page, and material written after \end{columns} came out on the page
// BEFORE the frame's own content.
func TestTwoBeamerColumnsKeepTheirPageBreaks(t *testing.T) {
// \beamer@colclose exists only in the REAL class; the built-in emulation makes
// \column a \par and never opens an actionenv, so it cannot show this defect.
// Skip rather than pass for the wrong reason where the tree is not installed.
tree := os.Getenv("GOTEX_TEXMF")
if tree == "" {
tree = "/Users/Shared/gotex/measure/texmf"
}
if _, err := os.Stat(filepath.Join(tree, "beamer.cls")); err != nil {
t.Skip("no real beamer.cls under GOTEX_TEXMF: the emulation does not exercise \\beamer@colclose")
}
t.Setenv("GOTEX_TEXMF", tree)
src := `\documentclass{beamer}\begin{document}` +
`\begin{frame}{Un}\begin{columns}` +
`\column{0.5\textwidth}gauche` +
`\column{0.5\textwidth}droite` +
`\end{columns}\end{frame}` +
`\begin{frame}{Deux}beta\end{frame}` +
`\begin{frame}{Trois}gamma\end{frame}\end{document}`
e, err := compile([]byte(src), Options{Lenient: true})
if err != nil {
t.Fatal(err)
}
pages := e.Pages()
if len(pages) != 3 {
var got []string
for _, p := range pages {
got = append(got, mvlText(p.list))
}
t.Fatalf("pages=%d want 3: %q", len(pages), got)
}
if txt := mvlText(pages[1].list); !strings.Contains(txt, "Deux") || strings.Contains(txt, "Trois") {
t.Errorf("page 2 should carry the second frame alone: %q", txt)
}
}

// colCloseAfterEndMinipage keeps what follows the \end{minipage} it honours, and
// answers nil for a closer that does not begin with one (nothing honoured, nothing
// to drop).
func TestColCloseKeepsWhatFollowsTheEndMinipage(t *testing.T) {
body := tokenizeTeX(`\end{minipage}\hfill\end{actionenv}\ignorespaces`)
rest := colCloseAfterEndMinipage(body)
e := New()
if got := e.toksToString(rest); got != `\hfill \end {actionenv}\ignorespaces ` && !strings.Contains(got, "actionenv") {
t.Errorf("rest = %q, want the \\end{actionenv} tail", got)
}
if colCloseAfterEndMinipage(tokenizeTeX(`\hfill`)) != nil {
t.Error("a closer with no leading \\end{minipage} must be left alone")
}
if colCloseAfterEndMinipage(nil) != nil {
t.Error("an empty closer must stay empty")
}
}
47 changes: 46 additions & 1 deletion minipage.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,51 @@ func (e *Engine) doMinipage() {
e.place(alignParbox(vbox, pos))
}

// honourColClose takes the \end{minipage} off the front of \beamer@colclose and
// leaves the REST of it in place. The closer beamer builds is
//
// \def\beamer@colclose{\end{minipage}\hfill\end{actionenv}\ignorespaces}
//
// (beamerbaseframecomponents.sty:283), and a body scanner that meets the next
// \column has honoured only its FIRST half — the \end{minipage}. Emptying the whole
// macro, as this did, dropped the \end{actionenv} that pairs with the
// \begin{actionenv} the column opened: two columns then left one environment group
// open, and the \end{frame} that followed closed THAT instead of the frame, so the
// next frame never started its own page. A talk with two columns lost a page break
// and could emit its pages out of order (issue #205).
func (e *Engine) honourColClose() {
m := e.eq["beamer@colclose"]
rest := []tok(nil)
if m != nil && m.kind == mMacro {
rest = colCloseAfterEndMinipage(m.body)
}
e.define("beamer@colclose", &meaning{kind: mMacro, body: rest}, true)
}

// colCloseAfterEndMinipage returns what follows a leading \end{minipage} in a
// \beamer@colclose body, or nil when the body does not begin with one (nothing was
// honoured, so nothing is dropped).
func colCloseAfterEndMinipage(body []tok) []tok {
if len(body) == 0 || !body[0].cs_ || body[0].cs != "end" {
return nil
}
i, name := 1, ""
if i < len(body) && !body[i].cs_ && body[i].cat == catBegin {
i++
for i < len(body) && !body[i].cs_ && body[i].cat != catEnd {
name += string(body[i].ch)
i++
}
if i < len(body) {
i++ // the closing brace
}
}
if name != "minipage" {
return nil
}
return append([]tok(nil), body[i:]...)
}

// collectEnvBody reads raw tokens (no expansion) up to the matching \end{name} and
// returns the body tokens. It tracks nesting: a \begin{name} deeper in the body
// increments the depth and a \end{name} decrements it, so only the \end{name} at
Expand Down Expand Up @@ -118,7 +163,7 @@ func (e *Engine) collectEnvBody(name string) []tok {
// \beamer@colclose is emptied because its \end{minipage} has just been
// honoured, which is what beamer itself does after running it (:269).
e.back(t)
e.define("beamer@colclose", &meaning{kind: mMacro}, true)
e.honourColClose()
e.endEnvGroup()
return body
case t.cs_ && t.cs != "end" && t.cs != "begin" && e.expandsToEnd(t):
Expand Down