Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

31 changed files with 227 additions and 624 deletions

View File

@ -1,29 +0,0 @@
name: golangci-lint
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
# Optional: allow read access to pull request. Use with `only-new-issues` option.
# pull-requests: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
- name: Check out code
uses: actions/checkout@v4
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.64

View File

@ -10,7 +10,7 @@ jobs:
strategy: strategy:
matrix: matrix:
go-version: ['1.23.x'] go-version: ['1.23.x']
typst-version: ['0.12.0', '0.13.0', '0.13.1'] typst-version: ['0.12.0']
steps: steps:
- name: Install typst-cli ${{ matrix.typst-version }} from crates.io - name: Install typst-cli ${{ matrix.typst-version }} from crates.io

View File

@ -2,7 +2,6 @@
"cSpell.words": [ "cSpell.words": [
"Dadido", "Dadido",
"Foogaloo", "Foogaloo",
"golangci",
"typst", "typst",
"Vogel" "Vogel"
] ]

View File

@ -6,13 +6,9 @@ Its goal is to provide Go developers with a seamless, "Go-like" interface to Typ
## Stability and Compatibility ## Stability and Compatibility
`go-typst` is a work in progress, and the API may change as Typst evolves. `go-typst` is a work in progress, and the API may change as Typst evolves.
Supported Typst versions are tested by unit tests to ensure compatibility. Supported Typst versions are tested by unit tests to ensure compatibility:
**Supported Versions:** - **Supported Version:** Typst 0.12.0
- Typst 0.12.0
- Typst 0.13.0
- Typst 0.13.1
While breaking changes may occur, i aim to minimize disruptions. While breaking changes may occur, i aim to minimize disruptions.
Use at your own discretion for production systems. Use at your own discretion for production systems.
@ -21,7 +17,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 Value Encoder: Seamlessly inject any Go values (Including `image.Image` with a [wrapper](image.go)) into Typst documents via the provided encoder. - 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.
- 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

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -16,18 +16,9 @@ type OutputFormat string
const ( const (
OutputFormatAuto OutputFormat = "" OutputFormatAuto OutputFormat = ""
OutputFormatPDF OutputFormat = "pdf" OutputFormatPDF OutputFormat = "pdf"
OutputFormatPNG OutputFormat = "png" OutputFormatPNG OutputFormat = "png"
OutputFormatSVG OutputFormat = "svg" OutputFormatSVG OutputFormat = "svg"
OutputFormatHTML OutputFormat = "html" // this format is only available since 0.13.0
)
type PDFStandard string
const (
PDFStandard1_7 PDFStandard = "1.7" // PDF 1.7
PDFStandardA_2B PDFStandard = "a-2b" // PDF/A-2b
PDFStandardA_3B PDFStandard = "a-3b" // PDF/A-3b (Available since Typst 0.13.0 and later)
) )
type CLIOptions struct { type CLIOptions struct {
@ -50,10 +41,13 @@ type CLIOptions struct {
Format OutputFormat // The format of the output file, inferred from the extension by default. Format OutputFormat // The format of the output file, inferred from the extension by default.
PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144. PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144.
// One (or multiple) PDF standards that Typst will enforce conformance with. // One (or multiple comma-separated) PDF standards that Typst will enforce conformance with.
// //
// See typst.PDFStandard for possible values. // Possible values:
PDFStandards []PDFStandard //
// - 1.7: PDF 1.7
// - a-2b: PDF/A-2b
PDFStandard string
Custom []string // Custom command line options go here. Custom []string // Custom command line options go here.
} }
@ -76,7 +70,6 @@ func (c *CLIOptions) Args() (result []string) {
} }
paths += path paths += path
} }
result = append(result, "--font-path", paths)
} }
if c.IgnoreSystemFonts { if c.IgnoreSystemFonts {
@ -105,30 +98,15 @@ func (c *CLIOptions) Args() (result []string) {
if c.Format != OutputFormatAuto { if c.Format != OutputFormatAuto {
result = append(result, "-f", string(c.Format)) result = append(result, "-f", string(c.Format))
if c.Format == OutputFormatHTML {
// this is specific to version 0.13.0 where html
// is a feature than need explicit activation
// we must remove this when html becomes standard
result = append(result, "--features", "html")
}
} }
if c.PPI > 0 { if c.PPI > 0 {
result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10)) result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10))
} }
if len(c.PDFStandards) > 0 { if c.PDFStandard != "" {
var standards string result = append(result, "--pdf-standard", c.PDFStandard)
for i, standard := range c.PDFStandards {
if i > 0 {
standards += ","
}
standards += string(standard)
}
result = append(result, "--pdf-standard", standards)
} }
result = append(result, c.Custom...)
return return
} }

