From 1294d5f009d9ea0665788481fe296e0dea209c21 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sat, 15 Nov 2025 20:38:04 +0100 Subject: [PATCH 01/17] Add support for multiple Typst callers - Add Caller interface type - Rename CLIOptions to Options --- README.md | 2 +- caller.go | 19 +++++++++++++++++++ cli.go | 13 +++++-------- cli_test.go | 2 +- errors_test.go | 2 +- cli-options.go => options.go | 5 +++-- cli-options_test.go => options_test.go | 4 ++-- 7 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 caller.go rename cli-options.go => options.go (97%) rename cli-options_test.go => options_test.go (91%) diff --git a/README.md b/README.md index 57762a6..3952800 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Use at your own discretion for production systems. ## Features - 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 [options.go](options.go). - Go-to-Typst Value Encoder: Seamlessly inject any Go values. - Encode and inject images as a Typst markup simply by [wrapping](image.go) `image.Image` types or byte slices with raw JPEG or PNG data. - Errors from Typst CLI are returned as structured Go error objects with detailed information, such as line numbers and file paths. diff --git a/caller.go b/caller.go new file mode 100644 index 0000000..269f5ef --- /dev/null +++ b/caller.go @@ -0,0 +1,19 @@ +package typst + +import "io" + +// TODO: Add an interface for the Typst caller and let CLI (and later docker and WASM) be implementations of that + +// TODO: Add docker support to CLI, by calling docker run instead + +// TODO: Add special type "Filename" (or similar) that implements a io.Reader/io.Writer that can be plugged into the input and output parameters of the Compile method + +// Caller contains all functions that can be +type Caller interface { + // VersionString returns the version string as returned by Typst. + VersionString() (string, error) + + // Compile takes a Typst document from the supplied input reader, and renders it into the output writer. + // The options parameter is optional, and can be nil. + Compile(input io.Reader, output io.Writer, options *Options) error +} diff --git a/cli.go b/cli.go index 36aefa9..533a992 100644 --- a/cli.go +++ b/cli.go @@ -12,16 +12,13 @@ import ( "os/exec" ) -// TODO: Add docker support to CLI, by calling docker run instead - -// TODO: Add an interface for the Typst caller and let CLI (and later docker and WASM) be implementations of that - type CLI struct { ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used. WorkingDirectory string // The path where the Typst executable is run in. When left empty, the Typst executable will be run in the process's current directory. } -// TODO: Add method for querying the Typst version resulting in a semver object +// Ensure that CLI implements the Caller interface. +var _ Caller = CLI{} // VersionString returns the version string as returned by Typst. func (c CLI) VersionString() (string, error) { @@ -55,7 +52,7 @@ func (c CLI) VersionString() (string, error) { // 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 { +func (c CLI) Compile(input io.Reader, output io.Writer, options *Options) error { args := []string{"c"} if options != nil { args = append(args, options.Args()...) @@ -96,8 +93,8 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err // // 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 { +// Deprecated: You should use typst.InjectValues in combination with the normal Compile method instead. +func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *Options, variables map[string]any) error { varBuffer := bytes.Buffer{} if err := InjectValues(&varBuffer, variables); err != nil { diff --git a/cli_test.go b/cli_test.go index c7158c7..088cbea 100644 --- a/cli_test.go +++ b/cli_test.go @@ -37,7 +37,7 @@ func TestCLI_Compile(t *testing.T) { #lorem(5)`) - opts := typst.CLIOptions{ + opts := typst.Options{ Format: typst.OutputFormatPNG, PPI: ppi, } diff --git a/errors_test.go b/errors_test.go index 0486ae2..b7c2855 100644 --- a/errors_test.go +++ b/errors_test.go @@ -63,7 +63,7 @@ func TestErrors1(t *testing.T) { func TestErrors2(t *testing.T) { cli := typst.CLI{} - opts := typst.CLIOptions{ + opts := typst.Options{ Pages: "a", } diff --git a/cli-options.go b/options.go similarity index 97% rename from cli-options.go rename to options.go index 3b4910c..1c61930 100644 --- a/cli-options.go +++ b/options.go @@ -45,7 +45,8 @@ const ( PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) ) -type CLIOptions struct { +// Options contains all parameters that can be passed to a Typst CLI. +type Options struct { Root string // Configures the project root (for absolute paths). Input map[string]string // String key-value pairs visible through `sys.inputs`. FontPaths []string // Adds additional directories that are recursively searched for fonts. @@ -76,7 +77,7 @@ type CLIOptions struct { } // Args returns a list of CLI arguments that should be passed to the executable. -func (c *CLIOptions) Args() (result []string) { +func (c *Options) Args() (result []string) { if c.Root != "" { result = append(result, "--root", c.Root) } diff --git a/cli-options_test.go b/options_test.go similarity index 91% rename from cli-options_test.go rename to options_test.go index 0e0ad36..4309833 100644 --- a/cli-options_test.go +++ b/options_test.go @@ -12,8 +12,8 @@ import ( "github.com/Dadido3/go-typst" ) -func TestCliOptions(t *testing.T) { - o := typst.CLIOptions{ +func TestOptions(t *testing.T) { + o := typst.Options{ FontPaths: []string{"somepath/to/somewhere", "another/to/somewhere"}, } args := o.Args() From e9bd6a09281fcd7ba92771613afee01112e8b854 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sat, 15 Nov 2025 20:40:21 +0100 Subject: [PATCH 02/17] Add CLIOptions as an alias type of Options --- cli-options.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 cli-options.go diff --git a/cli-options.go b/cli-options.go new file mode 100644 index 0000000..d45f3fd --- /dev/null +++ b/cli-options.go @@ -0,0 +1,11 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst + +// CLIOptions contains all parameters that can be passed to a Typst CLI. +// +// Deprecated: Use typst.Options instead. +type CLIOptions = Options From a8a24661722a70e27b7d64f60a512a735ace8015 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sat, 15 Nov 2025 20:19:26 +0000 Subject: [PATCH 03/17] Change error message --- cli.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli.go b/cli.go index 533a992..84d3334 100644 --- a/cli.go +++ b/cli.go @@ -28,7 +28,7 @@ func (c CLI) VersionString() (string, error) { execPath = c.ExecutablePath } if execPath == "" { - return "", fmt.Errorf("go-typst doesn't support this platform") + return "", fmt.Errorf("not supported on this platform") } cmd := exec.Command(execPath, "--version") @@ -65,7 +65,7 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *Options) error execPath = c.ExecutablePath } if execPath == "" { - return fmt.Errorf("go-typst doesn't support this platform") + return fmt.Errorf("not supported on this platform") } cmd := exec.Command(execPath, args...) From 37d4b485dd4f7a84feed48e537b0fe2ff4cb8a6e Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 12:03:36 +0000 Subject: [PATCH 04/17] Add Docker caller & Add support for the fonts command --- caller.go | 16 ++++-- cli.go | 40 ++++++++++++- cli_test.go | 12 ++++ docker.go | 148 +++++++++++++++++++++++++++++++++++++++++++++++++ docker_test.go | 94 +++++++++++++++++++++++++++++++ 5 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 docker.go create mode 100644 docker_test.go diff --git a/caller.go b/caller.go index 269f5ef..c48a2d2 100644 --- a/caller.go +++ b/caller.go @@ -1,18 +1,24 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + package typst import "io" -// TODO: Add an interface for the Typst caller and let CLI (and later docker and WASM) be implementations of that +// TODO: Add WASM caller -// TODO: Add docker support to CLI, by calling docker run instead +// TODO: Add special type "Filename" (or similar) that implements a io.Reader/io.Writer that can be plugged into the input and output parameters of the Compile method to signal the use of input/output files instead of readers/writers -// TODO: Add special type "Filename" (or similar) that implements a io.Reader/io.Writer that can be plugged into the input and output parameters of the Compile method - -// Caller contains all functions that can be +// Caller contains all Typst commands that are supported by this library. type Caller interface { // VersionString returns the version string as returned by Typst. VersionString() (string, error) + // Fonts returns all fonts that are available to Typst. + Fonts() ([]string, error) + // Compile takes a Typst document from the supplied input reader, and renders it into the output writer. // The options parameter is optional, and can be nil. Compile(input io.Reader, output io.Writer, options *Options) error diff --git a/cli.go b/cli.go index 84d3334..0466ee1 100644 --- a/cli.go +++ b/cli.go @@ -6,12 +6,14 @@ package typst import ( + "bufio" "bytes" "fmt" "io" "os/exec" ) +// CLI allows you to invoke commands on a native Typst executable. type CLI struct { ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used. WorkingDirectory string // The path where the Typst executable is run in. When left empty, the Typst executable will be run in the process's current directory. @@ -50,6 +52,42 @@ func (c CLI) VersionString() (string, error) { return output.String(), nil } +// Fonts returns all fonts that are available to Typst. +func (c CLI) Fonts() ([]string, error) { + // Get path of executable. + execPath := ExecutablePath + if c.ExecutablePath != "" { + execPath = c.ExecutablePath + } + if execPath == "" { + return nil, fmt.Errorf("not supported on this platform") + } + + cmd := exec.Command(execPath, "fonts") + cmd.Dir = c.WorkingDirectory + + var output, errBuffer bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &errBuffer + + if err := cmd.Run(); err != nil { + switch err := err.(type) { + case *exec.ExitError: + return nil, ParseStderr(errBuffer.String(), err) + default: + return nil, err + } + } + + var result []string + scanner := bufio.NewScanner(&output) + for scanner.Scan() { + result = append(result, scanner.Text()) + } + + return result, nil +} + // 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 *Options) error { @@ -57,7 +95,7 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *Options) error if options != nil { args = append(args, options.Args()...) } - args = append(args, "--diagnostic-format", "human", "-", "-") + args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into Options // Get path of executable. execPath := ExecutablePath diff --git a/cli_test.go b/cli_test.go index 088cbea..76e23b0 100644 --- a/cli_test.go +++ b/cli_test.go @@ -25,6 +25,18 @@ func TestCLI_VersionString(t *testing.T) { } } +func TestCLI_Fonts(t *testing.T) { + caller := typst.CLI{} + + result, err := caller.Fonts() + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) < 4 { + t.Errorf("Unexpected number of detected fonts. Got %d, want >= %d.", len(result), 4) + } +} + // Test basic compile functionality. func TestCLI_Compile(t *testing.T) { const inches = 1 diff --git a/docker.go b/docker.go new file mode 100644 index 0000000..e941f6f --- /dev/null +++ b/docker.go @@ -0,0 +1,148 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os/exec" +) + +// Theoretically it's possible to use the Docker SDK directly: +// https://docs.docker.com/reference/api/engine/sdk/examples/ +// But that dependency is unnecessarily huge, therefore we will just call the Docker executable. + +// The default Docker image to use. +// This is the latest supported version of Typst. +const DockerDefaultImage = "ghcr.io/typst/typst:0.14.0" + +// Docker allows you to invoke commands on a Typst Docker image. +type Docker struct { + Image string // The image to use, defaults to the latest supported offical Typst Docker image if left empty. See: typst.DockerDefaultImage. + WorkingDirectory string // The working directory of Docker. When left empty, Docker will be run with the process's current working directory. + + // Additional bind-mounts or volumes that are passed via "--volume" flag to Docker. + // For details, see: https://docs.docker.com/engine/storage/volumes/#syntax + // + // Example: + // typst.Docker{Volumes: []string{".:/markup"}} // This bind mounts the current working directory to "/markup" inside the container. + // typst.Docker{Volumes: []string{"/usr/share/fonts:/usr/share/fonts"}} // This makes all system fonts available to Typst running inside the container. + Volumes []string +} + +// Ensure that Docker implements the Caller interface. +var _ Caller = Docker{} + +// VersionString returns the version string as returned by Typst. +func (d Docker) VersionString() (string, error) { + image := DockerDefaultImage + if d.Image != "" { + image = d.Image + } + + cmd := exec.Command("docker", "run", "-i", image, "--version") + + var output, errBuffer bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &errBuffer + + if err := cmd.Run(); err != nil { + switch err := err.(type) { + case *exec.ExitError: + return "", ParseStderr(errBuffer.String(), err) + default: + return "", err + } + } + + return output.String(), nil +} + +// Fonts returns all fonts that are available to Typst. +func (d Docker) Fonts() ([]string, error) { + image := DockerDefaultImage + if d.Image != "" { + image = d.Image + } + + cmd := exec.Command("docker", "run", "-i", image, "fonts") + + var output, errBuffer bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = &errBuffer + + if err := cmd.Run(); err != nil { + switch err := err.(type) { + case *exec.ExitError: + return nil, ParseStderr(errBuffer.String(), err) + default: + return nil, err + } + } + + var result []string + scanner := bufio.NewScanner(&output) + for scanner.Scan() { + result = append(result, scanner.Text()) + } + + return result, nil +} + +// Compile takes a Typst document from input, and renders it into the output writer. +// The options parameter is optional. +func (d Docker) Compile(input io.Reader, output io.Writer, options *Options) error { + image := DockerDefaultImage + if d.Image != "" { + image = d.Image + } + + // Argument -i is needed for stdio to work. + args := []string{"run", "-i"} + + // Add mounts. + for _, volume := range d.Volumes { + args = append(args, "-v", volume) + } + + args = append(args, image) + + // From here on come Typst arguments. + + args = append(args, "c") + if options != nil { + args = append(args, options.Args()...) + } + args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into Options + + cmd := exec.Command("docker", args...) + cmd.Dir = d.WorkingDirectory + cmd.Stdin = input + cmd.Stdout = output + + errBuffer := bytes.Buffer{} + cmd.Stderr = &errBuffer + + if err := cmd.Run(); err != nil { + switch err := err.(type) { + case *exec.ExitError: + if err.ExitCode() >= 125 { + // Most likely docker related error. + // TODO: Find a better way to distinguish between Typst or Docker errors. + return fmt.Errorf("exit code %d: %s", err.ExitCode(), errBuffer.String()) + } else { + // Typst related error. + return ParseStderr(errBuffer.String(), err) + } + default: + return err + } + } + + return nil +} diff --git a/docker_test.go b/docker_test.go new file mode 100644 index 0000000..10936ef --- /dev/null +++ b/docker_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst_test + +import ( + "bytes" + "image" + "path/filepath" + "strconv" + "testing" + + "github.com/Dadido3/go-typst" +) + +func TestDocker_VersionString(t *testing.T) { + caller := typst.Docker{} + + _, err := caller.VersionString() + if err != nil { + t.Fatalf("Failed to get typst version: %v.", err) + } +} + +func TestDocker_Fonts(t *testing.T) { + caller := typst.Docker{} + + result, err := caller.Fonts() + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) < 4 { + t.Errorf("Unexpected number of detected fonts. Got %d, want >= %d.", len(result), 4) + } +} + +// Test basic compile functionality. +func TestDocker_Compile(t *testing.T) { + const inches = 1 + const ppi = 144 + + typstCaller := typst.Docker{} + + r := bytes.NewBufferString(`#set page(width: ` + strconv.FormatInt(inches, 10) + `in, height: ` + strconv.FormatInt(inches, 10) + `in, margin: (x: 1mm, y: 1mm)) += Test + +#lorem(5)`) + + opts := typst.Options{ + Format: typst.OutputFormatPNG, + PPI: ppi, + } + + var w bytes.Buffer + if err := typstCaller.Compile(r, &w, &opts); err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } + + imgConf, imgType, err := image.DecodeConfig(&w) + if err != nil { + t.Fatalf("Failed to decode image: %v.", err) + } + if imgType != "png" { + t.Fatalf("Resulting image is of type %q, expected %q.", imgType, "png") + } + if imgConf.Width != inches*ppi { + t.Fatalf("Resulting image width is %d, expected %d.", imgConf.Width, inches*ppi) + } + if imgConf.Height != inches*ppi { + t.Fatalf("Resulting image height is %d, expected %d.", imgConf.Height, inches*ppi) + } +} + +// Test basic compile functionality with a given working directory. +func TestDocker_CompileWithWorkingDir(t *testing.T) { + typstCaller := typst.Docker{ + WorkingDirectory: filepath.Join(".", "test-files"), + Volumes: []string{".:/markup"}, + } + + r := bytes.NewBufferString(`#import "hello-world-template.typ": template +#show: doc => template()`) + + var w bytes.Buffer + err := typstCaller.Compile(r, &w, &typst.Options{Root: "/markup"}) + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } + if w.Available() == 0 { + t.Errorf("No output was written.") + } +} From 24c09dfcbea47b8835e5fdb0339b1130de3f161e Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 12:05:09 +0000 Subject: [PATCH 05/17] Update VersionString comment --- caller.go | 2 +- cli.go | 2 +- docker.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/caller.go b/caller.go index c48a2d2..e23ea5b 100644 --- a/caller.go +++ b/caller.go @@ -13,7 +13,7 @@ import "io" // Caller contains all Typst commands that are supported by this library. type Caller interface { - // VersionString returns the version string as returned by Typst. + // VersionString returns the Typst version as a string. VersionString() (string, error) // Fonts returns all fonts that are available to Typst. diff --git a/cli.go b/cli.go index 0466ee1..6afbee2 100644 --- a/cli.go +++ b/cli.go @@ -22,7 +22,7 @@ type CLI struct { // Ensure that CLI implements the Caller interface. var _ Caller = CLI{} -// VersionString returns the version string as returned by Typst. +// VersionString returns the Typst version as a string. func (c CLI) VersionString() (string, error) { // Get path of executable. execPath := ExecutablePath diff --git a/docker.go b/docker.go index e941f6f..cc571eb 100644 --- a/docker.go +++ b/docker.go @@ -38,7 +38,7 @@ type Docker struct { // Ensure that Docker implements the Caller interface. var _ Caller = Docker{} -// VersionString returns the version string as returned by Typst. +// VersionString returns the Typst version as a string. func (d Docker) VersionString() (string, error) { image := DockerDefaultImage if d.Image != "" { From a075fb41bfc581964ae11656197da92ec47cbfa4 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 12:18:50 +0000 Subject: [PATCH 06/17] Rename Options to OptionsCompile as there will be different options for different Typst commands in the future --- caller.go | 2 +- cli-options.go | 4 ++-- cli.go | 10 +++++----- cli_test.go | 2 +- docker.go | 4 ++-- docker_test.go | 4 ++-- errors_test.go | 2 +- options.go | 6 +++--- options_test.go | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/caller.go b/caller.go index e23ea5b..b70955b 100644 --- a/caller.go +++ b/caller.go @@ -21,5 +21,5 @@ type Caller interface { // Compile takes a Typst document from the supplied input reader, and renders it into the output writer. // The options parameter is optional, and can be nil. - Compile(input io.Reader, output io.Writer, options *Options) error + Compile(input io.Reader, output io.Writer, options *OptionsCompile) error } diff --git a/cli-options.go b/cli-options.go index d45f3fd..6d05ccc 100644 --- a/cli-options.go +++ b/cli-options.go @@ -7,5 +7,5 @@ package typst // CLIOptions contains all parameters that can be passed to a Typst CLI. // -// Deprecated: Use typst.Options instead. -type CLIOptions = Options +// Deprecated: Use typst.OptionsCompile instead. +type CLIOptions = OptionsCompile diff --git a/cli.go b/cli.go index 6afbee2..0c7d629 100644 --- a/cli.go +++ b/cli.go @@ -89,13 +89,13 @@ func (c CLI) Fonts() ([]string, error) { } // 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 *Options) error { +// The options parameter is optional, and can be nil. +func (c CLI) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { args := []string{"c"} if options != nil { args = append(args, options.Args()...) } - args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into Options + args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into OptionsCompile // Get path of executable. execPath := ExecutablePath @@ -127,12 +127,12 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *Options) error } // CompileWithVariables takes a Typst document from input, and renders it into the output writer. -// The options parameter is optional. +// The options parameter is optional, and can be nil. // // Additionally this will inject the given map of variables into the global scope of the Typst document. // // Deprecated: You should use typst.InjectValues in combination with the normal Compile method instead. -func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *Options, variables map[string]any) error { +func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *OptionsCompile, variables map[string]any) error { varBuffer := bytes.Buffer{} if err := InjectValues(&varBuffer, variables); err != nil { diff --git a/cli_test.go b/cli_test.go index 76e23b0..4f51566 100644 --- a/cli_test.go +++ b/cli_test.go @@ -49,7 +49,7 @@ func TestCLI_Compile(t *testing.T) { #lorem(5)`) - opts := typst.Options{ + opts := typst.OptionsCompile{ Format: typst.OutputFormatPNG, PPI: ppi, } diff --git a/docker.go b/docker.go index cc571eb..cb00930 100644 --- a/docker.go +++ b/docker.go @@ -95,8 +95,8 @@ func (d Docker) Fonts() ([]string, error) { } // Compile takes a Typst document from input, and renders it into the output writer. -// The options parameter is optional. -func (d Docker) Compile(input io.Reader, output io.Writer, options *Options) error { +// The options parameter is optional, and can be nil. +func (d Docker) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { image := DockerDefaultImage if d.Image != "" { image = d.Image diff --git a/docker_test.go b/docker_test.go index 10936ef..d3e9b58 100644 --- a/docker_test.go +++ b/docker_test.go @@ -48,7 +48,7 @@ func TestDocker_Compile(t *testing.T) { #lorem(5)`) - opts := typst.Options{ + opts := typst.OptionsCompile{ Format: typst.OutputFormatPNG, PPI: ppi, } @@ -84,7 +84,7 @@ func TestDocker_CompileWithWorkingDir(t *testing.T) { #show: doc => template()`) var w bytes.Buffer - err := typstCaller.Compile(r, &w, &typst.Options{Root: "/markup"}) + err := typstCaller.Compile(r, &w, &typst.OptionsCompile{Root: "/markup"}) if err != nil { t.Fatalf("Failed to compile document: %v.", err) } diff --git a/errors_test.go b/errors_test.go index b7c2855..35a8ce8 100644 --- a/errors_test.go +++ b/errors_test.go @@ -63,7 +63,7 @@ func TestErrors1(t *testing.T) { func TestErrors2(t *testing.T) { cli := typst.CLI{} - opts := typst.Options{ + opts := typst.OptionsCompile{ Pages: "a", } diff --git a/options.go b/options.go index 1c61930..e40fb05 100644 --- a/options.go +++ b/options.go @@ -45,8 +45,8 @@ const ( PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) ) -// Options contains all parameters that can be passed to a Typst CLI. -type Options struct { +// OptionsCompile contains all parameters for the compile command. +type OptionsCompile struct { Root string // Configures the project root (for absolute paths). Input map[string]string // String key-value pairs visible through `sys.inputs`. FontPaths []string // Adds additional directories that are recursively searched for fonts. @@ -77,7 +77,7 @@ type Options struct { } // Args returns a list of CLI arguments that should be passed to the executable. -func (c *Options) Args() (result []string) { +func (c *OptionsCompile) Args() (result []string) { if c.Root != "" { result = append(result, "--root", c.Root) } diff --git a/options_test.go b/options_test.go index 4309833..9a93bd2 100644 --- a/options_test.go +++ b/options_test.go @@ -13,7 +13,7 @@ import ( ) func TestOptions(t *testing.T) { - o := typst.Options{ + o := typst.OptionsCompile{ FontPaths: []string{"somepath/to/somewhere", "another/to/somewhere"}, } args := o.Args() From 83d5fc35ea6e5d3f429711f53d73cb2ba8ab58f0 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 12:35:13 +0000 Subject: [PATCH 07/17] Add options to Fonts command --- caller.go | 3 ++- cli.go | 10 ++++++++-- cli_test.go | 14 +++++++++++++- docker.go | 10 ++++++++-- docker_test.go | 14 +++++++++++++- options.go | 40 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 7 deletions(-) diff --git a/caller.go b/caller.go index b70955b..190be86 100644 --- a/caller.go +++ b/caller.go @@ -17,7 +17,8 @@ type Caller interface { VersionString() (string, error) // Fonts returns all fonts that are available to Typst. - Fonts() ([]string, error) + // The options parameter is optional, and can be nil. + Fonts(options *OptionsFonts) ([]string, error) // Compile takes a Typst document from the supplied input reader, and renders it into the output writer. // The options parameter is optional, and can be nil. diff --git a/cli.go b/cli.go index 0c7d629..51f2ac6 100644 --- a/cli.go +++ b/cli.go @@ -53,7 +53,8 @@ func (c CLI) VersionString() (string, error) { } // Fonts returns all fonts that are available to Typst. -func (c CLI) Fonts() ([]string, error) { +// The options parameter is optional, and can be nil. +func (c CLI) Fonts(options *OptionsFonts) ([]string, error) { // Get path of executable. execPath := ExecutablePath if c.ExecutablePath != "" { @@ -63,7 +64,12 @@ func (c CLI) Fonts() ([]string, error) { return nil, fmt.Errorf("not supported on this platform") } - cmd := exec.Command(execPath, "fonts") + args := []string{"fonts"} + if options != nil { + args = append(args, options.Args()...) + } + + cmd := exec.Command(execPath, args...) cmd.Dir = c.WorkingDirectory var output, errBuffer bytes.Buffer diff --git a/cli_test.go b/cli_test.go index 4f51566..5e02ec2 100644 --- a/cli_test.go +++ b/cli_test.go @@ -28,7 +28,7 @@ func TestCLI_VersionString(t *testing.T) { func TestCLI_Fonts(t *testing.T) { caller := typst.CLI{} - result, err := caller.Fonts() + result, err := caller.Fonts(nil) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -37,6 +37,18 @@ func TestCLI_Fonts(t *testing.T) { } } +func TestCLI_FontsWithOptions(t *testing.T) { + caller := typst.CLI{} + + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, IgnoreEmbeddedFonts: true}) + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) != 0 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 0) + } +} + // Test basic compile functionality. func TestCLI_Compile(t *testing.T) { const inches = 1 diff --git a/docker.go b/docker.go index cb00930..5c297fe 100644 --- a/docker.go +++ b/docker.go @@ -64,13 +64,19 @@ func (d Docker) VersionString() (string, error) { } // Fonts returns all fonts that are available to Typst. -func (d Docker) Fonts() ([]string, error) { +// The options parameter is optional, and can be nil. +func (d Docker) Fonts(options *OptionsFonts) ([]string, error) { image := DockerDefaultImage if d.Image != "" { image = d.Image } - cmd := exec.Command("docker", "run", "-i", image, "fonts") + args := []string{"run", "-i", image, "fonts"} + if options != nil { + args = append(args, options.Args()...) + } + + cmd := exec.Command("docker", args...) var output, errBuffer bytes.Buffer cmd.Stdout = &output diff --git a/docker_test.go b/docker_test.go index d3e9b58..93c15d7 100644 --- a/docker_test.go +++ b/docker_test.go @@ -27,7 +27,7 @@ func TestDocker_VersionString(t *testing.T) { func TestDocker_Fonts(t *testing.T) { caller := typst.Docker{} - result, err := caller.Fonts() + result, err := caller.Fonts(nil) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -36,6 +36,18 @@ func TestDocker_Fonts(t *testing.T) { } } +func TestDocker_FontsWithOptions(t *testing.T) { + caller := typst.Docker{} + + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, IgnoreEmbeddedFonts: true}) + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) != 0 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 0) + } +} + // Test basic compile functionality. func TestDocker_Compile(t *testing.T) { const inches = 1 diff --git a/options.go b/options.go index e40fb05..42279f6 100644 --- a/options.go +++ b/options.go @@ -45,6 +45,46 @@ const ( PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) ) +// OptionsFonts contains all parameters for the fonts command. +type OptionsFonts struct { + FontPaths []string // Adds additional directories that are recursively searched for fonts. + IgnoreSystemFonts bool // Ensures system fonts won't be searched, unless explicitly included via FontPaths. + IgnoreEmbeddedFonts bool // Disables the use of fonts embedded into the Typst binary. (Available since Typst 0.14.0) + Variants bool // Also lists style variants of each font family. + + Custom []string // Custom command line options go here. +} + +// Args returns a list of CLI arguments that should be passed to the executable. +func (o *OptionsFonts) Args() (result []string) { + if len(o.FontPaths) > 0 { + var paths string + for i, path := range o.FontPaths { + if i > 0 { + paths += string(os.PathListSeparator) + } + paths += path + } + result = append(result, "--font-path", paths) + } + + if o.IgnoreSystemFonts { + result = append(result, "--ignore-system-fonts") + } + + if o.IgnoreEmbeddedFonts { + result = append(result, "--ignore-embedded-fonts") + } + + if o.Variants { + result = append(result, "--variants") + } + + result = append(result, o.Custom...) + + return +} + // OptionsCompile contains all parameters for the compile command. type OptionsCompile struct { Root string // Configures the project root (for absolute paths). From 9536d6510ed9f83eed0dfc94273adf6aef077c65 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 13:44:26 +0000 Subject: [PATCH 08/17] Circumvent test failures with older versions of Typst --- cli_test.go | 6 +++--- docker_test.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cli_test.go b/cli_test.go index 5e02ec2..2a7c648 100644 --- a/cli_test.go +++ b/cli_test.go @@ -40,12 +40,12 @@ func TestCLI_Fonts(t *testing.T) { func TestCLI_FontsWithOptions(t *testing.T) { caller := typst.CLI{} - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, IgnoreEmbeddedFonts: true}) + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } - if len(result) != 0 { - t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 0) + if len(result) != 4 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 4) } } diff --git a/docker_test.go b/docker_test.go index 93c15d7..b7b77b4 100644 --- a/docker_test.go +++ b/docker_test.go @@ -39,12 +39,12 @@ func TestDocker_Fonts(t *testing.T) { func TestDocker_FontsWithOptions(t *testing.T) { caller := typst.Docker{} - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, IgnoreEmbeddedFonts: true}) + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } - if len(result) != 0 { - t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 0) + if len(result) != 4 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 4) } } From f6b2cda0d514b8b93846ff48d8f5b04520c137d1 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 14:19:06 +0000 Subject: [PATCH 09/17] Clean up options.go --- options.go | 58 +++++++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/options.go b/options.go index 42279f6..e0bf25e 100644 --- a/options.go +++ b/options.go @@ -45,7 +45,7 @@ const ( PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) ) -// OptionsFonts contains all parameters for the fonts command. +// OptionsFonts contains all supported parameters for the fonts command. type OptionsFonts struct { FontPaths []string // Adds additional directories that are recursively searched for fonts. IgnoreSystemFonts bool // Ensures system fonts won't be searched, unless explicitly included via FontPaths. @@ -85,7 +85,7 @@ func (o *OptionsFonts) Args() (result []string) { return } -// OptionsCompile contains all parameters for the compile command. +// OptionsCompile contains all supported parameters for the compile command. type OptionsCompile struct { Root string // Configures the project root (for absolute paths). Input map[string]string // String key-value pairs visible through `sys.inputs`. @@ -117,18 +117,18 @@ type OptionsCompile struct { } // Args returns a list of CLI arguments that should be passed to the executable. -func (c *OptionsCompile) Args() (result []string) { - if c.Root != "" { - result = append(result, "--root", c.Root) +func (o *OptionsCompile) Args() (result []string) { + if o.Root != "" { + result = append(result, "--root", o.Root) } - for key, value := range c.Input { + for key, value := range o.Input { result = append(result, "--input", key+"="+value) } - if len(c.FontPaths) > 0 { + if len(o.FontPaths) > 0 { var paths string - for i, path := range c.FontPaths { + for i, path := range o.FontPaths { if i > 0 { paths += string(os.PathListSeparator) } @@ -137,41 +137,41 @@ func (c *OptionsCompile) Args() (result []string) { result = append(result, "--font-path", paths) } - if c.IgnoreSystemFonts { + if o.IgnoreSystemFonts { result = append(result, "--ignore-system-fonts") } - if c.IgnoreEmbeddedFonts { + if o.IgnoreEmbeddedFonts { result = append(result, "--ignore-embedded-fonts") } - if c.NoPDFTags { + if o.NoPDFTags { result = append(result, "--no-pdf-tags") } - if !c.CreationTime.IsZero() { - result = append(result, "--creation-timestamp", strconv.FormatInt(c.CreationTime.Unix(), 10)) + if !o.CreationTime.IsZero() { + result = append(result, "--creation-timestamp", strconv.FormatInt(o.CreationTime.Unix(), 10)) } - if c.PackagePath != "" { - result = append(result, "--package-path", c.PackagePath) + if o.PackagePath != "" { + result = append(result, "--package-path", o.PackagePath) } - if c.PackageCachePath != "" { - result = append(result, "--package-cache-path", c.PackageCachePath) + if o.PackageCachePath != "" { + result = append(result, "--package-cache-path", o.PackageCachePath) } - if c.Jobs > 0 { - result = append(result, "-j", strconv.FormatInt(int64(c.Jobs), 10)) + if o.Jobs > 0 { + result = append(result, "-j", strconv.FormatInt(int64(o.Jobs), 10)) } - if c.Pages != "" { - result = append(result, "--pages", c.Pages) + if o.Pages != "" { + result = append(result, "--pages", o.Pages) } - if c.Format != OutputFormatAuto { - result = append(result, "-f", string(c.Format)) - if c.Format == OutputFormatHTML { + if o.Format != OutputFormatAuto { + result = append(result, "-f", string(o.Format)) + if o.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 @@ -179,13 +179,13 @@ func (c *OptionsCompile) Args() (result []string) { } } - if c.PPI > 0 { - result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10)) + if o.PPI > 0 { + result = append(result, "--ppi", strconv.FormatInt(int64(o.PPI), 10)) } - if len(c.PDFStandards) > 0 { + if len(o.PDFStandards) > 0 { var standards string - for i, standard := range c.PDFStandards { + for i, standard := range o.PDFStandards { if i > 0 { standards += "," } @@ -194,7 +194,7 @@ func (c *OptionsCompile) Args() (result []string) { result = append(result, "--pdf-standard", standards) } - result = append(result, c.Custom...) + result = append(result, o.Custom...) return } From eb8890b7db2d2e542e6ce4f6ff43f1c1e51fdb12 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 14:21:33 +0000 Subject: [PATCH 10/17] Make github action test different Docker image versions --- .github/workflows/test.yml | 32 +++++++++++++++++++++++++++++--- docker_test.go | 24 ++++++++++++++++++++---- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d7e87e..4dbe4ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,8 @@ on: [push, pull_request] jobs: - build: - name: test + test-non-docker: + name: test non-docker runs-on: ubuntu-latest strategy: matrix: @@ -28,4 +28,30 @@ jobs: uses: actions/checkout@v4 - name: Test package - run: go test -v ./... + run: go test -run ^(?!Docker).*$ -v ./... + + test-docker: + name: test docker + runs-on: ubuntu-latest + strategy: + matrix: + go-version: ['1.23.x'] + typst-docker-image: + - 'ghcr.io/typst/typst:v0.12.0' + - 'ghcr.io/typst/typst:v0.13.0' + - 'ghcr.io/typst/typst:v0.13.1' + - 'ghcr.io/typst/typst:0.14.0' + + steps: + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + + - name: Check out code + uses: actions/checkout@v4 + + - name: Test package + run: go test -run ^Docker.*$ -v ./... + env: + TYPST_DOCKER_IMAGE: ${{ matrix.typst-docker-image }} diff --git a/docker_test.go b/docker_test.go index b7b77b4..46a6b49 100644 --- a/docker_test.go +++ b/docker_test.go @@ -8,6 +8,7 @@ package typst_test import ( "bytes" "image" + "os" "path/filepath" "strconv" "testing" @@ -15,8 +16,16 @@ import ( "github.com/Dadido3/go-typst" ) +// Returns the TYPST_DOCKER_IMAGE environment variable. +// If that's not set, it will return an empty string, which makes the tests default to typst.DockerDefaultImage. +func typstDockerImage() string { + return os.Getenv("TYPST_DOCKER_IMAGE") +} + func TestDocker_VersionString(t *testing.T) { - caller := typst.Docker{} + caller := typst.Docker{ + Image: typstDockerImage(), + } _, err := caller.VersionString() if err != nil { @@ -25,7 +34,9 @@ func TestDocker_VersionString(t *testing.T) { } func TestDocker_Fonts(t *testing.T) { - caller := typst.Docker{} + caller := typst.Docker{ + Image: typstDockerImage(), + } result, err := caller.Fonts(nil) if err != nil { @@ -37,7 +48,9 @@ func TestDocker_Fonts(t *testing.T) { } func TestDocker_FontsWithOptions(t *testing.T) { - caller := typst.Docker{} + caller := typst.Docker{ + Image: typstDockerImage(), + } result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) if err != nil { @@ -53,7 +66,9 @@ func TestDocker_Compile(t *testing.T) { const inches = 1 const ppi = 144 - typstCaller := typst.Docker{} + typstCaller := typst.Docker{ + Image: typstDockerImage(), + } r := bytes.NewBufferString(`#set page(width: ` + strconv.FormatInt(inches, 10) + `in, height: ` + strconv.FormatInt(inches, 10) + `in, margin: (x: 1mm, y: 1mm)) = Test @@ -88,6 +103,7 @@ func TestDocker_Compile(t *testing.T) { // Test basic compile functionality with a given working directory. func TestDocker_CompileWithWorkingDir(t *testing.T) { typstCaller := typst.Docker{ + Image: typstDockerImage(), WorkingDirectory: filepath.Join(".", "test-files"), Volumes: []string{".:/markup"}, } From 7561a99a5b36d66033a556275c71ebf0695cd13c Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 14:54:21 +0000 Subject: [PATCH 11/17] Fix GitHub test action --- .github/workflows/test.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4dbe4ed..05a9928 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v4 - name: Test package - run: go test -run ^(?!Docker).*$ -v ./... + run: go test -skip "^TestDocker.*$" -v ./... test-docker: name: test docker @@ -41,6 +41,7 @@ jobs: - 'ghcr.io/typst/typst:v0.13.0' - 'ghcr.io/typst/typst:v0.13.1' - 'ghcr.io/typst/typst:0.14.0' + - '' # Also include the default image, just in case. steps: - name: Set up Go ${{ matrix.go-version }} @@ -52,6 +53,6 @@ jobs: uses: actions/checkout@v4 - name: Test package - run: go test -run ^Docker.*$ -v ./... + run: go test -run "^TestDocker.*$" -v ./... env: TYPST_DOCKER_IMAGE: ${{ matrix.typst-docker-image }} From f4c71399024afbac2c3387d426690e48e3a2b3fd Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 14:59:41 +0000 Subject: [PATCH 12/17] Log Typst version string when testing --- cli_test.go | 4 +++- docker_test.go | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cli_test.go b/cli_test.go index 2a7c648..8d3f8cf 100644 --- a/cli_test.go +++ b/cli_test.go @@ -19,10 +19,12 @@ import ( func TestCLI_VersionString(t *testing.T) { cli := typst.CLI{} - _, err := cli.VersionString() + v, err := cli.VersionString() if err != nil { t.Fatalf("Failed to get typst version: %v.", err) } + + t.Logf("VersionString: %s", v) } func TestCLI_Fonts(t *testing.T) { diff --git a/docker_test.go b/docker_test.go index 46a6b49..8c7da04 100644 --- a/docker_test.go +++ b/docker_test.go @@ -27,10 +27,12 @@ func TestDocker_VersionString(t *testing.T) { Image: typstDockerImage(), } - _, err := caller.VersionString() + v, err := caller.VersionString() if err != nil { t.Fatalf("Failed to get typst version: %v.", err) } + + t.Logf("VersionString: %s", v) } func TestDocker_Fonts(t *testing.T) { From cb8075d88f1fdfdf4821c14d2764eafc78dc9fe8 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 15:56:44 +0000 Subject: [PATCH 13/17] Get rid of descriptions on deprecated types --- cli-options.go | 2 -- cli.go | 5 ----- 2 files changed, 7 deletions(-) diff --git a/cli-options.go b/cli-options.go index 6d05ccc..72045af 100644 --- a/cli-options.go +++ b/cli-options.go @@ -5,7 +5,5 @@ package typst -// CLIOptions contains all parameters that can be passed to a Typst CLI. -// // Deprecated: Use typst.OptionsCompile instead. type CLIOptions = OptionsCompile diff --git a/cli.go b/cli.go index 51f2ac6..1040b94 100644 --- a/cli.go +++ b/cli.go @@ -132,11 +132,6 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *OptionsCompile) return nil } -// CompileWithVariables takes a Typst document from input, and renders it into the output writer. -// The options parameter is optional, and can be nil. -// -// Additionally this will inject the given map of variables into the global scope of the Typst document. -// // Deprecated: You should use typst.InjectValues in combination with the normal Compile method instead. func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *OptionsCompile, variables map[string]any) error { varBuffer := bytes.Buffer{} From e8620ad02bce43d254152dbf4b30225187f8ebee Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 15:58:07 +0000 Subject: [PATCH 14/17] Add test for the FontPaths option --- cli_test.go | 12 ++++++++++++ docker_test.go | 15 +++++++++++++++ test-files/Delius-Regular.ttf | Bin 0 -> 58848 bytes 3 files changed, 27 insertions(+) create mode 100644 test-files/Delius-Regular.ttf diff --git a/cli_test.go b/cli_test.go index 8d3f8cf..c30047c 100644 --- a/cli_test.go +++ b/cli_test.go @@ -51,6 +51,18 @@ func TestCLI_FontsWithOptions(t *testing.T) { } } +func TestCLI_FontsWithFontPaths(t *testing.T) { + caller := typst.CLI{} + + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{filepath.Join(".", "test-files")}}) + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) != 5 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 5) + } +} + // Test basic compile functionality. func TestCLI_Compile(t *testing.T) { const inches = 1 diff --git a/docker_test.go b/docker_test.go index 8c7da04..62060f4 100644 --- a/docker_test.go +++ b/docker_test.go @@ -63,6 +63,21 @@ func TestDocker_FontsWithOptions(t *testing.T) { } } +func TestDocker_FontsWithFontPaths(t *testing.T) { + caller := typst.Docker{ + Image: typstDockerImage(), + Volumes: []string{"./test-files:/fonts"}, + } + + result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{"/fonts"}}) + if err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } + if len(result) != 5 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 5) + } +} + // Test basic compile functionality. func TestDocker_Compile(t *testing.T) { const inches = 1 diff --git a/test-files/Delius-Regular.ttf b/test-files/Delius-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2cd4c9ae92a73ba9e2e7d16eff2b25a3afe779c7 GIT binary patch literal 58848 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 Date: Sun, 16 Nov 2025 16:07:00 +0000 Subject: [PATCH 15/17] Refactor and fix argument handling - Move all Typst arguments into the respective Args methods the Options* types - Simplify the logic in the Caller implementations - Add docker volume handling for other commands than compile - Get rid of options_test.go as its test got replaced with the TestCLI_FontsWithFontPaths test --- cli.go | 19 +++++++---------- docker.go | 57 +++++++++++++++++++++++++------------------------ options.go | 14 ++++++++++++ options_test.go | 29 ------------------------- 4 files changed, 51 insertions(+), 68 deletions(-) delete mode 100644 options_test.go diff --git a/cli.go b/cli.go index 1040b94..2801b98 100644 --- a/cli.go +++ b/cli.go @@ -64,12 +64,11 @@ func (c CLI) Fonts(options *OptionsFonts) ([]string, error) { return nil, fmt.Errorf("not supported on this platform") } - args := []string{"fonts"} - if options != nil { - args = append(args, options.Args()...) + if options == nil { + options = new(OptionsFonts) } - cmd := exec.Command(execPath, args...) + cmd := exec.Command(execPath, options.Args()...) cmd.Dir = c.WorkingDirectory var output, errBuffer bytes.Buffer @@ -97,12 +96,6 @@ func (c CLI) Fonts(options *OptionsFonts) ([]string, error) { // Compile takes a Typst document from input, and renders it into the output writer. // The options parameter is optional, and can be nil. func (c CLI) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { - args := []string{"c"} - if options != nil { - args = append(args, options.Args()...) - } - args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into OptionsCompile - // Get path of executable. execPath := ExecutablePath if c.ExecutablePath != "" { @@ -112,7 +105,11 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *OptionsCompile) return fmt.Errorf("not supported on this platform") } - cmd := exec.Command(execPath, args...) + if options == nil { + options = new(OptionsCompile) + } + + cmd := exec.Command(execPath, options.Args()...) cmd.Dir = c.WorkingDirectory cmd.Stdin = input cmd.Stdout = output diff --git a/docker.go b/docker.go index 5c297fe..2c398c5 100644 --- a/docker.go +++ b/docker.go @@ -38,14 +38,32 @@ type Docker struct { // Ensure that Docker implements the Caller interface. var _ Caller = Docker{} -// VersionString returns the Typst version as a string. -func (d Docker) VersionString() (string, error) { +// args returns docker related arguments. +func (d Docker) args() []string { image := DockerDefaultImage if d.Image != "" { image = d.Image } - cmd := exec.Command("docker", "run", "-i", image, "--version") + // Argument -i is needed for stdio to work. + args := []string{"run", "-i"} + + // Add mounts. + for _, volume := range d.Volumes { + args = append(args, "-v", volume) + } + + // Which docker image to use. + args = append(args, image) + + return args +} + +// VersionString returns the Typst version as a string. +func (d Docker) VersionString() (string, error) { + args := append(d.args(), "--version") + + cmd := exec.Command("docker", args...) var output, errBuffer bytes.Buffer cmd.Stdout = &output @@ -66,15 +84,12 @@ func (d Docker) VersionString() (string, error) { // Fonts returns all fonts that are available to Typst. // The options parameter is optional, and can be nil. func (d Docker) Fonts(options *OptionsFonts) ([]string, error) { - image := DockerDefaultImage - if d.Image != "" { - image = d.Image - } + args := d.args() - args := []string{"run", "-i", image, "fonts"} - if options != nil { - args = append(args, options.Args()...) + if options == nil { + options = new(OptionsFonts) } + args = append(args, options.Args()...) cmd := exec.Command("docker", args...) @@ -103,28 +118,14 @@ func (d Docker) Fonts(options *OptionsFonts) ([]string, error) { // Compile takes a Typst document from input, and renders it into the output writer. // The options parameter is optional, and can be nil. func (d Docker) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { - image := DockerDefaultImage - if d.Image != "" { - image = d.Image - } - - // Argument -i is needed for stdio to work. - args := []string{"run", "-i"} - - // Add mounts. - for _, volume := range d.Volumes { - args = append(args, "-v", volume) - } - - args = append(args, image) + args := d.args() // From here on come Typst arguments. - args = append(args, "c") - if options != nil { - args = append(args, options.Args()...) + if options == nil { + options = new(OptionsCompile) } - args = append(args, "--diagnostic-format", "human", "-", "-") // TODO: Move these default arguments into Options + args = append(args, options.Args()...) cmd := exec.Command("docker", args...) cmd.Dir = d.WorkingDirectory diff --git a/options.go b/options.go index e0bf25e..f6b87d3 100644 --- a/options.go +++ b/options.go @@ -57,6 +57,9 @@ type OptionsFonts struct { // Args returns a list of CLI arguments that should be passed to the executable. func (o *OptionsFonts) Args() (result []string) { + // The first argument is the command we want to run. + result = []string{"fonts"} + if len(o.FontPaths) > 0 { var paths string for i, path := range o.FontPaths { @@ -118,6 +121,9 @@ type OptionsCompile struct { // Args returns a list of CLI arguments that should be passed to the executable. func (o *OptionsCompile) Args() (result []string) { + // The first argument is the command we want to run. + result = []string{"c"} + if o.Root != "" { result = append(result, "--root", o.Root) } @@ -194,7 +200,15 @@ func (o *OptionsCompile) Args() (result []string) { result = append(result, "--pdf-standard", standards) } + // Use human diagnostic format, as that's the format that we support right now. + // TODO: Switch to a different diagnostic format in the future + result = append(result, "--diagnostic-format", "human") + result = append(result, o.Custom...) + // Use stdio for input and output. + // TODO: Add Args parameters for when we want to use files instead + result = append(result, "-", "-") + return } diff --git a/options_test.go b/options_test.go deleted file mode 100644 index 9a93bd2..0000000 --- a/options_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2025 David Vogel -// -// This software is released under the MIT License. -// https://opensource.org/licenses/MIT - -package typst_test - -import ( - "os" - "testing" - - "github.com/Dadido3/go-typst" -) - -func TestOptions(t *testing.T) { - o := typst.OptionsCompile{ - 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 args[0] != "--font-path" { - t.Error("wrong font path option, expected --font-path, got", args[0]) - } - if args[1] != "somepath/to/somewhere"+string(os.PathListSeparator)+"another/to/somewhere" { - t.Error("wrong font path option, expected my two paths concatenated, got", args[1]) - } -} From 1aa13dbfb578b9fa1ade83003e27cf367e20afa9 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 18:15:25 +0000 Subject: [PATCH 16/17] Update README.md & Cleanup --- README.md | 151 ++++- cli_test.go | 12 +- docker_test.go | 16 +- documentation/images/readme-1.svg | 584 ------------------ .../images/readme-example-injection.svg | 125 ++++ .../images/readme-example-simple.svg | 571 +++++++++++++++++ examples/passing-values/main.go | 4 +- examples/simple/main.go | 4 +- readme_test.go | 105 +++- value-encoder_test.go | 4 +- 10 files changed, 956 insertions(+), 620 deletions(-) delete mode 100644 documentation/images/readme-1.svg create mode 100644 documentation/images/readme-example-injection.svg create mode 100644 documentation/images/readme-example-simple.svg diff --git a/README.md b/README.md index 3952800..857ae7f 100644 --- a/README.md +++ b/README.md @@ -20,22 +20,35 @@ Use at your own discretion for production systems. ## Features -- PDF, SVG and PNG generation. +- PDF, SVG, PNG and HTML generation. - All Typst parameters are discoverable and documented in [options.go](options.go). - Go-to-Typst Value Encoder: Seamlessly inject any Go values. - Encode and inject images as a Typst markup simply by [wrapping](image.go) `image.Image` types or byte slices with raw JPEG or PNG data. - 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. +- Supports native Typst installations and the official Docker image. - Good unit test coverage. ## Installation -1. Use `go get github.com/Dadido3/go-typst` inside of your project to add this module to your project. -2. Install Typst by following [the instructions in the Typst repository]. +Use `go get github.com/Dadido3/go-typst` inside of your project to add this module to your project. -## Runtime requirements +## Usage -This module assumes that the Typst executable is accessible from your system's PATH. +This module needs either a native installation of Typst, or a working Docker installation. +The following subsections will show how to use this library in detail. + +### Native Typst installation + +The basic usage pattern for calling a natively installed Typst executable looks like this: + +```go +typstCaller := typst.CLI{} + +err := typstCaller.Compile(input, output, options) +``` + +In this case the module assumes that the Typst executable is accessible from your system's PATH. Ensure that you have [Typst] installed on any machine that your project will be executed. You can install it by following [the instructions in the Typst repository]. @@ -43,7 +56,7 @@ Alternatively you can pack the Typst executable with your application. In this case you have to provide the path to the executable when setting up the `typst.CLI` object: ```go -typstCLI := typst.CLI{ +typstCaller := typst.CLI{ ExecutablePath: "./typst", // Relative path to executable. } ``` @@ -51,9 +64,75 @@ typstCLI := typst.CLI{ > [!NOTE] > Make sure to follow the Typst license requirements when you pack and distribute the Typst executable with your software. -## Usage +### Docker -Here we will create a simple PDF document by passing a reader with Typst markup into `typstCLI.Compile` and then let it write the resulting PDF data into a file: +To use the official Typst docker image ensure that you have a working Docker installation. + +This module will automatically pull and run a Docker container with the latest supported Typst image. +The basic usage pattern is similar to the CLI variant: + +```go +typstCaller := typst.Docker{} + +err := typstCaller.Compile(input, output, options) +``` + +#### Tips and tricks + +As the Typst instance that's running inside the container is fully encapsulated, you have pass through any resources manually. +This requires a bit more set up than using a native installation. + +Let's say you want to compile a document which imports other local Typst markup files. +In this case you have to ensure that you mount any needed folders and files to the Docker container. +You also have to set up the root of Typst to that mounted directory, or a parent of it: + +```go +typstCaller := typst.Docker{ + Volumes: []string{"./test-files:/markup"}, +} + +r := bytes.NewBufferString(`#include "hello-world.typ"`) + +var w bytes.Buffer +err := typstCaller.Compile(r, &w, &typst.OptionsCompile{Root: "/markup"}) +``` + +This will mount `./test-files` to `/markup` inside the Docker container. +When Typst compiles the input buffer `r`, it expects `./test-files/hello-world.typ` to exist outside of the container. + +Another thing is that the Dockerized version of Typst doesn't see your system fonts. +To get all your system fonts mounted into the container, you can add the volume parameter `/usr/share/fonts:/usr/share/fonts` to your `typst.Docker`. +For example: + +```go +typstCaller := typst.Docker{ + Volumes: []string{ + "./test-files:/markup", + "/usr/share/fonts:/usr/share/fonts", + }, +} +``` + +The same applies when you want to use custom fonts. +You need to mount the folder containing the fonts, and then you have to tell Typst where the fonts are mounted to inside the container: + +```go +typstCaller := typst.Docker{ + Volumes: []string{"./test-files:/fonts"}, +} + +err := typstCaller.Compile(input, output, &typst.OptionsCompile{FontPaths: []string{"/fonts"}}) +``` + +## Caller interface + +`typst.CLI` and `typst.Docker` both implement the `typst.Caller` interface. + +## More examples + +### Simple document + +Here we will create a simple PDF document by passing a reader with Typst markup into `typstCaller.Compile` and then let it write the resulting PDF data into a file: ```go func main() { @@ -70,23 +149,69 @@ A library to generate documents and reports by utilizing the command line versio - Uses stdio; No temporary files need to be created. - Test coverage of most features.`) - typstCLI := typst.CLI{} + typstCaller := typst.CLI{} - f, err := os.Create("output.pdf") + f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-simple.svg")) if err != nil { t.Fatalf("Failed to create output file: %v.", err) } defer f.Close() - if err := typstCLI.Compile(r, f, nil); err != nil { + if err := typstCaller.Compile(r, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { t.Fatalf("Failed to compile document: %v.", err) } } ``` -The resulting document will look like this: +Output: -![readme-1.svg](documentation/images/readme-1.svg) +![readme-example-simple.svg](documentation/images/readme-example-simple.svg) + +### Value injection + +If you need to create documents that rely on data coming from your Go application, you can use `typst.InjectValues` to encode any Go variables, structures, maps, arrays, slices into their respective Typst markup counterparts: + +```go +func main() { + var markup bytes.Buffer + + customValues := map[string]any{ + "time": time.Now(), + "customText": "Hey there!", + } + + // Inject Go values as Typst markup. + if err := typst.InjectValues(&markup, customValues); err != nil { + t.Fatalf("Failed to inject values into Typst markup: %v.", err) + } + + // Some Typst markup using the previously injected values. + markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) +#customText | Some date and time: #time.display()`) + + f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-injection.svg")) + if err != nil { + t.Fatalf("Failed to create output file: %v.", err) + } + defer f.Close() + + typstCaller := typst.CLI{} + if err := typstCaller.Compile(&markup, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} +``` + +Output: + +![readme-example-injection.svg](documentation/images/readme-example-injection.svg) + +### Templates + +You can also write your own templates and call them with custom data. +A tutorial for the Typst side can be found in the [Typst documentation: Making a Template](https://typst.app/docs/tutorial/making-a-template/). + +An example on how to invoke Typst templates can be found in the [passing-values example package](examples/passing-values). [the instructions in the Typst repository]: https://github.com/typst/typst?tab=readme-ov-file#installation [Typst]: https://typst.app/ diff --git a/cli_test.go b/cli_test.go index c30047c..b314c40 100644 --- a/cli_test.go +++ b/cli_test.go @@ -28,9 +28,9 @@ func TestCLI_VersionString(t *testing.T) { } func TestCLI_Fonts(t *testing.T) { - caller := typst.CLI{} + typstCaller := typst.CLI{} - result, err := caller.Fonts(nil) + result, err := typstCaller.Fonts(nil) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -40,9 +40,9 @@ func TestCLI_Fonts(t *testing.T) { } func TestCLI_FontsWithOptions(t *testing.T) { - caller := typst.CLI{} + typstCaller := typst.CLI{} - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -52,9 +52,9 @@ func TestCLI_FontsWithOptions(t *testing.T) { } func TestCLI_FontsWithFontPaths(t *testing.T) { - caller := typst.CLI{} + typstCaller := typst.CLI{} - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{filepath.Join(".", "test-files")}}) + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{filepath.Join(".", "test-files")}}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } diff --git a/docker_test.go b/docker_test.go index 62060f4..74cbdbe 100644 --- a/docker_test.go +++ b/docker_test.go @@ -23,11 +23,11 @@ func typstDockerImage() string { } func TestDocker_VersionString(t *testing.T) { - caller := typst.Docker{ + typstCaller := typst.Docker{ Image: typstDockerImage(), } - v, err := caller.VersionString() + v, err := typstCaller.VersionString() if err != nil { t.Fatalf("Failed to get typst version: %v.", err) } @@ -36,11 +36,11 @@ func TestDocker_VersionString(t *testing.T) { } func TestDocker_Fonts(t *testing.T) { - caller := typst.Docker{ + typstCaller := typst.Docker{ Image: typstDockerImage(), } - result, err := caller.Fonts(nil) + result, err := typstCaller.Fonts(nil) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -50,11 +50,11 @@ func TestDocker_Fonts(t *testing.T) { } func TestDocker_FontsWithOptions(t *testing.T) { - caller := typst.Docker{ + typstCaller := typst.Docker{ Image: typstDockerImage(), } - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } @@ -64,12 +64,12 @@ func TestDocker_FontsWithOptions(t *testing.T) { } func TestDocker_FontsWithFontPaths(t *testing.T) { - caller := typst.Docker{ + typstCaller := typst.Docker{ Image: typstDockerImage(), Volumes: []string{"./test-files:/fonts"}, } - result, err := caller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{"/fonts"}}) + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true, FontPaths: []string{"/fonts"}}) if err != nil { t.Fatalf("Failed to get available fonts: %v.", err) } diff --git a/documentation/images/readme-1.svg b/documentation/images/readme-1.svg deleted file mode 100644 index 6b2fb72..0000000 --- a/documentation/images/readme-1.svg +++ /dev/null @@ -1,584 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/documentation/images/readme-example-injection.svg b/documentation/images/readme-example-injection.svg new file mode 100644 index 0000000..2cfb81e --- /dev/null +++ b/documentation/images/readme-example-injection.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/images/readme-example-simple.svg b/documentation/images/readme-example-simple.svg new file mode 100644 index 0000000..e046513 --- /dev/null +++ b/documentation/images/readme-example-simple.svg @@ -0,0 +1,571 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/passing-values/main.go b/examples/passing-values/main.go index 6c75bd3..0cd7fe3 100644 --- a/examples/passing-values/main.go +++ b/examples/passing-values/main.go @@ -46,8 +46,8 @@ func main() { } defer f.Close() - typstCLI := typst.CLI{} - if err := typstCLI.Compile(&markup, f, nil); err != nil { + typstCaller := typst.CLI{} + if err := typstCaller.Compile(&markup, f, nil); err != nil { log.Panicf("Failed to compile document: %v.", err) } } diff --git a/examples/simple/main.go b/examples/simple/main.go index 2b2f092..964dd32 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -30,8 +30,8 @@ This document was created at #%s.display() using typst-go.`, date) } defer f.Close() - typstCLI := typst.CLI{} - if err := typstCLI.Compile(&markup, f, nil); err != nil { + typstCaller := typst.CLI{} + if err := typstCaller.Compile(&markup, f, nil); err != nil { log.Panic("failed to compile document: %w", err) } } diff --git a/readme_test.go b/readme_test.go index 0ae4480..74b257c 100644 --- a/readme_test.go +++ b/readme_test.go @@ -8,12 +8,82 @@ package typst_test import ( "bytes" "os" + "path/filepath" "testing" + "time" "github.com/Dadido3/go-typst" ) func TestREADME1(t *testing.T) { + input, output, options := new(bytes.Reader), new(bytes.Buffer), new(typst.OptionsCompile) + // ----------------------- + typstCaller := typst.CLI{} + + err := typstCaller.Compile(input, output, options) + // ----------------------- + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME3(t *testing.T) { + input, output, options := new(bytes.Reader), new(bytes.Buffer), new(typst.OptionsCompile) + // ----------------------- + typstCaller := typst.Docker{} + + err := typstCaller.Compile(input, output, options) + // ----------------------- + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME4(t *testing.T) { + // ----------------------- + typstCaller := typst.Docker{ + Volumes: []string{"./test-files:/markup"}, + } + + r := bytes.NewBufferString(`#include "hello-world.typ"`) + + var w bytes.Buffer + err := typstCaller.Compile(r, &w, &typst.OptionsCompile{Root: "/markup"}) + // ----------------------- + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME5(t *testing.T) { + // ----------------------- + typstCaller := typst.Docker{ + Volumes: []string{ + "./test-files:/markup", + "/usr/share/fonts:/usr/share/fonts", + }, + } + // ----------------------- + + if _, err := typstCaller.Fonts(nil); err != nil { + t.Fatalf("Failed to get available fonts: %v.", err) + } +} +func TestREADME6(t *testing.T) { + input, output := new(bytes.Reader), new(bytes.Buffer) + // ----------------------- + typstCaller := typst.Docker{ + Volumes: []string{"./test-files:/fonts"}, + } + + err := typstCaller.Compile(input, output, &typst.OptionsCompile{FontPaths: []string{"/fonts"}}) + // ----------------------- + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME7(t *testing.T) { r := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) = go-typst @@ -27,15 +97,44 @@ A library to generate documents and reports by utilizing the command line versio - Uses stdio; No temporary files need to be created. - Test coverage of most features.`) - typstCLI := typst.CLI{} + typstCaller := typst.CLI{} - f, err := os.Create("output.pdf") + f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-simple.svg")) if err != nil { t.Fatalf("Failed to create output file: %v.", err) } defer f.Close() - if err := typstCLI.Compile(r, f, nil); err != nil { + if err := typstCaller.Compile(r, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME8(t *testing.T) { + var markup bytes.Buffer + + customValues := map[string]any{ + "time": time.Now(), + "customText": "Hey there!", + } + + // Inject Go values as Typst markup. + if err := typst.InjectValues(&markup, customValues); err != nil { + t.Fatalf("Failed to inject values into Typst markup: %v.", err) + } + + // Some Typst markup using the previously injected values. + markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) +#customText | Some date and time: #time.display()`) + + f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-injection.svg")) + if err != nil { + t.Fatalf("Failed to create output file: %v.", err) + } + defer f.Close() + + typstCaller := typst.CLI{} + if err := typstCaller.Compile(&markup, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { t.Fatalf("Failed to compile document: %v.", err) } } diff --git a/value-encoder_test.go b/value-encoder_test.go index 657a8a8..9d088ea 100644 --- a/value-encoder_test.go +++ b/value-encoder_test.go @@ -195,10 +195,10 @@ func TestValueEncoder(t *testing.T) { // Compile to test parsing. if !tt.wantErr { - typstCLI := typst.CLI{} + typstCaller := typst.CLI{} input := strings.NewReader("#" + result.String()) var output bytes.Buffer - if err := typstCLI.Compile(input, &output, nil); err != nil { + if err := typstCaller.Compile(input, &output, nil); err != nil { t.Errorf("Failed to compile generated Typst markup: %v", err) } } From 5f513ca789aff70d0fb5a0d039143ac697cc4ce9 Mon Sep 17 00:00:00 2001 From: David Vogel Date: Sun, 16 Nov 2025 19:01:02 +0000 Subject: [PATCH 17/17] Update README.md --- README.md | 31 +- .../images/readme-example-injection.svg | 292 ++++++++++++++---- readme_test.go | 20 +- 3 files changed, 274 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 857ae7f..3c44e43 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Use at your own discretion for production systems. - PDF, SVG, PNG and HTML generation. - All Typst parameters are discoverable and documented in [options.go](options.go). -- Go-to-Typst Value Encoder: Seamlessly inject any Go values. -- Encode and inject images as a Typst markup simply by [wrapping](image.go) `image.Image` types or byte slices with raw JPEG or PNG data. +- Go-to-Typst Value Encoder: Seamlessly encode any Go values as Typst markup. +- Encode and inject images as a Typst markup simply by [wrapping](image.go) `image.Image` types or raw image data. - 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. - Supports native Typst installations and the official Docker image. @@ -128,7 +128,7 @@ err := typstCaller.Compile(input, output, &typst.OptionsCompile{FontPaths: []str `typst.CLI` and `typst.Docker` both implement the `typst.Caller` interface. -## More examples +## Examples ### Simple document @@ -136,7 +136,7 @@ Here we will create a simple PDF document by passing a reader with Typst markup ```go func main() { - r := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) + markup := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) = go-typst A library to generate documents and reports by utilizing the command line version of Typst. @@ -157,7 +157,7 @@ A library to generate documents and reports by utilizing the command line versio } defer f.Close() - if err := typstCaller.Compile(r, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { + if err := typstCaller.Compile(markup, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { t.Fatalf("Failed to compile document: %v.", err) } } @@ -173,21 +173,29 @@ If you need to create documents that rely on data coming from your Go applicatio ```go func main() { - var markup bytes.Buffer - customValues := map[string]any{ "time": time.Now(), "customText": "Hey there!", + "struct": struct { + Foo int + Bar []string + }{ + Foo: 123, + Bar: []string{"this", "is", "a", "string", "slice"}, + }, } // Inject Go values as Typst markup. + var markup bytes.Buffer if err := typst.InjectValues(&markup, customValues); err != nil { t.Fatalf("Failed to inject values into Typst markup: %v.", err) } - // Some Typst markup using the previously injected values. + // Add some Typst markup using the previously injected values. markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) -#customText | Some date and time: #time.display()`) +#customText Today's date is #time.display("[year]-[month]-[day]") and the time is #time.display("[hour]:[minute]:[second]"). + +#struct`) f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-injection.svg")) if err != nil { @@ -206,9 +214,10 @@ Output: ![readme-example-injection.svg](documentation/images/readme-example-injection.svg) -### Templates +### More examples -You can also write your own templates and call them with custom data. +It's possible to write custom Typst templates that can be called. +Th A tutorial for the Typst side can be found in the [Typst documentation: Making a Template](https://typst.app/docs/tutorial/making-a-template/). An example on how to invoke Typst templates can be found in the [passing-values example package](examples/passing-values). diff --git a/documentation/images/readme-example-injection.svg b/documentation/images/readme-example-injection.svg index 2cfb81e..c96bd54 100644 --- a/documentation/images/readme-example-injection.svg +++ b/documentation/images/readme-example-injection.svg @@ -1,5 +1,5 @@ - - + + @@ -11,41 +11,157 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -70,33 +186,27 @@ - - - - - + + - - - - - + + + + + - - - @@ -115,11 +225,89 @@ + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/readme_test.go b/readme_test.go index 74b257c..bcf23d9 100644 --- a/readme_test.go +++ b/readme_test.go @@ -84,7 +84,7 @@ func TestREADME6(t *testing.T) { } func TestREADME7(t *testing.T) { - r := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) + markup := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) = go-typst A library to generate documents and reports by utilizing the command line version of Typst. @@ -105,27 +105,35 @@ A library to generate documents and reports by utilizing the command line versio } defer f.Close() - if err := typstCaller.Compile(r, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { + if err := typstCaller.Compile(markup, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { t.Fatalf("Failed to compile document: %v.", err) } } func TestREADME8(t *testing.T) { - var markup bytes.Buffer - customValues := map[string]any{ "time": time.Now(), "customText": "Hey there!", + "struct": struct { + Foo int + Bar []string + }{ + Foo: 123, + Bar: []string{"this", "is", "a", "string", "slice"}, + }, } // Inject Go values as Typst markup. + var markup bytes.Buffer if err := typst.InjectValues(&markup, customValues); err != nil { t.Fatalf("Failed to inject values into Typst markup: %v.", err) } - // Some Typst markup using the previously injected values. + // Add some Typst markup using the previously injected values. markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) -#customText | Some date and time: #time.display()`) +#customText Today's date is #time.display("[year]-[month]-[day]") and the time is #time.display("[hour]:[minute]:[second]"). + +#struct`) f, err := os.Create(filepath.Join(".", "documentation", "images", "readme-example-injection.svg")) if err != nil {