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
2 changes: 2 additions & 0 deletions color.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func (e *Engine) doDefineColor() {
e.colors = map[string]uint32{}
}
e.colors[name] = parseColorSpec(model, spec)
e.publishColor(name, e.colors[name])
}

// parseColorSpec turns a color model + spec string into 0xRRGGBB.
Expand Down Expand Up @@ -188,6 +189,7 @@ func (e *Engine) doColorlet() {
e.colors = map[string]uint32{}
}
e.colors[name] = c
e.publishColor(name, c)
}

// doPagecolor implements \pagecolor{expr}: fill the page background with the colour
Expand Down
68 changes: 68 additions & 0 deletions colorbridge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) the go-tex/engine authors.
// SPDX-License-Identifier: BSD-3-Clause

package engine

import (
"fmt"
"strings"
)

// This file publishes the engine's named colours in the form the color/xcolor
// packages store theirs, so a package that asks "what colour is this, exactly?"
// gets an answer.
//
// The engine resolves colour natively (see color.go) rather than loading xcolor,
// which is fine as long as nothing looks *inside* a colour. A drawing package
// does: pgf sets a stroke colour with \colorlet and then reads the result back —
//
// \expandafter\let\expandafter\pgf@temp\csname\string\color@pgfstrokecolor\endcsname
// \expandafter\pgf@setstrokecolor\pgf@temp % → #1#2#3#4#5, #4 = model, #5 = values
//
// — in order to hand the model and its values to its own driver. Without the
// stored form it reads whatever happens to follow and reports the colour model as
// unsupported, so no picture can set a colour at all.
//
// The stored form is xcolor's: the control sequence \color@<name> expands to five
// arguments, of which the fourth is the model and the fifth its comma-separated
// values. The engine keeps colours as RGB, so the model published is rgb with
// three 0–1 components — the model every driver understands.

// publishColor stores a colour under the name a colour-reading package expects,
// alongside the engine's own table. It is called wherever a colour gets a name.
func (e *Engine) publishColor(name string, c uint32) {
if name == "" {
return
}
// \string\color@<name> is the control sequence's *characters*, which is the
// name xcolor builds with \csname: a literal backslash, then "color@<name>".
e.define(`\color@`+name, &meaning{kind: mMacro, body: xcolorValue(c)}, true)
}

// xcolorValue renders 0xRRGGBB as xcolor's stored five arguments.
func xcolorValue(c uint32) []tok {
r := float64((c>>16)&0xff) / 255
g := float64((c>>8)&0xff) / 255
b := float64(c&0xff) / 255
spec := fmt.Sprintf("%s,%s,%s", colorComponent(r), colorComponent(g), colorComponent(b))
// The value is a token list, not text: the reader destructures it into five
// arguments, so the groups have to be real begin/end-group tokens and the
// leading \xcolor@ a real control sequence.
out := []tok{csTok("xcolor@")}
for _, group := range []string{"", "", "rgb", spec} {
out = append(out, chTok('{', catBegin))
for _, r := range group {
out = append(out, chTok(r, catOther))
}
out = append(out, chTok('}', catEnd))
}
return out
}

// colorComponent formats one 0–1 colour component the way a colour specification
// spells it: at most five decimals, with trailing zeros trimmed.
func colorComponent(v float64) string {
s := fmt.Sprintf("%.5f", v)
s = strings.TrimRight(s, "0")
return strings.TrimSuffix(s, ".")
}
138 changes: 130 additions & 8 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,11 @@ type Engine struct {
// (Run resets it to 0; class/package loading splices file bodies into e.base at
// e.bpos and scanning advances through them), which makes it a sound progress
// signal even while a heavy .cls is loading.
expandDepth int // >0 while an isolated expansion (\edef/\message) is running
progBpos int // e.bpos at the last observed forward progress
noProgSteps int // expansion steps since e.bpos last advanced
tightLimit int // no-progress ceiling (New sets tightLoopSteps; tests may adjust)
afterToken *tok // token saved by \afterassignment, inserted after the next one
expandDepth int // >0 while an isolated expansion (\edef/\message) is running
progBpos int // e.bpos at the last observed forward progress
noProgSteps int // expansion steps since e.bpos last advanced
tightLimit int // no-progress ceiling (New sets tightLoopSteps; tests may adjust)
}