View File

@ -1,24 +0,0 @@
package typst_test
import (
"os"
"testing"
"github.com/Dadido3/go-typst"
)
func TestCliOptions(t *testing.T) {
o := typst.CLIOptions{
FontPaths: []string{"somepath/to/somewhere", "another/to/somewhere"},
}
args := o.Args()
if len(args) != 2 {
t.Errorf("wrong number of arguments, expected 2, got %d", len(args))
}
if "--font-path" != args[0] {
t.Error("wrong font path option, expected --font-path, got", args[0])
}
if "somepath/to/somewhere"+string(os.PathListSeparator)+"another/to/somewhere" != args[1] {
t.Error("wrong font path option, expected my two paths concatenated, got", args[1])
}
}

27
cli.go
View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -15,12 +15,12 @@ import (
// TODO: Add docker support to CLI, by calling docker run instead // TODO: Add docker support to CLI, by calling docker run instead
type CLI struct { type CLI struct {
ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used. ExecutablePath string // The typst executable path can be overridden here. Otherwise the default path will be used.
} }
// TODO: Add method for querying the Typst version resulting in a semver object // TODO: Add method for querying the typst version resulting in a semver object
// VersionString returns the version string as returned by Typst. // VersionString returns the version string as returned by typst.
func (c CLI) VersionString() (string, error) { func (c CLI) VersionString() (string, error) {
// Get path of executable. // Get path of executable.
execPath := ExecutablePath execPath := ExecutablePath
@ -46,7 +46,7 @@ func (c CLI) VersionString() (string, error) {
return output.String(), nil return output.String(), nil
} }
// Compile takes a Typst document from input, and renders it into the output writer. // Compile takes a typst document from input, and renders it into the output writer.
// The options parameter is optional. // The options parameter is optional.
func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error { func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error {
args := []string{"c"} args := []string{"c"}
@ -80,17 +80,22 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err
return nil return nil
} }
// CompileWithVariables takes a Typst document from input, and renders it into the output writer. // Compile takes a typst document from input, and renders it into the output writer.
// The options parameter is optional. // The options parameter is optional.
// //
// Additionally this will inject the given map of variables into the global scope of the Typst document. // 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 { func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
varBuffer := bytes.Buffer{} varBuffer := bytes.Buffer{}
if err := InjectValues(&varBuffer, variables); err != nil { // TODO: Use io.pipe instead of a bytes.Buffer
return fmt.Errorf("failed to inject values into Typst markup: %w", err)
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')
} }
reader := io.MultiReader(&varBuffer, input) reader := io.MultiReader(&varBuffer, input)

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -7,5 +7,5 @@
package typst package typst
// The path to the Typst executable. // The path to the typst executable.
var ExecutablePath = "typst" var ExecutablePath = "typst"

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -7,9 +7,9 @@
package typst package typst
// The path to the Typst executable. // The path to the typst executable.
// We assume the executable is in the current working directory. // We assume the executable is in the current working directory.
//var ExecutablePath = "." + string(filepath.Separator) + filepath.Join("typst.exe") //var ExecutablePath = "." + string(filepath.Separator) + filepath.Join("typst.exe")
// The path to the Typst executable. // The path to the typst executable.
var ExecutablePath = "typst.exe" var ExecutablePath = "typst.exe"

123
errors.go
View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -8,26 +8,14 @@ package typst
import ( import (
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
// ErrorDetails contains the details of a typst.Error. // Error represents a generic typst error.
type ErrorDetails struct {
Message string // The parsed error message.
Path string // Path of the Typst file where the error is located in. Zero value means that there is no further information.
Line int // Line number of the error. Zero value means that there is no further information.
Column int // Column of the error. Zero value means that there is no further information.
}
// Error represents an error as returned by Typst.
// This can contain multiple sub-errors or sub-warnings which are listed in the field Details.
type Error struct { type Error struct {
Inner error Inner error
Raw string // The raw output from stderr. Raw string // The raw output from stderr.
Message string // The parsed error message.
// Raw output parsed into errors and warnings.
Details []ErrorDetails
} }
func (e *Error) Error() string { func (e *Error) Error() string {
@ -38,43 +26,72 @@ func (e *Error) Unwrap() error {
return e.Inner return e.Inner
} }
var stderrRegex = regexp.MustCompile(`(?s)^(?<error>.+?)(?:(?:\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n)|(?:$))`) // ErrorWithPath represents a typst error that also contains information about its origin (filepath, line and column).
type ErrorWithPath struct {
Inner error
// ParseStderr will parse the given stderr output and return a typst.Error. Raw string // The raw error string as returned by the executable.
func ParseStderr(stderr string, inner error) error { Message string // Error message from typst.
err := Error{
Inner: inner,
Raw: stderr,
}
// Get all "blocks" ending with double new lines. Path string // Path of the typst file where the error is located in.
parts := strings.Split(stderr, "\n\n") Line int // Line number of the error.
parts = parts[:len(parts)-1] Column int // Column of the error.
}
for _, part := range parts {
if parsed := stderrRegex.FindStringSubmatch(part); parsed != nil { func (e *ErrorWithPath) Error() string {
var details ErrorDetails return e.Raw
}
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) && parsed[i] != "" {
details.Message = parsed[i] func (e *ErrorWithPath) Unwrap() error {
} return e.Inner
if i := stderrRegex.SubexpIndex("path"); i > 0 && i < len(parsed) && parsed[i] != "" { }
details.Path = parsed[i]
} var stderrRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n`)
if i := stderrRegex.SubexpIndex("line"); i > 0 && i < len(parsed) && parsed[i] != "" { var stderrWithPathRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n`)
if line, err := strconv.ParseInt(parsed[i], 10, 0); err == nil {
details.Line = int(line) // ParseStderr will parse the given stderr output and return a suitable error object.
} // Depending on the stderr message, this will return either a typst.Error or a typst.ErrorWithPath error.
} func ParseStderr(stderr string, inner error) error {
if i := stderrRegex.SubexpIndex("column"); i > 0 && i < len(parsed) && parsed[i] != "" { if parsed := stderrWithPathRegex.FindStringSubmatch(stderr); parsed != nil {
if column, err := strconv.ParseInt(parsed[i], 10, 0); err == nil { err := ErrorWithPath{
details.Column = int(column) Raw: stderr,
} Inner: inner,
} }
err.Details = append(err.Details, details) if i := stderrWithPathRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
} err.Message = parsed[i]
} }
if i := stderrWithPathRegex.SubexpIndex("path"); i > 0 && i < len(parsed) {
return &err err.Path = parsed[i]
}
if i := stderrWithPathRegex.SubexpIndex("line"); i > 0 && i < len(parsed) {
line, _ := strconv.ParseInt(parsed[i], 10, 0)
err.Line = int(line)
}
if i := stderrWithPathRegex.SubexpIndex("column"); i > 0 && i < len(parsed) {
column, _ := strconv.ParseInt(parsed[i], 10, 0)
err.Column = int(column)
}
return &err
}
if parsed := stderrRegex.FindStringSubmatch(stderr); parsed != nil {
err := Error{
Raw: stderr,
Inner: inner,
}
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
err.Message = parsed[i]
}
return &err
}
// Fall back to the raw error message.
return &Error{
Raw: stderr,
Inner: inner,
}
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -11,7 +11,6 @@ import (
"testing" "testing"
"github.com/Dadido3/go-typst" "github.com/Dadido3/go-typst"
"github.com/google/go-cmp/cmp"
) )
func TestErrors0(t *testing.T) { func TestErrors0(t *testing.T) {
@ -36,26 +35,22 @@ func TestErrors1(t *testing.T) {
if err := cli.Compile(r, &w, nil); err == nil { if err := cli.Compile(r, &w, nil); err == nil {
t.Fatalf("Expected error, but got nil") t.Fatalf("Expected error, but got nil")
} else { } else {
var errTypst *typst.Error var errWithPath *typst.ErrorWithPath
if errors.As(err, &errTypst) { if errors.As(err, &errWithPath) {
if len(errTypst.Details) != 1 { if errWithPath.Message != "assertion failed: Test" {
t.Fatalf("Expected error doesn't contain the expected number of detail entries. Got %v, want %v", len(errTypst.Details), 1) t.Errorf("Expected error with error message %q, got %q", "assertion failed: Test", errWithPath.Message)
} }
details := errTypst.Details[0] /*if errWithPath.Path != "" {
if details.Message != "error: assertion failed: Test" { t.Errorf("Expected error to point to path %q, got path %q", "", errWithPath.Path)
t.Errorf("Expected error with error message %q, got %q", "error: assertion failed: Test", details.Message)
}
/*if details.Path != "" {
t.Errorf("Expected error to point to path %q, got path %q", "", details.Path)
}*/ }*/
if details.Line != 3 { if errWithPath.Line != 3 {
t.Errorf("Expected error to point at line %d, got line %d", 3, details.Line) t.Errorf("Expected error to point at line %d, got line %d", 3, errWithPath.Line)
} }
if details.Column != 1 { if errWithPath.Column != 1 {
t.Errorf("Expected error to point at column %d, got column %d", 1, details.Column) t.Errorf("Expected error to point at column %d, got column %d", 1, errWithPath.Column)
} }
} else { } else {
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err) t.Errorf("Expected error type %T, got %T: %v", errWithPath, err, err)
} }
} }
} }
@ -75,108 +70,13 @@ func TestErrors2(t *testing.T) {
} else { } else {
var errTypst *typst.Error var errTypst *typst.Error
if errors.As(err, &errTypst) { if errors.As(err, &errTypst) {
if len(errTypst.Details) != 1 {
t.Fatalf("Expected error doesn't contain the expected number of detail entries. Got %v, want %v", len(errTypst.Details), 1)
}
details := errTypst.Details[0]
// Don't check the specific error message, as that may change over time. // Don't check the specific error message, as that may change over time.
// The expected message should be similar to: error: invalid value 'a' for '--pages <PAGES>': not a valid page number. // The expected message should be similar to: invalid value 'a' for '--pages <PAGES>': not a valid page number.
if details.Message == "" { if errTypst.Message == "" {
t.Errorf("Expected error message, got %q", details.Message) t.Errorf("Expected error message, got %q", errTypst.Message)
} }
} else { } else {
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err) t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
} }
} }
} }
func TestErrorParsing(t *testing.T) {
var tests = map[string]struct {
StdErr string // The original and raw stderr message.
ExpectedDetails []typst.ErrorDetails // Expected parsed result.
}{
"Typst 0.13.0 HTML warning + error": {
StdErr: "warning: html export is under active development and incomplete\n = hint: its behaviour may change at any time\n = hint: do not rely on this feature for production use cases\n = hint: see https://github.com/typst/typst/issues/5512 for more information\n\nerror: page configuration is not allowed inside of containers\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:1:1\n │\n1 │ #set page(width: 100mm, height: auto, margin: 5mm)\n │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n",
ExpectedDetails: []typst.ErrorDetails{
{
Message: "warning: html export is under active development and incomplete\n = hint: its behaviour may change at any time\n = hint: do not rely on this feature for production use cases\n = hint: see https://github.com/typst/typst/issues/5512 for more information",
},
{
Message: "error: page configuration is not allowed inside of containers",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
Line: 1,
Column: 1,
},
},
},
"Typst 0.13.0 error with path": {
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:12:34\n │\n12 │ - Test coverage of most features.#\n │ ^\n\n",
ExpectedDetails: []typst.ErrorDetails{
{
Message: "error: expected expression",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
Line: 12,
Column: 34,
},
},
},
"Typst 0.13.0 multiple errors with paths": {
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:11:53\n │\n11 │ - Uses stdio; No temporary files need to be created.#\n │ ^\n\nerror: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:12:34\n │\n12 │ - Test coverage of most features.#\n │ ^\n\n",
ExpectedDetails: []typst.ErrorDetails{
{
Message: "error: expected expression",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
Line: 11,
Column: 53,
},
{
Message: "error: expected expression",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
Line: 12,
Column: 34,
},
},
},
"Typst 0.13.0 stacked errors with paths": {
StdErr: "error: expected expression\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\test.typ:1:4\n │\n1 │ hey#\n │ ^\n\nhelp: error occurred while importing this module\n ┌─ \\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>:14:9\n │\n14 │ #include \"test.typ\"\n │ ^^^^^^^^^^\n\n",
ExpectedDetails: []typst.ErrorDetails{
{
Message: "error: expected expression",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\test.typ",
Line: 1,
Column: 4,
},
{
Message: "help: error occurred while importing this module",
Path: "\\\\?\\C:\\Users\\David Vogel\\Desktop\\Synced\\Go\\Libraries\\go-typst\\<stdin>",
Line: 14,
Column: 9,
},
},
},
"Typst 0.13.0 error without path": {
StdErr: "error: invalid value 'a' for '--pages <PAGES>': not a valid page number\n\nFor more information, try '--help'.\n",
ExpectedDetails: []typst.ErrorDetails{
{
Message: "error: invalid value 'a' for '--pages <PAGES>': not a valid page number",
},
},
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
result := typst.ParseStderr(tt.StdErr, nil)
var typstError *typst.Error
if errors.As(result, &typstError) {
if !cmp.Equal(typstError.Details, tt.ExpectedDetails) {
t.Errorf("Parsed details don't match expected details: %s", cmp.Diff(tt.ExpectedDetails, typstError.Details))
}
} else {
t.Errorf("Parsed error is not of type %T", typstError)
}
})
}
}

