Rename Variable* to Value*

- Rename MarshalVariable to MarshalValue
- Rename NewVariableEncoder to NewValueEncoder
- Rename VariableEncoder to ValueEncoder
- Rename VariableMarshaler to ValueMarshaler
- Rename MarshalTypstVariable to MarshalTypstValue

There are now wrappers which ensure compatibility with code that still uses some of the old functions/types.

- Improve image_test.go by adding an assertion
- Rename all occurrences of Variable to Value
- Remove "TODO: Handle images..." as that's already working with the image wrapper
- Update README.md
This commit is contained in:
David Vogel 2025-02-27 18:07:46 +01:00
parent 7c87e3fee8
commit c3876b340b
8 changed files with 88 additions and 55 deletions

View File

@ -20,7 +20,7 @@ Use at your own discretion for production systems.
- PDF, SVG and PNG generation. - PDF, SVG and PNG generation.
- All Typst parameters are discoverable and documented in [cli-options.go](cli-options.go). - All Typst parameters are discoverable and documented in [cli-options.go](cli-options.go).
- Go-to-Typst Object Encoder: Seamlessly inject any Go values (Including `image.Image` with a [wrapper](image.go)) into Typst documents via the provided encoder. - Go-to-Typst Value Encoder: Seamlessly inject any Go values (Including `image.Image` with a [wrapper](image.go)) into Typst documents via the provided encoder.
- Errors from Typst CLI are returned as structured Go error objects with detailed information, such as line numbers and file paths. - Errors from Typst CLI are returned as structured Go error objects with detailed information, such as line numbers and file paths.
- Uses stdio; No temporary files will be created. - Uses stdio; No temporary files will be created.
- Good unit test coverage. - Good unit test coverage.

View File

