Compare commits

..

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

30 changed files with 140 additions and 443 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:
matrix:
go-version: ['1.23.x']
typst-version: ['0.12.0', '0.13.0', '0.13.1']
typst-version: ['0.12.0', '0.13.0']
steps:
- name: Install typst-cli ${{ matrix.typst-version }} from crates.io

View File

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

View File

@ -12,7 +12,6 @@ Supported Typst versions are tested by unit tests to ensure compatibility.
- Typst 0.12.0
- Typst 0.13.0
- Typst 0.13.1
While breaking changes may occur, i aim to minimize disruptions.
Use at your own discretion for production systems.
@ -21,7 +20,7 @@ Use at your own discretion for production systems.
- PDF, SVG and PNG generation.
- 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.
- Uses stdio; No temporary files will be created.
- 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.
// https://opensource.org/licenses/MIT
@ -22,14 +22,6 @@ const (
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 {
Root string // Configures the project root (for absolute paths).
Input map[string]string // String key-value pairs visible through `sys.inputs`.
@ -50,10 +42,13 @@ type CLIOptions struct {
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.
// 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.
PDFStandards []PDFStandard
// Possible values:
//
// - 1.7: PDF 1.7
// - a-2b: PDF/A-2b
PDFStandard string
Custom []string // Custom command line options go here.
}
@ -117,18 +112,9 @@ func (c *CLIOptions) Args() (result []string) {
result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10))
}
if len(c.PDFStandards) > 0 {
var standards string
for i, standard := range c.PDFStandards {
if i > 0 {
standards += ","
}
standards += string(standard)
}
result = append(result, "--pdf-standard", standards)
if c.PDFStandard != "" {
result = append(result, "--pdf-standard", c.PDFStandard)
}
result = append(result, c.Custom...)
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.
// https://opensource.org/licenses/MIT
@ -15,12 +15,12 @@ import (
// TODO: Add docker support to CLI, by calling docker run instead
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) {
// Get path of executable.
execPath := ExecutablePath
@ -46,7 +46,7 @@ func (c CLI) VersionString() (string, error) {
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.
func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error {
args := []string{"c"}
@ -80,17 +80,22 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err
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.
//
// 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.
// Additionally this will inject the given map of variables into the global scope of the typst document.
func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
varBuffer := bytes.Buffer{}
if err := InjectValues(&varBuffer, variables); err != nil {
return fmt.Errorf("failed to inject values into Typst markup: %w", err)
// 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')
}
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.
// https://opensource.org/licenses/MIT
@ -7,5 +7,5 @@
package typst
// The path to the Typst executable.
// The path to the typst executable.
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.
// https://opensource.org/licenses/MIT
@ -7,9 +7,9 @@
package typst
// The path to the Typst executable.
// The path to the typst executable.
// We assume the executable is in the current working directory.
//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"

View File

@ -14,13 +14,13 @@ import (
// ErrorDetails contains the details of a 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.
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.
// Error represents a typst error.
// This can contain multiple sub-errors or sub-warnings.
type Error struct {
Inner error

View File

@ -1,7 +1,6 @@
package main
import (
"bytes"
"log"
"os"
"time"
@ -9,7 +8,7 @@ import (
"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 {
Name string
Size struct{ X, Y, Z float64 }
@ -26,28 +25,21 @@ var TestData = []DataEntry{
}
func main() {
var markup bytes.Buffer
typstCLI := typst.CLI{}
// Inject Go values as Typst markup.
if err := typst.InjectValues(&markup, map[string]any{"data": TestData, "customText": "This data is coming from a Go application."}); err != nil {
log.Panicf("Failed to inject values into Typst markup: %v.", err)
r, err := os.Open("template.typ")
if err != nil {
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")
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 {
if err := typstCLI.CompileWithVariables(r, f, nil, map[string]any{"Data": TestData}); err != nil {
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: (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)),
)
#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.
// https://opensource.org/licenses/MIT
@ -11,7 +11,7 @@ import (
"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.
//
// 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
import (
@ -13,12 +8,12 @@ import (
"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 }
func (i Image) MarshalTypstValue() ([]byte, error) {
func (i Image) MarshalTypstVariable() ([]byte, error) {
var buffer bytes.Buffer
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: Consider using raw pixel encoding instead of PNG
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() {
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
}

View File

@ -33,26 +33,18 @@ func (p *testImage) Opaque() bool {
}
func TestImage(t *testing.T) {
img := &testImage{image.Rect(0, 0, 256, 256)}
img := &testImage{image.Rect(0, 0, 255, 255)}
// Wrap image.
typstImage := typst.Image{img}
cli := typst.CLI{}
var r bytes.Buffer
r := bytes.NewBufferString(`= Image test
if err := typst.InjectValues(&r, map[string]any{"TestImage": typstImage}); err != nil {
t.Fatalf("Failed to inject values into Typst markup: %v.", err)
}
#TestImage`)
r.WriteString(`= Image test
#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.CompileWithVariables(r, io.Discard, nil, map[string]any{"TestImage": typstImage}); err != nil {
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.
// https://opensource.org/licenses/MIT
@ -18,11 +18,11 @@ import (
"time"
)
// MarshalValue takes any Go type and returns a Typst markup representation as a byte slice.
func MarshalValue(v any) ([]byte, error) {
// MarshalVariable takes any go type and returns a typst markup representation as a byte slice.
func MarshalVariable(v any) ([]byte, error) {
var buf bytes.Buffer
enc := NewValueEncoder(&buf)
enc := NewVariableEncoder(&buf)
if err := enc.Encode(v); err != nil {
return nil, err
}
@ -30,37 +30,37 @@ func MarshalValue(v any) ([]byte, error) {
return buf.Bytes(), nil
}
// ValueMarshaler can be implemented by types to support custom Typst marshaling.
type ValueMarshaler interface {
MarshalTypstValue() ([]byte, error)
// VariableMarshaler can be implemented by types to support custom typst marshaling.
type VariableMarshaler interface {
MarshalTypstVariable() ([]byte, error)
}
type ValueEncoder struct {
type VariableEncoder struct {
indentLevel int
writer io.Writer
}
// NewValueEncoder returns a new encoder that writes into w.
func NewValueEncoder(w io.Writer) *ValueEncoder {
return &ValueEncoder{
// NewVariableEncoder returns a new encoder that writes into w.
func NewVariableEncoder(w io.Writer) *VariableEncoder {
return &VariableEncoder{
writer: w,
}
}
func (e *ValueEncoder) Encode(v any) error {
func (e *VariableEncoder) Encode(v any) error {
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))
}
func (e *ValueEncoder) writeRune(r rune) error {
func (e *VariableEncoder) writeRune(r rune) error {
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 = append(dst, '"')
@ -85,7 +85,7 @@ func (e *ValueEncoder) writeStringLiteral(s []byte) error {
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 {
return fmt.Errorf("failed to write into writer: %w", err)
}
@ -93,11 +93,11 @@ func (e *ValueEncoder) writeBytes(b []byte) error {
return nil
}
func (e *ValueEncoder) writeIndentationCharacters() error {
func (e *VariableEncoder) writeIndentationCharacters() error {
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() {
return e.writeString("none")
//return fmt.Errorf("invalid reflect.Value %v", v)
@ -113,7 +113,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil
case *time.Time:
if i == nil {
return e.writeString("none")
e.writeString("none")
return nil
}
if err := e.encodeTime(*i); err != nil {
return err
@ -126,7 +127,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil
case *time.Duration:
if i == nil {
return e.writeString("none")
e.writeString("none")
return nil
}
if err := e.encodeDuration(*i); err != nil {
return err
@ -134,18 +136,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return nil
}
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: Handle images, maybe create a wrapper type that does this
// TODO: Remove this in a future update, it's only here for compatibility reasons
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
if m, ok := v.Interface().(VariableMarshaler); ok {
bytes, err := m.MarshalTypstVariable()
@ -232,11 +224,11 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
return err
}
func (e *ValueEncoder) encodeString(v reflect.Value) error {
func (e *VariableEncoder) encodeString(v reflect.Value) error {
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 {
return e.writeString("()")
}
@ -286,7 +278,7 @@ func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
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.
if v.Kind() == reflect.String {
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())
}
func (e *ValueEncoder) encodeMap(v reflect.Value) error {
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
if v.Len() == 0 {
return e.writeString("()")
}
@ -367,12 +359,12 @@ func (e *ValueEncoder) encodeMap(v reflect.Value) error {
return e.writeRune(')')
}
func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
if err := e.writeString("bytes(("); err != nil {
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 {
if i > 0 {
@ -395,7 +387,7 @@ func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
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.
if t.Elem().Kind() == reflect.Uint8 {
@ -427,7 +419,7 @@ func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
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 {
return err
}
@ -453,7 +445,7 @@ func (e *ValueEncoder) encodeArray(v reflect.Value) error {
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)",
t.Year(),
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()))))
}

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.
// https://opensource.org/licenses/MIT
@ -18,7 +18,7 @@ import (
"github.com/google/go-cmp/cmp"
)
func TestMarshalValue(t *testing.T) {
func TestMarshalVariable(t *testing.T) {
tests := []struct {
name string
arg any
@ -31,33 +31,33 @@ func TestMarshalValue(t *testing.T) {
}
for _, tt := range tests {
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 {
t.Errorf("MarshalValue() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("MarshalVariable() error = %v, wantErr %v", err, tt.wantErr)
return
}
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(result, '"')
return result, nil
}
type ValueMarshalerTypePointer []byte
type VariableMarshalerTypePointer []byte
var valueMarshalerTypePointer = ValueMarshalerTypePointer("test")
var valueMarshalerTypePointerNil = ValueMarshalerTypePointer(nil)
var variableMarshalerTypePointer = VariableMarshalerTypePointer("test")
var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil)
func (v *ValueMarshalerTypePointer) MarshalTypstValue() ([]byte, error) {
func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) {
if v != nil {
result := append([]byte{'"'}, *v...)
result = append(result, '"')
@ -87,7 +87,7 @@ func (v *TextMarshalerTypePointer) MarshalText() ([]byte, error) {
return nil, fmt.Errorf("no data")
}
func TestValueEncoder(t *testing.T) {
func TestVariableEncoder(t *testing.T) {
tests := []struct {
name string
@ -156,11 +156,11 @@ func TestValueEncoder(t *testing.T) {
{"byte slice 1", []byte{1}, false, `bytes((1,))`},
{"byte slice empty", []byte{}, false, `bytes(())`},
{"byte slice nil", []byte(nil), false, `bytes(())`},
{"MarshalTypstValue value", ValueMarshalerType("test"), false, `"test"`},
{"MarshalTypstValue value nil", ValueMarshalerType(nil), false, `""`},
{"MarshalTypstValue pointer", &valueMarshalerTypePointer, false, `"test"`},
{"MarshalTypstValue pointer nil", &valueMarshalerTypePointerNil, false, `""`},
{"MarshalTypstValue nil pointer", struct{ A *ValueMarshalerTypePointer }{nil}, true, ``},
{"MarshalTypstVariable value", VariableMarshalerType("test"), false, `"test"`},
{"MarshalTypstVariable value nil", VariableMarshalerType(nil), false, `""`},
{"MarshalTypstVariable pointer", &variableMarshalerTypePointer, false, `"test"`},
{"MarshalTypstVariable pointer nil", &variableMarshalerTypePointerNil, false, `""`},
{"MarshalTypstVariable nil pointer", struct{ A *VariableMarshalerTypePointer }{nil}, true, ``},
{"MarshalText value", TextMarshalerType("test"), false, `"test"`},
{"MarshalText value nil", TextMarshalerType(nil), false, `""`},
{"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`},
@ -179,12 +179,12 @@ func TestValueEncoder(t *testing.T) {
t.Parallel()
var result bytes.Buffer
vEnc := typst.NewValueEncoder(&result)
vEnc := typst.NewVariableEncoder(&result)
err := vEnc.Encode(tt.params)
switch {
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:
t.Fatalf("Expected error, but got none")
}
@ -199,7 +199,7 @@ func TestValueEncoder(t *testing.T) {
input := strings.NewReader("#" + result.String())
var output bytes.Buffer
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)
}
}
})