const (
Expand Down Expand Up @@ -515,14 +516,39 @@ func (e *Engine) meaningOf(t tok) *meaning {
}

func (e *Engine) define(name string, m *meaning, global bool) {
if !global && len(e.groups) > 0 {
if global {
e.forgetSaved(0, 0, name)
} else if len(e.groups) > 0 {
e.save = append(e.save, saveItem{kind: 0, name: name, old: e.eq[name]})
}
e.eq[name] = m
}

// forgetSaved drops the pending restores for one quantity, which is what makes an
// assignment *global*: the value must outlive every group that is open, so a
// local assignment made earlier in one of them must no longer be restored over
// it at the closing brace. Without this, {\x=1 \global\x=2 } would leave \x at
// its value from before the group — and the idiom that carries a result out of a
// group (pgf's \pgf@process does exactly {…\global\pgf@x=\pgf@x}) would return
// nothing.
func (e *Engine) forgetSaved(kind, idx int, name string) {
if len(e.save) == 0 {
return
}
out := e.save[:0]
for _, it := range e.save {
if it.kind == kind && ((kind == 0 && it.name == name) || (kind != 0 && it.idx == idx)) {
continue
}
out = append(out, it)
}
e.save = out
}

func (e *Engine) setCount(i, v int, global bool) {
if !global && len(e.groups) > 0 {
if global {
e.forgetSaved(1, i, "")
} else if len(e.groups) > 0 {
e.save = append(e.save, saveItem{kind: 1, idx: i, oldi: e.count[i]})
}
e.count[i] = v
Expand All @@ -532,7 +558,9 @@ func (e *Engine) setDimen(i, v int, global bool) {
if i < 0 || i >= 256 {
return
}
if !global && len(e.groups) > 0 {
if global {
e.forgetSaved(3, i, "")
} else if len(e.groups) > 0 {
e.save = append(e.save, saveItem{kind: 3, idx: i, oldd: e.dimen[i]})
}
e.dimen[i] = v
Expand All @@ -542,7 +570,9 @@ func (e *Engine) setSkip(i int, v glueSpec, global bool) {
if i < 0 || i >= 256 {
return
}
if !global && len(e.groups) > 0 {
if global {
e.forgetSaved(4, i, "")
} else if len(e.groups) > 0 {
e.save = append(e.save, saveItem{kind: 4, idx: i, oldg: e.skip[i]})
}
e.skip[i] = v
Expand Down Expand Up @@ -1042,6 +1072,11 @@ func (e *Engine) execCS(t tok) bool {
e.fail("Undefined control sequence \\" + t.cs)
return false
}
if m.kind == mPrim && m.name == "afterassignment" {
m.prim(e)
return true
}
defer e.flushAfterAssignment(m)
switch m.kind {
case mCountRef:
e.countRefAssign(m.code, false) // \n=<v>
Expand Down Expand Up @@ -1169,12 +1204,30 @@ func (e *Engine) scanInt() int {
if m.kind == mCharDef {
return sign * m.code
}
// A box-register handle from \newbox is a register *number*: TeX
// allocates it with \chardef, so \box\mybox, \wd\mybox and
// \setbox\mybox all read it as the integer it stands for.
if m.kind == mBoxRef {
return sign * m.code
}
if m.kind == mCountRef {
return sign * e.count[m.code]
}
if m.kind == mPrim && m.name == "count" {
return sign * e.count[e.scanInt()]
}
// TeX coerces an internal dimension (or glue) to an integer: its
// value in scaled points. \number\pgf@x and \ifnum\wd0>0 both
// rely on it, and a package that computes with lengths uses it
// constantly.
if e.isInternalDimen(t) && m.name != "dimexpr" {
e.back(t)
v, _ := e.scanDimenValue(false)
return sign * v
}
if m.kind == mPrim && m.name == "catcode" {
return sign * int(e.catcode[rune(e.scanInt())])
}
if m.kind == mPrim && m.name == "numexpr" {
return sign * e.scanExpr(false)
}
Expand All @@ -1185,6 +1238,13 @@ func (e *Engine) scanInt() int {
}
}
}
// TeX's alphabetic constant: `<character> or `<single-character control
// sequence> is that character's code. It is how a source names a character
// it cannot write as a number — \catcode`\%=14, \lccode`\a=`\A,
// \chardef\bslash=`\\ — so a package that sets any catcode needs it.
if t.is('`', catOther) {
return sign * e.scanCharCode()
}
if !t.cs_ && t.ch >= '0' && t.ch <= '9' {
n := int(t.ch - '0')
for {
Expand All @@ -1205,6 +1265,68 @@ func (e *Engine) scanInt() int {
}
}

// assignmentPrims are the primitives that perform an assignment, after which a
// token saved by \afterassignment is inserted (TeX §1269). A register alias
// (\pgf@x=…) assigns too and is handled by kind, not by name.
var assignmentPrims = map[string]bool{
"def": true, "gdef": true, "edef": true, "xdef": true, "let": true,
"futurelet": true, "global": true, "chardef": true, "countdef": true,
"dimendef": true, "skipdef": true, "toksdef": true, "newcount": true,
"newdimen": true, "newskip": true, "newtoks": true, "newbox": true,
"count": true, "dimen": true, "skip": true, "toks": true, "catcode": true,
"advance": true, "multiply": true, "divide": true, "setbox": true,
"font": true, "hsize": true, "vsize": true, "parindent": true,
"baselineskip": true, "leftskip": true, "rightskip": true, "sfcode": true,
"hskip": true, "vskip": true, "wd": true, "ht": true, "dp": true,
"columnsep": true, "columnseprule": true,
}

// flushAfterAssignment inserts the token \afterassignment saved, once the
// assignment it was waiting for has been performed. TeX keeps exactly one such
// token, and it is inserted after the assignment, not before — which is what
// lets a macro see the value that was just assigned (pgf uses it to resume a
// scanner right after \let\next= has swallowed a token).
func (e *Engine) flushAfterAssignment(m *meaning) {
if e.afterToken == nil {
return
}
switch m.kind {
case mCountRef, mDimenRef, mSkipRef, mToksRef, mFont:
case mPrim:
if !assignmentPrims[m.name] {
return
}
default:
return
}
t := *e.afterToken
e.afterToken = nil
e.back(t)
}

// scanCharCode reads the character after a ` : a character token gives its own
// code, and a control sequence gives the code of its single character (TeX reads
// this token unexpanded, so `\a is the letter a even when \a is a macro). A
// multi-letter control sequence is not a character constant and yields zero.
func (e *Engine) scanCharCode() int {
t, ok := e.getNext()
if !ok {
return 0
}
code := 0
if t.cs_ {
r := []rune(t.cs)
if len(r) != 1 {
return 0
}
code = int(r[0])
} else {
code = int(t.ch)
}
e.skipOneOptSpace()
return code
}

// unitRatio maps a physical unit keyword to TeX's exact (num, denom) ratio to
// points (§458 set_conversion). pt/sp are handled specially in scanDimen.
var unitRatio = map[string][2]int{
Expand Down
30 changes: 29 additions & 1 deletion packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,11 +475,39 @@ func (e *Engine) doIfFileExists() {
}
}

// readBraceNameX reads a braced file name, expanding it as TeX's file-name
// scanner does: a package names the file to load through a macro (pgf loads its
// driver as \pgfutil@InputIfFileExists{\pgfsysdriver}), so an unexpanded name
// would never resolve.
func (e *Engine) readBraceNameX() string {
e.skipOptSpace()
t, ok := e.getXToken()
if !ok || t.cs_ || t.cat != catBegin {
if ok {
e.back(t)
}
return ""
}
var b []rune
for {
u, ok := e.getXToken()
if !ok || (!u.cs_ && u.cat == catEnd) {
break
}
if u.cs_ {
b = append(b, []rune(u.cs)...)
continue
}
b = append(b, u.ch)
}
return strings.TrimSpace(string(b))
}

// doInputIfFileExists implements \InputIfFileExists{name}{then}{else}: when name
// resolves it splices the file (with the then-code queued to run after it), else it
// runs the else-code.
func (e *Engine) doInputIfFileExists() {
name := e.readBraceName()
name := e.readBraceNameX()
then := e.readBraceToksRaw()
els := e.readBraceToksRaw()
data, _, ok := e.findTeXFile(name, []string{"", ".tex"})
Expand Down
Loading
Loading