View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"bytes"
"log" "log"
"os" "os"
"time" "time"
@ -9,7 +8,7 @@ import (
"github.com/Dadido3/go-typst" "github.com/Dadido3/go-typst"
) )
// DataEntry contains data to be passed to Typst. // DataEntry contains fake data to be passed to typst.
type DataEntry struct { type DataEntry struct {
Name string Name string
Size struct{ X, Y, Z float64 } Size struct{ X, Y, Z float64 }
@ -26,28 +25,21 @@ var TestData = []DataEntry{
} }
func main() { func main() {
var markup bytes.Buffer typstCLI := typst.CLI{}
// Inject Go values as Typst markup. r, err := os.Open("template.typ")
if err := typst.InjectValues(&markup, map[string]any{"data": TestData, "customText": "This data is coming from a Go application."}); err != nil { if err != nil {
log.Panicf("Failed to inject values into Typst markup: %v.", err) log.Panicf("Failed to open template file for reading: %v.", err)
} }
defer r.Close()
// Import the template and invoke the template function with the custom data.
// Show is used to replace the current document with whatever content the template function in `template.typ` returns.
markup.WriteString(`
#import "template.typ": template
#show: doc => template(data, customText)`)
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
f, err := os.Create("output.pdf") f, err := os.Create("output.pdf")
if err != nil { if err != nil {
log.Panicf("Failed to create output file: %v.", err) log.Panicf("Failed to create output file: %v.", err)
} }
defer f.Close() defer f.Close()
typstCLI := typst.CLI{} if err := typstCLI.CompileWithVariables(r, f, nil, map[string]any{"Data": TestData}); err != nil {
if err := typstCLI.Compile(&markup, f, nil); err != nil {
log.Panicf("Failed to compile document: %v.", err) log.Panicf("Failed to compile document: %v.", err)
} }
} }

Binary file not shown.

View File

@ -1,12 +1,5 @@
// Prepare data that will be used as preview. #let Data = (
#let data = (
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (1, 2, 3)), (Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (1, 2, 3)),
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (10, 12, 15)), (Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (10, 12, 15)),
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (100, 109, 199, 200)), (Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (100, 109, 199, 200)),
) )
#let customText = "Hey, this is example data to test the template."
// Invoke the template with the preview data and replace this whole document with the result.
#import "template.typ": template
#show: doc => template(data, customText)

