Compare commits

...

22 Commits
v0.1.0 ... main

Author SHA1 Message Date
David Vogel
bdbda5f874
Merge pull request #4 from Dadido3/typst-0.13.1
Some checks failed
golangci-lint / lint (push) Successful in 2m48s
test / test (1.23.x, 0.12.0) (push) Failing after 7s
test / test (1.23.x, 0.13.0) (push) Failing after 4s
test / test (1.23.x, 0.13.1) (push) Failing after 4s
Add Typst 0.13.1 to Compatibility list
2025-03-07 14:28:14 +01:00
e1879e4d36 Add Typst 0.13.1 to Compatibility list 2025-03-07 13:56:11 +01:00
ab3cee4666 Correct capitalization of Typst and Go 2025-02-27 18:23:30 +01:00
c3876b340b 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
2025-02-27 18:07:46 +01:00
7c87e3fee8 Fix missing error handling in VariableEncoder.marshal 2025-02-27 17:35:43 +01:00
4ee94ec233 Add golangci-lint to GitHub actions 2025-02-27 17:29:42 +01:00
6ecfb61e5b Make TestInjectValues deterministic 2025-02-27 16:28:19 +01:00
2a1d2990e7 Add simple example 2025-02-27 16:08:02 +01:00
9268a6691e Rework passing-values example
This changes the example to use the template pattern; we now have a single Typst file containing a template function.
Instead of loading and rendering the template, we will now generate temporary Typst markup that will import the template function and call it with custom data.

This also means that it's pretty easy to test, preview and debug the template.typ outside of go-typst.
All that is needed is another Typst file which will call the template with mock data.

Also, we now use Compile instead of CompileWithVariables and inject encoded Go values into our temporary markup.
2025-02-27 16:07:34 +01:00
69bd0ed5b5 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.
2025-02-27 15:17:39 +01:00
112898d1d7 Rename example from passing-objects to passing-values 2025-02-27 10:35:01 +01:00
0668d15070 Add PDFStandard type and constants
Also rename the PDFStandard field in CLIOptions to PDFStandards, and change its type from string to []PDFStandard.

This is a breaking change.
2025-02-26 17:27:24 +01:00
c2d62b2373 Fix Custom command line options 2025-02-26 17:01:44 +01:00
David Vogel
d7fc966a42
Merge pull request #3 from faide/fontPath
Font path add unit test
2025-02-26 16:47:19 +01:00
Florent Aide
2b21d37fce Merge remote-tracking branch 'origin' into fontPath 2025-02-26 16:26:32 +01:00
Florent Aide
fb12dfb9fa add unit test for the font-path option 2025-02-26 16:20:36 +01:00
David Vogel
81bf84c51b
Merge pull request #2 from faide/fontPath
Add support for font-path option
2025-02-26 16:08:16 +01:00
Florent Aide
0e2bdda951 Add support for font-path option 2025-02-26 15:35:15 +01:00
c67b56bc06 Add Typst 0.13.0 to testing with GitHub actions
- Update README.md
2025-02-24 22:34:21 +01:00
648c449890 Simplify and improve stderr parsing
- Allow multiple errors and warnings
- Remove ErrorWithPath, which is now replaced by Error
- Simplify parsing, allow multiple errors
- Add more tests for stderr parsing
2025-02-24 22:32:27 +01:00
David Vogel
188f5c36cb
Merge pull request #1 from faide/0.13.0
add support for 0.13.0 html feature
2025-02-24 18:01:04 +01:00
Florent Aide
2f45207c40 add support for 0.13.0 html feature 2025-02-24 17:04:35 +01:00
31 changed files with 622 additions and 225 deletions

29
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,29 @@
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'] typst-version: ['0.12.0', '0.13.0', '0.13.1']
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,6 +2,7 @@
"cSpell.words": [ "cSpell.words": [
"Dadido", "Dadido",
"Foogaloo", "Foogaloo",
"golangci",
"typst", "typst",
"Vogel" "Vogel"
] ]

View File

@ -6,9 +6,13 @@ 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 Version:** Typst 0.12.0 **Supported Versions:**
- 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.
@ -17,7 +21,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

