Add InjectValues function

This will make CompileWithVariables obsolete, as you can use InjectValues in combination with the normal Compile instead.

This also introduces a breaking change with CompileWithVariables, as now invalid identifiers will return an error.
This commit is contained in:
David Vogel 2025-02-27 15:17:39 +01:00
parent 112898d1d7
commit 69bd0ed5b5
5 changed files with 102 additions and 16 deletions

17
cli.go
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.
// https://opensource.org/licenses/MIT
@ -80,22 +80,17 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err
return nil
}
// Compile takes a typst document from input, and renders it into the output writer.
// CompileWithVariables takes a typst document from input, and renders it into the output writer.
// The options parameter is optional.
//
// Additionally this will inject the given map of variables into the global scope of the typst document.
//
// Deprecated: You should use InjectValues in combination with the normal Compile method instead.
func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
varBuffer := bytes.Buffer{}
// TODO: Use io.pipe instead of a bytes.Buffer
enc := NewVariableEncoder(&varBuffer)
for k, v := range variables {
varBuffer.WriteString("#let " + CleanIdentifier(k) + " = ")
if err := enc.Encode(v); err != nil {
return fmt.Errorf("failed to encode variables with key %q: %w", k, err)
}
varBuffer.WriteRune('\n')
if err := InjectValues(&varBuffer, variables); err != nil {
return fmt.Errorf("failed to inject values into Typst markup: %w", err)
}
reader := io.MultiReader(&varBuffer, input)

View File

@ -1,3 +1,8 @@
// Copyright (c) 2024-2025 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package typst
import (
@ -22,8 +27,10 @@ func (i Image) MarshalTypstVariable() ([]byte, error) {
// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
// TODO: Consider using raw pixel encoding instead of PNG
var buf bytes.Buffer
buf.WriteString("image.decode(bytes((")
buf.WriteString("image.decode(bytes((") // TODO: Pass bytes directly to image once Typst 0.12.0 is not supported anymore
for _, b := range buffer.Bytes() {
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
}

View File

@ -33,18 +33,24 @@ func (p *testImage) Opaque() bool {
}
func TestImage(t *testing.T) {
img := &testImage{image.Rect(0, 0, 255, 255)}
img := &testImage{image.Rect(0, 0, 256, 256)}
// Wrap image.
typstImage := typst.Image{img}
cli := typst.CLI{}
r := bytes.NewBufferString(`= Image test
var r bytes.Buffer
#TestImage`)
if err := typst.InjectValues(&r, map[string]any{"TestImage": typstImage}); err != nil {
t.Fatalf("Failed to inject values into Typst markup: %v.", err)
}
if err := cli.CompileWithVariables(r, io.Discard, nil, map[string]any{"TestImage": typstImage}); err != nil {
r.WriteString(`= Image test
#TestImage`) // TODO: Add 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 {
t.Fatalf("Failed to compile document: %v.", err)
}
}

42
util.go Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2025 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package typst
import (
"fmt"
"io"
)
// InjectValues will write the given key-value pairs as Typst markup into output.
// 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 value in values will be marshaled according to VariableEncoder into equivalent Typst markup.
//
// Passing {"foo": 1, "bar": 60 * time.Second} as values will produce the following output:
//
// #let foo = 1
// #let bar = duration(seconds: 60)
func InjectValues(output io.Writer, values map[string]any) error {
enc := NewVariableEncoder(output)
for k, v := range values {
if !IsIdentifier(k) {
return fmt.Errorf("%q is not a valid identifier", k)
}
if _, err := output.Write([]byte("#let " + CleanIdentifier(k) + " = ")); err != nil {
return err
}
if err := enc.Encode(v); err != nil {
return fmt.Errorf("failed to encode variables with key %q: %w", k, err)
}
if _, err := output.Write([]byte("\n")); err != nil {
return err
}
}
return nil
}

36
util_test.go Normal file
View File

@ -0,0 +1,36 @@
package typst
import (
"bytes"
"testing"
"time"
)
func TestInjectValues(t *testing.T) {
type args struct {
values map[string]any
}
tests := []struct {
name string
args args
wantOutput string
wantErr bool
}{
{"empty", args{values: nil}, "", false},
{"nil", args{values: map[string]any{"foo": nil}}, "#let foo = none\n", false},
{"example", args{values: map[string]any{"foo": 1, "bar": 60 * time.Second}}, "#let foo = 1\n#let bar = duration(seconds: 60)\n", false},
{"invalid identifier", args{values: map[string]any{"foo😀": 1}}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output := &bytes.Buffer{}
if err := InjectValues(output, tt.args.values); (err != nil) != tt.wantErr {
t.Errorf("InjectValues() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotOutput := output.String(); gotOutput != tt.wantOutput {
t.Errorf("InjectValues() = %v, want %v", gotOutput, tt.wantOutput)
}
})
}
}