View File

@ -0,0 +1,33 @@
#set page(paper: "a4")
// Uncomment to use test data.
//#import "template-test-data.typ": Data
= List of items
#show table.cell.where(y: 0): strong
#set table(
stroke: (x, y) => if y == 0 {
(bottom: 0.7pt + black)
},
)
#table(
columns: 5,
table.header(
[Name],
[Size],
[Example box],
[Created],
[Numbers],
),
..for value in Data {
(
[#value.Name],
[#value.Size],
box(fill: black, width: 0.1mm * value.Size.X, height: 0.1mm * value.Size.Y),
value.Created.display(),
[#list(..for num in value.Numbers {([#num],)})],
)
}
)

View File

@ -1,13 +0,0 @@
# Passing values example
This example demonstrates how to pass values to Typst, which can be useful in rendering custom documents such as reports, invoices, and more.
## How it works
This example follows the [template pattern](https://typst.app/docs/tutorial/making-a-template/) described in the Typst documentation.
Here is a short overview of the files:
- [template.typ](template.typ) defines a Typst template function that constructs a document based on parameters.
- [main.go](main.go) shows how to convert/encode Go values into Typst markup, and how to call/render the template with these converted values.
- [template-preview.typ](template-preview.typ) also invokes the template while providing mock data.
This is useful when you want to preview, update or debug the template.

View File

@ -1,16 +0,0 @@
package main
import (
"testing"
)
// Run the example as a test.
func TestMain(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Error(r)
}
}()
main()
}

Binary file not shown.

View File

@ -1,36 +0,0 @@
#let template(data, customText) = {
set page(paper: "a4")
[= Example]
customText
[== List of items]
show table.cell.where(y: 0): strong
set table(
stroke: (x, y) => if y == 0 {
(bottom: 0.7pt + black)
},
)
table(
columns: 5,
table.header(
[Name],
[Size],
[Example box],
[Created],
[Numbers],
),
..for value in data {
(
[#value.Name],
[#value.Size],
box(fill: black, width: 0.1mm * value.Size.X, height: 0.1mm * value.Size.Y),
value.Created.display(),
[#list(..for num in value.Numbers {([#num],)})],
)
}
)
}

View File

@ -1,12 +0,0 @@
# Simple example
This example shows how to render Typst documents directly from strings in Go.
## The pros and cons
The main advantage of this method is that it's really easy to set up.
In the most simple case you build your Typst markup by concatenating strings, or by using `fmt.Sprintf`.
The downside is that the final Typst markup is only generated on demand.
This means that you can't easily use the existing Typst tooling to write, update or debug your Typst markup.
Especially as your your documents get more complex, you should switch to other methods.

View File

@ -1,37 +0,0 @@
package main
import (
"bytes"
"fmt"
"log"
"os"
"time"
"github.com/Dadido3/go-typst"
)
func main() {
// Convert a time.Time value into Typst markup.
date, err := typst.MarshalValue(time.Now())
if err != nil {
log.Panicf("Failed to marshal date into Typst markup: %v", err)
}
// Write Typst markup into buffer.
var markup bytes.Buffer
fmt.Fprintf(&markup, `= Hello world
This document was created at #%s.display() using typst-go.`, date)
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
f, err := os.Create("output.pdf")
if err != nil {
log.Panicf("Failed to create output file: %v.", err)
}
defer f.Close()
typstCLI := typst.CLI{}
if err := typstCLI.Compile(&markup, f, nil); err != nil {
log.Panic("failed to compile document: %w", err)
}
}

View File

@ -1,16 +0,0 @@
package main
import (
"testing"
)
// Run the example as a test.
func TestMain(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Error(r)
}
}()
main()
}

Binary file not shown.

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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
@ -11,7 +11,7 @@ import (
"github.com/smasher164/xid" "github.com/smasher164/xid"
) )
// CleanIdentifier will return the input cleaned up in a way so that it can safely be used as a Typst identifier. // CleanIdentifier will return the input cleaned up in a way so that it can safely be used as a typst identifier.
// This function will replace all illegal characters, which means collisions are possible in some cases. // This function will replace all illegal characters, which means collisions are possible in some cases.
// //
// See https://github.com/typst/typst/blob/76c24ee6e35715cd14bb892d7b6b8d775c680bf7/crates/typst-syntax/src/lexer.rs#L932 for details. // See https://github.com/typst/typst/blob/76c24ee6e35715cd14bb892d7b6b8d775c680bf7/crates/typst-syntax/src/lexer.rs#L932 for details.