@ -12,7 +12,7 @@ import (
func main() { func main() {
// Convert a time.Time value into Typst markup. // Convert a time.Time value into Typst markup.
date, err := typst.MarshalVariable(time.Now()) date, err := typst.MarshalValue(time.Now())
if err != nil { if err != nil {
log.Panicf("Failed to marshal date into Typst markup: %v", err) log.Panicf("Failed to marshal date into Typst markup: %v", err)
} }

View File

@ -15,10 +15,10 @@ import (
// Image can be used to encode any image.Image into a typst image. // Image can be used to encode any image.Image into a typst image.
// //
// For this, just wrap any image.Image with this type before passing it to MarshalVariable or a VariableEncoder. // For this, just wrap any image.Image with this type before passing it to MarshalValue or a ValueEncoder.
type Image struct{ image.Image } type Image struct{ image.Image }
func (i Image) MarshalTypstVariable() ([]byte, error) { func (i Image) MarshalTypstValue() ([]byte, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
if err := png.Encode(&buffer, i); err != nil { if err := png.Encode(&buffer, i); err != nil {

View File

@ -48,7 +48,9 @@ func TestImage(t *testing.T) {
r.WriteString(`= Image test r.WriteString(`= Image test
#TestImage`) // TODO: Add assertion for the image width and height as soon as it's possible to query that #TestImage
#assert(type(TestImage) == content, message: "TestImage is not of expected type: got " + str(type(TestImage)) + ", want content")`) // TODO: Add another assertion for the image width and height as soon as it's possible to query that
if err := cli.Compile(&r, io.Discard, nil); err != nil { if err := cli.Compile(&r, io.Discard, nil); err != nil {
t.Fatalf("Failed to compile document: %v.", err) t.Fatalf("Failed to compile document: %v.", err)

View File

@ -16,14 +16,14 @@ import (
// This can be used to inject Go values into typst documents. // This can be used to inject Go values into typst documents.
// //
// Every key in values needs to be a valid identifier, otherwise this function will return an error. // Every key in values needs to be a valid identifier, otherwise this function will return an error.
// Every value in values will be marshaled according to VariableEncoder into equivalent Typst markup. // Every value in values will be marshaled according to ValueEncoder into equivalent Typst markup.
// //
// Passing {"foo": 1, "bar": 60 * time.Second} as values will produce the following output: // Passing {"foo": 1, "bar": 60 * time.Second} as values will produce the following output:
// //
// #let foo = 1 // #let foo = 1
// #let bar = duration(seconds: 60) // #let bar = duration(seconds: 60)
func InjectValues(output io.Writer, values map[string]any) error { func InjectValues(output io.Writer, values map[string]any) error {
enc := NewVariableEncoder(output) enc := NewValueEncoder(output)
// We will have to iterate over the sorted list of map keys. // We will have to iterate over the sorted list of map keys.
// Otherwise the output is not deterministic, and tests will fail randomly. // Otherwise the output is not deterministic, and tests will fail randomly.
@ -36,7 +36,7 @@ func InjectValues(output io.Writer, values map[string]any) error {
return err return err
} }
if err := enc.Encode(v); err != nil { if err := enc.Encode(v); err != nil {
return fmt.Errorf("failed to encode variables with key %q: %w", k, err) return fmt.Errorf("failed to encode values with key %q: %w", k, err)
} }
if _, err := output.Write([]byte("\n")); err != nil { if _, err := output.Write([]byte("\n")); err != nil {
return err return err

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 David Vogel // Copyright (c) 2024-2025 David Vogel
// //
// This software is released under the MIT License. // This software is released under the MIT License.
// https://opensource.org/licenses/MIT // https://opensource.org/licenses/MIT
@ -18,11 +18,11 @@ import (
"time" "time"
) )
// MarshalVariable takes any go type and returns a typst markup representation as a byte slice. // MarshalValue takes any go type and returns a typst markup representation as a byte slice.
func MarshalVariable(v any) ([]byte, error) { func MarshalValue(v any) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := NewVariableEncoder(&buf) enc := NewValueEncoder(&buf)
if err := enc.Encode(v); err != nil { if err := enc.Encode(v); err != nil {
return nil, err return nil, err
} }
@ -30,37 +30,37 @@ func MarshalVariable(v any) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// VariableMarshaler can be implemented by types to support custom typst marshaling. // ValueMarshaler can be implemented by types to support custom Typst marshaling.
type VariableMarshaler interface { type ValueMarshaler interface {
MarshalTypstVariable() ([]byte, error) MarshalTypstValue() ([]byte, error)
} }
type VariableEncoder struct { type ValueEncoder struct {
indentLevel int indentLevel int
writer io.Writer writer io.Writer
} }
// NewVariableEncoder returns a new encoder that writes into w. // NewValueEncoder returns a new encoder that writes into w.
func NewVariableEncoder(w io.Writer) *VariableEncoder { func NewValueEncoder(w io.Writer) *ValueEncoder {
return &VariableEncoder{ return &ValueEncoder{
writer: w, writer: w,
} }
} }
func (e *VariableEncoder) Encode(v any) error { func (e *ValueEncoder) Encode(v any) error {
return e.marshal(reflect.ValueOf(v)) return e.marshal(reflect.ValueOf(v))
} }
func (e *VariableEncoder) writeString(s string) error { func (e *ValueEncoder) writeString(s string) error {
return e.writeBytes([]byte(s)) return e.writeBytes([]byte(s))
} }
func (e *VariableEncoder) writeRune(r rune) error { func (e *ValueEncoder) writeRune(r rune) error {
return e.writeBytes([]byte{byte(r)}) return e.writeBytes([]byte{byte(r)})
} }
func (e *VariableEncoder) writeStringLiteral(s []byte) error { func (e *ValueEncoder) writeStringLiteral(s []byte) error {
dst := make([]byte, 0, len(s)+5) dst := make([]byte, 0, len(s)+5)
dst = append(dst, '"') dst = append(dst, '"')
@ -85,7 +85,7 @@ func (e *VariableEncoder) writeStringLiteral(s []byte) error {
return e.writeBytes(dst) return e.writeBytes(dst)
} }
func (e *VariableEncoder) writeBytes(b []byte) error { func (e *ValueEncoder) writeBytes(b []byte) error {
if _, err := e.writer.Write(b); err != nil { if _, err := e.writer.Write(b); err != nil {
return fmt.Errorf("failed to write into writer: %w", err) return fmt.Errorf("failed to write into writer: %w", err)
} }
@ -93,11 +93,11 @@ func (e *VariableEncoder) writeBytes(b []byte) error {
return nil return nil
} }
func (e *VariableEncoder) writeIndentationCharacters() error { func (e *ValueEncoder) writeIndentationCharacters() error {
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel)) return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
} }
func (e *VariableEncoder) marshal(v reflect.Value) error { func (e *ValueEncoder) marshal(v reflect.Value) error {
if !v.IsValid() { if !v.IsValid() {
return e.writeString("none") return e.writeString("none")
//return fmt.Errorf("invalid reflect.Value %v", v) //return fmt.Errorf("invalid reflect.Value %v", v)
@ -134,8 +134,18 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
return nil return nil
} }
// TODO: Handle images, maybe create a wrapper type that does this if t.Implements(reflect.TypeFor[ValueMarshaler]()) {
if m, ok := v.Interface().(ValueMarshaler); ok {
bytes, err := m.MarshalTypstValue()
if err != nil {
return fmt.Errorf("error calling MarshalTypstValue for type %s: %w", t.String(), err)
}
return e.writeBytes(bytes)
}
return e.writeString("none")
}
// TODO: Remove this in a future update, it's only here for compatibility reasons
if t.Implements(reflect.TypeFor[VariableMarshaler]()) { if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
if m, ok := v.Interface().(VariableMarshaler); ok { if m, ok := v.Interface().(VariableMarshaler); ok {
bytes, err := m.MarshalTypstVariable() bytes, err := m.MarshalTypstVariable()
@ -222,11 +232,11 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
return err return err
} }
func (e *VariableEncoder) encodeString(v reflect.Value) error { func (e *ValueEncoder) encodeString(v reflect.Value) error {
return e.writeStringLiteral([]byte(v.String())) return e.writeStringLiteral([]byte(v.String()))
} }
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error { func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
if v.NumField() == 0 { if v.NumField() == 0 {
return e.writeString("()") return e.writeString("()")
} }
@ -276,7 +286,7 @@ func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) { func (e *ValueEncoder) resolveKeyName(v reflect.Value) (string, error) {
// From encoding/json/encode.go. // From encoding/json/encode.go.
if v.Kind() == reflect.String { if v.Kind() == reflect.String {
return v.String(), nil return v.String(), nil
@ -297,7 +307,7 @@ func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
return "", fmt.Errorf("unsupported map key type %q", v.Type().String()) return "", fmt.Errorf("unsupported map key type %q", v.Type().String())
} }
func (e *VariableEncoder) encodeMap(v reflect.Value) error { func (e *ValueEncoder) encodeMap(v reflect.Value) error {
if v.Len() == 0 { if v.Len() == 0 {
return e.writeString("()") return e.writeString("()")
} }
@ -357,7 +367,7 @@ func (e *VariableEncoder) encodeMap(v reflect.Value) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *VariableEncoder) EncodeByteSlice(bb []byte) error { func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
if err := e.writeString("bytes(("); err != nil { if err := e.writeString("bytes(("); err != nil {
return err return err
} }
@ -385,7 +395,7 @@ func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
return e.writeString("))") return e.writeString("))")
} }
func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error { func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
// Special case for byte slices. // Special case for byte slices.
if t.Elem().Kind() == reflect.Uint8 { if t.Elem().Kind() == reflect.Uint8 {
@ -417,7 +427,7 @@ func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *VariableEncoder) encodeArray(v reflect.Value) error { func (e *ValueEncoder) encodeArray(v reflect.Value) error {
if err := e.writeRune('('); err != nil { if err := e.writeRune('('); err != nil {
return err return err
} }
@ -443,7 +453,7 @@ func (e *VariableEncoder) encodeArray(v reflect.Value) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *VariableEncoder) encodeTime(t time.Time) error { func (e *ValueEncoder) encodeTime(t time.Time) error {
return e.writeString(fmt.Sprintf("datetime(year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d)", return e.writeString(fmt.Sprintf("datetime(year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d)",
t.Year(), t.Year(),
t.Month(), t.Month(),
@ -454,6 +464,6 @@ func (e *VariableEncoder) encodeTime(t time.Time) error {
)) ))
} }
func (e *VariableEncoder) encodeDuration(d time.Duration) error { func (e *ValueEncoder) encodeDuration(d time.Duration) error {
return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds())))) return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds()))))
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024 David Vogel // Copyright (c) 2024-2025 David Vogel
// //
// This software is released under the MIT License. // This software is released under the MIT License.
// https://opensource.org/licenses/MIT // https://opensource.org/licenses/MIT
@ -18,7 +18,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
func TestMarshalVariable(t *testing.T) { func TestMarshalValue(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
arg any arg any
@ -31,33 +31,33 @@ func TestMarshalVariable(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := typst.MarshalVariable(tt.arg) got, err := typst.MarshalValue(tt.arg)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("MarshalVariable() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("MarshalValue() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("MarshalVariable() = %v, want %v", got, tt.want) t.Errorf("MarshalValue() = %v, want %v", got, tt.want)
} }
}) })
} }
} }
type VariableMarshalerType []byte type ValueMarshalerType []byte
func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) { func (v ValueMarshalerType) MarshalTypstValue() ([]byte, error) {
result := append([]byte{'"'}, v...) result := append([]byte{'"'}, v...)
result = append(result, '"') result = append(result, '"')
return result, nil return result, nil
} }
type VariableMarshalerTypePointer []byte type ValueMarshalerTypePointer []byte
var variableMarshalerTypePointer = VariableMarshalerTypePointer("test") var valueMarshalerTypePointer = ValueMarshalerTypePointer("test")
var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil) var valueMarshalerTypePointerNil = ValueMarshalerTypePointer(nil)
func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) { func (v *ValueMarshalerTypePointer) MarshalTypstValue() ([]byte, error) {
if v != nil { if v != nil {
result := append([]byte{'"'}, *v...) result := append([]byte{'"'}, *v...)
result = append(result, '"') result = append(result, '"')
@ -87,7 +87,7 @@ func (v *TextMarshalerTypePointer) MarshalText() ([]byte, error) {
return nil, fmt.Errorf("no data") return nil, fmt.Errorf("no data")
} }
func TestVariableEncoder(t *testing.T) { func TestValueEncoder(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -156,11 +156,11 @@ func TestVariableEncoder(t *testing.T) {
{"byte slice 1", []byte{1}, false, `bytes((1,))`}, {"byte slice 1", []byte{1}, false, `bytes((1,))`},
{"byte slice empty", []byte{}, false, `bytes(())`}, {"byte slice empty", []byte{}, false, `bytes(())`},
{"byte slice nil", []byte(nil), false, `bytes(())`}, {"byte slice nil", []byte(nil), false, `bytes(())`},
{"MarshalTypstVariable value", VariableMarshalerType("test"), false, `"test"`}, {"MarshalTypstValue value", ValueMarshalerType("test"), false, `"test"`},
{"MarshalTypstVariable value nil", VariableMarshalerType(nil), false, `""`}, {"MarshalTypstValue value nil", ValueMarshalerType(nil), false, `""`},
{"MarshalTypstVariable pointer", &variableMarshalerTypePointer, false, `"test"`}, {"MarshalTypstValue pointer", &valueMarshalerTypePointer, false, `"test"`},
{"MarshalTypstVariable pointer nil", &variableMarshalerTypePointerNil, false, `""`}, {"MarshalTypstValue pointer nil", &valueMarshalerTypePointerNil, false, `""`},
{"MarshalTypstVariable nil pointer", struct{ A *VariableMarshalerTypePointer }{nil}, true, ``}, {"MarshalTypstValue nil pointer", struct{ A *ValueMarshalerTypePointer }{nil}, true, ``},
{"MarshalText value", TextMarshalerType("test"), false, `"test"`}, {"MarshalText value", TextMarshalerType("test"), false, `"test"`},
{"MarshalText value nil", TextMarshalerType(nil), false, `""`}, {"MarshalText value nil", TextMarshalerType(nil), false, `""`},
{"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`}, {"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`},
@ -179,12 +179,12 @@ func TestVariableEncoder(t *testing.T) {
t.Parallel() t.Parallel()
var result bytes.Buffer var result bytes.Buffer
vEnc := typst.NewVariableEncoder(&result) vEnc := typst.NewValueEncoder(&result)
err := vEnc.Encode(tt.params) err := vEnc.Encode(tt.params)
switch { switch {
case err != nil && !tt.wantErr: case err != nil && !tt.wantErr:
t.Fatalf("Failed to encode typst variables: %v", err) t.Fatalf("Failed to encode typst values: %v", err)
case err == nil && tt.wantErr: case err == nil && tt.wantErr:
t.Fatalf("Expected error, but got none") t.Fatalf("Expected error, but got none")
} }

View File

@ -0,0 +1,21 @@
// Copyright (c) 2025 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package typst
import "io"
// This exists for compatibility reasons.
// Deprecated: Use NewValueEncoder instead, as this will be removed in a future version.
func NewVariableEncoder(w io.Writer) *ValueEncoder { return NewValueEncoder(w) }
// Deprecated: Use MarshalValue instead, as this will be removed in a future version.
func MarshalVariable(v any) ([]byte, error) { return MarshalValue(v) }
// Deprecated: Use ValueMarshaler interface instead, as this will be removed in a future version.
type VariableMarshaler interface {
MarshalTypstVariable() ([]byte, error)
}