@ -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
@ -16,9 +16,18 @@ 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 {
@ -41,13 +50,10 @@ 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 comma-separated) PDF standards that Typst will enforce conformance with. // One (or multiple) PDF standards that Typst will enforce conformance with.
// //
// Possible values: // See typst.PDFStandard for 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.
} }
@ -70,6 +76,7 @@ func (c *CLIOptions) Args() (result []string) {
} }
paths += path paths += path
} }
result = append(result, "--font-path", paths)
} }
if c.IgnoreSystemFonts { if c.IgnoreSystemFonts {
@ -98,15 +105,30 @@ 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 c.PDFStandard != "" { if len(c.PDFStandards) > 0 {
result = append(result, "--pdf-standard", c.PDFStandard) var standards string
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
} }

24
cli-options_test.go Normal file
View File

@ -0,0 +1,24 @@
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 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
@ -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,22 +80,17 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err
return nil 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. // 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{}
// TODO: Use io.pipe instead of a bytes.Buffer if err := InjectValues(&varBuffer, variables); err != nil {
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 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
@ -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 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
@ -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"

119
errors.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. // This software is released under the MIT License.
// https://opensource.org/licenses/MIT // https://opensource.org/licenses/MIT
@ -8,14 +8,26 @@ package typst
import ( import (
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
// Error represents a generic typst error. // 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.
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 {
@ -26,72 +38,43 @@ func (e *Error) Unwrap() error {
return e.Inner return e.Inner
} }
// ErrorWithPath represents a typst error that also contains information about its origin (filepath, line and column). var stderrRegex = regexp.MustCompile(`(?s)^(?<error>.+?)(?:(?:\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n)|(?:$))`)
type ErrorWithPath struct {
Inner error
Raw string // The raw error string as returned by the executable. // ParseStderr will parse the given stderr output and return a typst.Error.
Message string // Error message from typst.
Path string // Path of the typst file where the error is located in.
Line int // Line number of the error.
Column int // Column of the error.
}
func (e *ErrorWithPath) Error() string {
return e.Raw
}
func (e *ErrorWithPath) Unwrap() error {
return e.Inner
}
var stderrRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n`)
var stderrWithPathRegex = regexp.MustCompile(`(?s)^error: (?<error>.+?)\n\s+┌─ (?<path>.+?):(?<line>\d+):(?<column>\d+)\n`)
// 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 { func ParseStderr(stderr string, inner error) error {
if parsed := stderrWithPathRegex.FindStringSubmatch(stderr); parsed != nil { err := Error{
err := ErrorWithPath{
Raw: stderr,
Inner: inner,
}
if i := stderrWithPathRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
err.Message = parsed[i]
}
if i := stderrWithPathRegex.SubexpIndex("path"); i > 0 && i < len(parsed) {
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, Inner: inner,
Raw: stderr,
} }
// Get all "blocks" ending with double new lines.
parts := strings.Split(stderr, "\n\n")
parts = parts[:len(parts)-1]
for _, part := range parts {
if parsed := stderrRegex.FindStringSubmatch(part); parsed != nil {
var details ErrorDetails
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) && parsed[i] != "" {
details.Message = parsed[i]
}
if i := stderrRegex.SubexpIndex("path"); i > 0 && i < len(parsed) && parsed[i] != "" {
details.Path = parsed[i]
}
if i := stderrRegex.SubexpIndex("line"); i > 0 && i < len(parsed) && parsed[i] != "" {
if line, err := strconv.ParseInt(parsed[i], 10, 0); err == nil {
details.Line = int(line)
}
}
if i := stderrRegex.SubexpIndex("column"); i > 0 && i < len(parsed) && parsed[i] != "" {
if column, err := strconv.ParseInt(parsed[i], 10, 0); err == nil {
details.Column = int(column)
}
}
err.Details = append(err.Details, details)
}
}
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
@ -11,6 +11,7 @@ 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) {
@ -35,22 +36,26 @@ 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 errWithPath *typst.ErrorWithPath var errTypst *typst.Error
if errors.As(err, &errWithPath) { if errors.As(err, &errTypst) {
if errWithPath.Message != "assertion failed: Test" { if len(errTypst.Details) != 1 {
t.Errorf("Expected error with error message %q, got %q", "assertion failed: Test", errWithPath.Message) t.Fatalf("Expected error doesn't contain the expected number of detail entries. Got %v, want %v", len(errTypst.Details), 1)
} }
/*if errWithPath.Path != "" { details := errTypst.Details[0]
t.Errorf("Expected error to point to path %q, got path %q", "", errWithPath.Path) if details.Message != "error: assertion failed: Test" {
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 errWithPath.Line != 3 { if details.Line != 3 {
t.Errorf("Expected error to point at line %d, got line %d", 3, errWithPath.Line) t.Errorf("Expected error to point at line %d, got line %d", 3, details.Line)
} }
if errWithPath.Column != 1 { if details.Column != 1 {
t.Errorf("Expected error to point at column %d, got column %d", 1, errWithPath.Column) t.Errorf("Expected error to point at column %d, got column %d", 1, details.Column)
} }
} else { } else {
t.Errorf("Expected error type %T, got %T: %v", errWithPath, err, err) t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
} }
} }
} }
@ -70,13 +75,108 @@ 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: invalid value 'a' for '--pages <PAGES>': not a valid page number. // The expected message should be similar to: error: invalid value 'a' for '--pages <PAGES>': not a valid page number.
if errTypst.Message == "" { if details.Message == "" {
t.Errorf("Expected error message, got %q", errTypst.Message) t.Errorf("Expected error message, got %q", details.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)
}
})
}
}

Binary file not shown.

View File

@ -1,33 +0,0 @@
#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

@ -0,0 +1,13 @@
# 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,6 +1,7 @@
package main package main
import ( import (
"bytes"
"log" "log"
"os" "os"
"time" "time"
@ -8,7 +9,7 @@ import (
"github.com/Dadido3/go-typst" "github.com/Dadido3/go-typst"
) )
// DataEntry contains fake data to be passed to typst. // DataEntry contains 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 }
@ -25,21 +26,28 @@ var TestData = []DataEntry{
} }
func main() { func main() {
typstCLI := typst.CLI{} var markup bytes.Buffer
r, err := os.Open("template.typ") // Inject Go values as Typst markup.
if err != nil { 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 open template file for reading: %v.", err) log.Panicf("Failed to inject values into Typst markup: %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()
if err := typstCLI.CompileWithVariables(r, f, nil, map[string]any{"Data": TestData}); err != nil { typstCLI := typst.CLI{}
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)
} }
} }

View File

@ -0,0 +1,16 @@
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,5 +1,12 @@
#let Data = ( // Prepare data that will be used as preview.
#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,36 @@
#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],)})],
)
}
)
}

12
examples/simple/README.md Normal file
View File

@ -0,0 +1,12 @@
# 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.

37
examples/simple/main.go Normal file
View File

@ -0,0 +1,37 @@
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

@ -0,0 +1,16 @@
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()
}

BIN
examples/simple/output.pdf Normal file

Binary file not shown.

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
@ -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,3 +1,8 @@
// 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 (
@ -8,12 +13,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 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 {
@ -22,8 +27,10 @@ func (i Image) MarshalTypstVariable() ([]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((") 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() { for _, b := range buffer.Bytes() {
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",") buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
} }

View File

@ -33,18 +33,26 @@ func (p *testImage) Opaque() bool {
} }
func TestImage(t *testing.T) { func TestImage(t *testing.T) {
img := &testImage{image.Rect(0, 0, 255, 255)} img := &testImage{image.Rect(0, 0, 256, 256)}
// Wrap image. // Wrap image.
typstImage := typst.Image{img} typstImage := typst.Image{img}
cli := typst.CLI{} 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
#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 Normal file
View File

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

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 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,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)
@ -113,8 +113,7 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
return nil return nil
case *time.Time: case *time.Time:
if i == nil { if i == nil {
e.writeString("none") return e.writeString("none")
return nil
} }
if err := e.encodeTime(*i); err != nil { if err := e.encodeTime(*i); err != nil {
return err return err
@ -127,8 +126,7 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
return nil return nil
case *time.Duration: case *time.Duration:
if i == nil { if i == nil {
e.writeString("none") return e.writeString("none")
return nil
} }
if err := e.encodeDuration(*i); err != nil { if err := e.encodeDuration(*i); err != nil {
return err return err
@ -136,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()
@ -224,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("()")
} }
@ -278,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
@ -299,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("()")
} }
@ -359,12 +367,12 @@ 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
} }
// 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 {
@ -387,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 {
@ -419,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
} }
@ -445,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(),
@ -456,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")
} }
@ -199,7 +199,7 @@ func TestVariableEncoder(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)
} }
} }
}) })

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)
}