View File

@ -1,8 +1,3 @@
// Copyright (c) 2024-2025 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package typst package typst
import ( import (
@ -13,12 +8,12 @@ import (
"strconv" "strconv"
) )
// 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 MarshalValue or a ValueEncoder. // For this, just wrap any image.Image with this type before passing it to MarshalVariable or a VariableEncoder.
type Image struct{ image.Image } type Image struct{ image.Image }
func (i Image) MarshalTypstValue() ([]byte, error) { func (i Image) MarshalTypstVariable() ([]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 {
@ -27,10 +22,8 @@ func (i Image) MarshalTypstValue() ([]byte, error) {
// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding // TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
// TODO: Consider using raw pixel encoding instead of PNG
var buf bytes.Buffer var buf bytes.Buffer
buf.WriteString("image.decode(bytes((") // TODO: Pass bytes directly to image once Typst 0.12.0 is not supported anymore buf.WriteString("image.decode(bytes((")
for _, b := range buffer.Bytes() { for _, b := range buffer.Bytes() {
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",") buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
} }

View File

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

47
util.go
View File

@ -1,47 +0,0 @@
// Copyright (c) 2025 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package typst
import (
"fmt"
"io"
"maps"
"slices"
)
// 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 ValueEncoder 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 := NewValueEncoder(output)
// We will have to iterate over the sorted list of map keys.
// Otherwise the output is not deterministic, and tests will fail randomly.
for _, k := range slices.Sorted(maps.Keys(values)) {
v := values[k]
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 values with key %q: %w", k, err)
}
if _, err := output.Write([]byte("\n")); err != nil {
return err
}
}
return nil
}

View File

@ -1,36 +0,0 @@
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 bar = duration(seconds: 60)\n#let foo = 1\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)
}
})
}
}

View File

@ -1,21 +0,0 @@
// 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)
}

View File

@ -1,4 +1,4 @@
// Copyright (c) 2024-2025 David Vogel // Copyright (c) 2024 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"
) )
// MarshalValue takes any Go type and returns a Typst markup representation as a byte slice. // MarshalVariable takes any go type and returns a typst markup representation as a byte slice.
func MarshalValue(v any) ([]byte, error) { func MarshalVariable(v any) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
enc := NewValueEncoder(&buf) enc := NewVariableEncoder(&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 MarshalValue(v any) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// ValueMarshaler can be implemented by types to support custom Typst marshaling. // VariableMarshaler can be implemented by types to support custom typst marshaling.
type ValueMarshaler interface { type VariableMarshaler interface {
MarshalTypstValue() ([]byte, error) MarshalTypstVariable() ([]byte, error)
} }
type ValueEncoder struct { type VariableEncoder struct {
indentLevel int indentLevel int
writer io.Writer writer io.Writer
} }
// NewValueEncoder returns a new encoder that writes into w. // NewVariableEncoder returns a new encoder that writes into w.
func NewValueEncoder(w io.Writer) *ValueEncoder { func NewVariableEncoder(w io.Writer) *VariableEncoder {
return &ValueEncoder{ return &VariableEncoder{
writer: w, writer: w,
} }
} }
func (e *ValueEncoder) Encode(v any) error { func (e *VariableEncoder) Encode(v any) error {
return e.marshal(reflect.ValueOf(v)) return e.marshal(reflect.ValueOf(v))
} }
func (e *ValueEncoder) writeString(s string) error { func (e *VariableEncoder) writeString(s string) error {
return e.writeBytes([]byte(s)) return e.writeBytes([]byte(s))
} }
func (e *ValueEncoder) writeRune(r rune) error { func (e *VariableEncoder) writeRune(r rune) error {
return e.writeBytes([]byte{byte(r)}) return e.writeBytes([]byte{byte(r)})
} }
func (e *ValueEncoder) writeStringLiteral(s []byte) error { func (e *VariableEncoder) 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 *ValueEncoder) writeStringLiteral(s []byte) error {
return e.writeBytes(dst) return e.writeBytes(dst)
} }
func (e *ValueEncoder) writeBytes(b []byte) error { func (e *VariableEncoder) 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 *ValueEncoder) writeBytes(b []byte) error {
return nil return nil
} }
func (e *ValueEncoder) writeIndentationCharacters() error { func (e *VariableEncoder) writeIndentationCharacters() error {
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel)) return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
} }
func (e *ValueEncoder) marshal(v reflect.Value) error { func (e *VariableEncoder) 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)
@ -113,7 +113,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil return nil
case *time.Time: case *time.Time:
if i == nil { if i == nil {
return e.writeString("none") e.writeString("none")
return nil
} }
if err := e.encodeTime(*i); err != nil { if err := e.encodeTime(*i); err != nil {
return err return err
@ -126,7 +127,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil return nil
case *time.Duration: case *time.Duration:
if i == nil { if i == nil {
return e.writeString("none") e.writeString("none")
return nil
} }
if err := e.encodeDuration(*i); err != nil { if err := e.encodeDuration(*i); err != nil {
return err return err
@ -134,18 +136,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil return nil
} }
if t.Implements(reflect.TypeFor[ValueMarshaler]()) { // TODO: Handle images, maybe create a wrapper type that does this
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()
@ -232,11 +224,11 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return err return err
} }
func (e *ValueEncoder) encodeString(v reflect.Value) error { func (e *VariableEncoder) encodeString(v reflect.Value) error {
return e.writeStringLiteral([]byte(v.String())) return e.writeStringLiteral([]byte(v.String()))
} }
func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error { func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
if v.NumField() == 0 { if v.NumField() == 0 {
return e.writeString("()") return e.writeString("()")
} }
@ -286,7 +278,7 @@ func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *ValueEncoder) resolveKeyName(v reflect.Value) (string, error) { func (e *VariableEncoder) 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
@ -307,7 +299,7 @@ func (e *ValueEncoder) 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 *ValueEncoder) encodeMap(v reflect.Value) error { func (e *VariableEncoder) encodeMap(v reflect.Value) error {
if v.Len() == 0 { if v.Len() == 0 {
return e.writeString("()") return e.writeString("()")
} }
@ -367,12 +359,12 @@ func (e *ValueEncoder) encodeMap(v reflect.Value) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *ValueEncoder) EncodeByteSlice(bb []byte) error { func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
if err := e.writeString("bytes(("); err != nil { if err := e.writeString("bytes(("); err != nil {
return err return err
} }
// TODO: Encode byte slice via base64 or similar and use a Typst package to convert it into the corresponding bytes type // TODO: Encode byte slice via base64 or similar and use a typst package to convert it into the corresponding bytes type
for i, b := range bb { for i, b := range bb {
if i > 0 { if i > 0 {
@ -395,7 +387,7 @@ func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
return e.writeString("))") return e.writeString("))")
} }
func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error { func (e *VariableEncoder) 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 {
@ -427,7 +419,7 @@ func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *ValueEncoder) encodeArray(v reflect.Value) error { func (e *VariableEncoder) encodeArray(v reflect.Value) error {
if err := e.writeRune('('); err != nil { if err := e.writeRune('('); err != nil {
return err return err
} }
@ -453,7 +445,7 @@ func (e *ValueEncoder) encodeArray(v reflect.Value) error {
return e.writeRune(')') return e.writeRune(')')
} }
func (e *ValueEncoder) encodeTime(t time.Time) error { func (e *VariableEncoder) 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(),
@ -464,6 +456,6 @@ func (e *ValueEncoder) encodeTime(t time.Time) error {
)) ))
} }
func (e *ValueEncoder) encodeDuration(d time.Duration) error { func (e *VariableEncoder) 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-2025 David Vogel // Copyright (c) 2024 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 TestMarshalValue(t *testing.T) { func TestMarshalVariable(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
arg any arg any
@ -31,33 +31,33 @@ func TestMarshalValue(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.MarshalValue(tt.arg) got, err := typst.MarshalVariable(tt.arg)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("MarshalValue() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("MarshalVariable() error = %v, wantErr %v", err, tt.wantErr)
return return
} }
if !reflect.DeepEqual(got, tt.want) { if !reflect.DeepEqual(got, tt.want) {
t.Errorf("MarshalValue() = %v, want %v", got, tt.want) t.Errorf("MarshalVariable() = %v, want %v", got, tt.want)
} }
}) })
} }
} }
type ValueMarshalerType []byte type VariableMarshalerType []byte
func (v ValueMarshalerType) MarshalTypstValue() ([]byte, error) { func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) {
result := append([]byte{'"'}, v...) result := append([]byte{'"'}, v...)
result = append(result, '"') result = append(result, '"')
return result, nil return result, nil
} }
type ValueMarshalerTypePointer []byte type VariableMarshalerTypePointer []byte
var valueMarshalerTypePointer = ValueMarshalerTypePointer("test") var variableMarshalerTypePointer = VariableMarshalerTypePointer("test")
var valueMarshalerTypePointerNil = ValueMarshalerTypePointer(nil) var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil)
func (v *ValueMarshalerTypePointer) MarshalTypstValue() ([]byte, error) { func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]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 TestValueEncoder(t *testing.T) { func TestVariableEncoder(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -156,11 +156,11 @@ func TestValueEncoder(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(())`},
{"MarshalTypstValue value", ValueMarshalerType("test"), false, `"test"`}, {"MarshalTypstVariable value", VariableMarshalerType("test"), false, `"test"`},
{"MarshalTypstValue value nil", ValueMarshalerType(nil), false, `""`}, {"MarshalTypstVariable value nil", VariableMarshalerType(nil), false, `""`},
{"MarshalTypstValue pointer", &valueMarshalerTypePointer, false, `"test"`}, {"MarshalTypstVariable pointer", &variableMarshalerTypePointer, false, `"test"`},
{"MarshalTypstValue pointer nil", &valueMarshalerTypePointerNil, false, `""`}, {"MarshalTypstVariable pointer nil", &variableMarshalerTypePointerNil, false, `""`},
{"MarshalTypstValue nil pointer", struct{ A *ValueMarshalerTypePointer }{nil}, true, ``}, {"MarshalTypstVariable nil pointer", struct{ A *VariableMarshalerTypePointer }{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 TestValueEncoder(t *testing.T) {
t.Parallel() t.Parallel()
var result bytes.Buffer var result bytes.Buffer
vEnc := typst.NewValueEncoder(&result) vEnc := typst.NewVariableEncoder(&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 values: %v", err) t.Fatalf("Failed to encode typst variables: %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")
} }
@ -199,7 +199,7 @@ func TestValueEncoder(t *testing.T) {
input := strings.NewReader("#" + result.String()) input := strings.NewReader("#" + result.String())
var output bytes.Buffer var output bytes.Buffer
if err := typstCLI.Compile(input, &output, nil); err != nil { if err := typstCLI.Compile(input, &output, nil); err != nil {
t.Errorf("Failed to compile generated Typst markup: %v", err) t.Errorf("Failed to compile generated typst markup: %v", err)
} }
} }
}) })