diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7d7e87e..05a9928 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,31 @@ jobs: uses: actions/checkout@v4 - name: Test package - run: go test -v ./... + run: go test -skip "^TestDocker.*$" -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' + - '' # Also include the default image, just in case. + + 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 "^TestDocker.*$" -v ./... + env: + TYPST_DOCKER_IMAGE: ${{ matrix.typst-docker-image }} diff --git a/README.md b/README.md index 57762a6..3c44e43 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. -- All Typst parameters are discoverable and documented in [cli-options.go](cli-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. +- PDF, SVG, PNG and HTML generation. +- All Typst parameters are discoverable and documented in [options.go](options.go). +- 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. - 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,13 +64,79 @@ 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. + +## 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() { - 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. @@ -70,23 +149,78 @@ 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(markup, 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() { + 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) + } + + // Add some Typst markup using the previously injected values. + markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) +#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 { + 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) + +### More examples + +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). [the instructions in the Typst repository]: https://github.com/typst/typst?tab=readme-ov-file#installation [Typst]: https://typst.app/ diff --git a/caller.go b/caller.go new file mode 100644 index 0000000..190be86 --- /dev/null +++ b/caller.go @@ -0,0 +1,26 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst + +import "io" + +// TODO: Add WASM caller + +// 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 + +// Caller contains all Typst commands that are supported by this library. +type Caller interface { + // VersionString returns the Typst version as a string. + VersionString() (string, error) + + // Fonts returns all fonts that are available to Typst. + // 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. + Compile(input io.Reader, output io.Writer, options *OptionsCompile) error +} diff --git a/cli-options.go b/cli-options.go index 3b4910c..72045af 100644 --- a/cli-options.go +++ b/cli-options.go @@ -1,159 +1,9 @@ -// Copyright (c) 2024-2025 David Vogel +// Copyright (c) 2025 David Vogel // // This software is released under the MIT License. // https://opensource.org/licenses/MIT package typst -import ( - "os" - "strconv" - "time" -) - -type OutputFormat string - -const ( - OutputFormatAuto OutputFormat = "" - - OutputFormatPDF OutputFormat = "pdf" - OutputFormatPNG OutputFormat = "png" - OutputFormatSVG OutputFormat = "svg" - OutputFormatHTML OutputFormat = "html" // this format is only available since 0.13.0 -) - -type PDFStandard string - -const ( - PDFStandard1_4 PDFStandard = "1.4" // PDF 1.4 (Available since Typst 0.14.0) - PDFStandard1_5 PDFStandard = "1.5" // PDF 1.5 (Available since Typst 0.14.0) - PDFStandard1_6 PDFStandard = "1.6" // PDF 1.6 (Available since Typst 0.14.0) - PDFStandard1_7 PDFStandard = "1.7" // PDF 1.7 - PDFStandard2_0 PDFStandard = "2.0" // PDF 2.0 (Available since Typst 0.14.0) - - PDFStandardA_1B PDFStandard = "a-1b" // PDF/A-1b (Available since Typst 0.14.0) - PDFStandardA_1A PDFStandard = "a-1a" // PDF/A-1a (Available since Typst 0.14.0) - PDFStandardA_2B PDFStandard = "a-2b" // PDF/A-2b - PDFStandardA_2U PDFStandard = "a-2u" // PDF/A-2u (Available since Typst 0.14.0) - PDFStandardA_2A PDFStandard = "a-2a" // PDF/A-2a (Available since Typst 0.14.0) - PDFStandardA_3B PDFStandard = "a-3b" // PDF/A-3b (Available since Typst 0.13.0) - PDFStandardA_3U PDFStandard = "a-3u" // PDF/A-3u (Available since Typst 0.14.0) - PDFStandardA_3A PDFStandard = "a-3a" // PDF/A-3a (Available since Typst 0.14.0) - PDFStandardA_4 PDFStandard = "a-4" // PDF/A-4 (Available since Typst 0.14.0) - PDFStandardA_4F PDFStandard = "a-4f" // PDF/A-4f (Available since Typst 0.14.0) - PDFStandardA_4E PDFStandard = "a-4e" // PDF/A-4e (Available since Typst 0.14.0) - PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) -) - -type CLIOptions 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. - 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) - NoPDFTags bool // Disables the automatic generation of accessibility tags. These are emitted when no particular standard like PDF/UA-1 is selected to provide a baseline of accessibility. (Available since Typst 0.14.0) - CreationTime time.Time // The document's creation date. For more information, see https://reproducible-builds.org/specs/source-date-epoch/. - PackagePath string // Custom path to local packages, defaults to system-dependent location. - PackageCachePath string // Custom path to package cache, defaults to system-dependent location. - Jobs int // Number of parallel jobs spawned during compilation, defaults to number of CPUs. Setting it to 1 disables parallelism. - - // Which pages to export. When unspecified, all document pages are exported. - // - // Pages to export are separated by commas, and can be either simple page numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any pages after it). - // - // Page numbers are one-indexed and correspond to real page numbers in the document (therefore not being affected by the document's page counter). - Pages string - - Format OutputFormat // The format of the output file, inferred from the extension by default. - PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144. - - // One (or multiple) PDF standards that Typst will enforce conformance with. - // - // See typst.PDFStandard for possible values. - PDFStandards []PDFStandard - - Custom []string // Custom command line options go here. -} - -// Args returns a list of CLI arguments that should be passed to the executable. -func (c *CLIOptions) Args() (result []string) { - if c.Root != "" { - result = append(result, "--root", c.Root) - } - - for key, value := range c.Input { - result = append(result, "--input", key+"="+value) - } - - if len(c.FontPaths) > 0 { - var paths string - for i, path := range c.FontPaths { - if i > 0 { - paths += string(os.PathListSeparator) - } - paths += path - } - result = append(result, "--font-path", paths) - } - - if c.IgnoreSystemFonts { - result = append(result, "--ignore-system-fonts") - } - - if c.IgnoreEmbeddedFonts { - result = append(result, "--ignore-embedded-fonts") - } - - if c.NoPDFTags { - result = append(result, "--no-pdf-tags") - } - - if !c.CreationTime.IsZero() { - result = append(result, "--creation-timestamp", strconv.FormatInt(c.CreationTime.Unix(), 10)) - } - - if c.PackagePath != "" { - result = append(result, "--package-path", c.PackagePath) - } - - if c.PackageCachePath != "" { - result = append(result, "--package-cache-path", c.PackageCachePath) - } - - if c.Jobs > 0 { - result = append(result, "-j", strconv.FormatInt(int64(c.Jobs), 10)) - } - - if c.Pages != "" { - result = append(result, "--pages", c.Pages) - } - - if c.Format != OutputFormatAuto { - 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 { - result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10)) - } - - if len(c.PDFStandards) > 0 { - var standards string - for i, standard := range c.PDFStandards { - if i > 0 { - standards += "," - } - standards += string(standard) - } - result = append(result, "--pdf-standard", standards) - } - - result = append(result, c.Custom...) - - return -} +// Deprecated: Use typst.OptionsCompile instead. +type CLIOptions = OptionsCompile diff --git a/cli-options_test.go b/cli-options_test.go deleted file mode 100644 index 0e0ad36..0000000 --- a/cli-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 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 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]) - } -} diff --git a/cli.go b/cli.go index 36aefa9..2801b98 100644 --- a/cli.go +++ b/cli.go @@ -6,24 +6,23 @@ package typst import ( + "bufio" "bytes" "fmt" "io" "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 - +// 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. } -// 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. +// VersionString returns the Typst version as a string. func (c CLI) VersionString() (string, error) { // Get path of executable. execPath := ExecutablePath @@ -31,7 +30,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") @@ -53,25 +52,64 @@ func (c CLI) VersionString() (string, error) { return output.String(), 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 *CLIOptions) error { - args := []string{"c"} - if options != nil { - args = append(args, options.Args()...) - } - args = append(args, "--diagnostic-format", "human", "-", "-") - +// Fonts returns all fonts that are available to Typst. +// 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 != "" { execPath = c.ExecutablePath } if execPath == "" { - return fmt.Errorf("go-typst doesn't support this platform") + return nil, fmt.Errorf("not supported on this platform") } - cmd := exec.Command(execPath, args...) + if options == nil { + options = new(OptionsFonts) + } + + cmd := exec.Command(execPath, options.Args()...) + 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, and can be nil. +func (c CLI) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { + // Get path of executable. + execPath := ExecutablePath + if c.ExecutablePath != "" { + execPath = c.ExecutablePath + } + if execPath == "" { + return fmt.Errorf("not supported on this platform") + } + + if options == nil { + options = new(OptionsCompile) + } + + cmd := exec.Command(execPath, options.Args()...) cmd.Dir = c.WorkingDirectory cmd.Stdin = input cmd.Stdout = output @@ -91,13 +129,8 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err return nil } -// CompileWithVariables takes a Typst document from input, and renders it into the output writer. -// The options parameter is optional. -// -// Additionally this will inject the given map of variables into the global scope of the Typst document. -// -// Deprecated: You should use InjectValues in combination with the normal Compile method instead. -func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error { +// 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{} if err := InjectValues(&varBuffer, variables); err != nil { diff --git a/cli_test.go b/cli_test.go index c7158c7..b314c40 100644 --- a/cli_test.go +++ b/cli_test.go @@ -19,10 +19,48 @@ 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) { + typstCaller := typst.CLI{} + + result, err := typstCaller.Fonts(nil) + 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) + } +} + +func TestCLI_FontsWithOptions(t *testing.T) { + typstCaller := typst.CLI{} + + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) + 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) + } +} + +func TestCLI_FontsWithFontPaths(t *testing.T) { + typstCaller := typst.CLI{} + + 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) + } + if len(result) != 5 { + t.Errorf("Unexpected number of detected fonts. Got %d, want %d.", len(result), 5) + } } // Test basic compile functionality. @@ -37,7 +75,7 @@ func TestCLI_Compile(t *testing.T) { #lorem(5)`) - opts := typst.CLIOptions{ + opts := typst.OptionsCompile{ Format: typst.OutputFormatPNG, PPI: ppi, } diff --git a/docker.go b/docker.go new file mode 100644 index 0000000..2c398c5 --- /dev/null +++ b/docker.go @@ -0,0 +1,155 @@ +// 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{} + +// args returns docker related arguments. +func (d Docker) args() []string { + 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) + } + + // 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 + 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. +// The options parameter is optional, and can be nil. +func (d Docker) Fonts(options *OptionsFonts) ([]string, error) { + args := d.args() + + if options == nil { + options = new(OptionsFonts) + } + args = append(args, options.Args()...) + + cmd := exec.Command("docker", args...) + + 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, and can be nil. +func (d Docker) Compile(input io.Reader, output io.Writer, options *OptionsCompile) error { + args := d.args() + + // From here on come Typst arguments. + + if options == nil { + options = new(OptionsCompile) + } + args = append(args, options.Args()...) + + 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..74cbdbe --- /dev/null +++ b/docker_test.go @@ -0,0 +1,139 @@ +// Copyright (c) 2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst_test + +import ( + "bytes" + "image" + "os" + "path/filepath" + "strconv" + "testing" + + "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) { + typstCaller := typst.Docker{ + Image: typstDockerImage(), + } + + v, err := typstCaller.VersionString() + if err != nil { + t.Fatalf("Failed to get typst version: %v.", err) + } + + t.Logf("VersionString: %s", v) +} + +func TestDocker_Fonts(t *testing.T) { + typstCaller := typst.Docker{ + Image: typstDockerImage(), + } + + result, err := typstCaller.Fonts(nil) + 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) + } +} + +func TestDocker_FontsWithOptions(t *testing.T) { + typstCaller := typst.Docker{ + Image: typstDockerImage(), + } + + result, err := typstCaller.Fonts(&typst.OptionsFonts{IgnoreSystemFonts: true}) + 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) + } +} + +func TestDocker_FontsWithFontPaths(t *testing.T) { + typstCaller := typst.Docker{ + Image: typstDockerImage(), + Volumes: []string{"./test-files:/fonts"}, + } + + result, err := typstCaller.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 + const ppi = 144 + + 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 + +#lorem(5)`) + + opts := typst.OptionsCompile{ + 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{ + Image: typstDockerImage(), + 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.OptionsCompile{Root: "/markup"}) + if err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } + if w.Available() == 0 { + t.Errorf("No output was written.") + } +} 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..c96bd54 --- /dev/null +++ b/documentation/images/readme-example-injection.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/errors_test.go b/errors_test.go index 0486ae2..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.CLIOptions{ + opts := typst.OptionsCompile{ Pages: "a", } 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/options.go b/options.go new file mode 100644 index 0000000..f6b87d3 --- /dev/null +++ b/options.go @@ -0,0 +1,214 @@ +// Copyright (c) 2024-2025 David Vogel +// +// This software is released under the MIT License. +// https://opensource.org/licenses/MIT + +package typst + +import ( + "os" + "strconv" + "time" +) + +type OutputFormat string + +const ( + OutputFormatAuto OutputFormat = "" + + OutputFormatPDF OutputFormat = "pdf" + OutputFormatPNG OutputFormat = "png" + OutputFormatSVG OutputFormat = "svg" + OutputFormatHTML OutputFormat = "html" // this format is only available since 0.13.0 +) + +type PDFStandard string + +const ( + PDFStandard1_4 PDFStandard = "1.4" // PDF 1.4 (Available since Typst 0.14.0) + PDFStandard1_5 PDFStandard = "1.5" // PDF 1.5 (Available since Typst 0.14.0) + PDFStandard1_6 PDFStandard = "1.6" // PDF 1.6 (Available since Typst 0.14.0) + PDFStandard1_7 PDFStandard = "1.7" // PDF 1.7 + PDFStandard2_0 PDFStandard = "2.0" // PDF 2.0 (Available since Typst 0.14.0) + + PDFStandardA_1B PDFStandard = "a-1b" // PDF/A-1b (Available since Typst 0.14.0) + PDFStandardA_1A PDFStandard = "a-1a" // PDF/A-1a (Available since Typst 0.14.0) + PDFStandardA_2B PDFStandard = "a-2b" // PDF/A-2b + PDFStandardA_2U PDFStandard = "a-2u" // PDF/A-2u (Available since Typst 0.14.0) + PDFStandardA_2A PDFStandard = "a-2a" // PDF/A-2a (Available since Typst 0.14.0) + PDFStandardA_3B PDFStandard = "a-3b" // PDF/A-3b (Available since Typst 0.13.0) + PDFStandardA_3U PDFStandard = "a-3u" // PDF/A-3u (Available since Typst 0.14.0) + PDFStandardA_3A PDFStandard = "a-3a" // PDF/A-3a (Available since Typst 0.14.0) + PDFStandardA_4 PDFStandard = "a-4" // PDF/A-4 (Available since Typst 0.14.0) + PDFStandardA_4F PDFStandard = "a-4f" // PDF/A-4f (Available since Typst 0.14.0) + PDFStandardA_4E PDFStandard = "a-4e" // PDF/A-4e (Available since Typst 0.14.0) + PDFStandardUA_1 PDFStandard = "ua-1" // PDF/UA-1 (Available since Typst 0.14.0) +) + +// 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. + 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) { + // 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 { + 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 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`. + 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) + NoPDFTags bool // Disables the automatic generation of accessibility tags. These are emitted when no particular standard like PDF/UA-1 is selected to provide a baseline of accessibility. (Available since Typst 0.14.0) + CreationTime time.Time // The document's creation date. For more information, see https://reproducible-builds.org/specs/source-date-epoch/. + PackagePath string // Custom path to local packages, defaults to system-dependent location. + PackageCachePath string // Custom path to package cache, defaults to system-dependent location. + Jobs int // Number of parallel jobs spawned during compilation, defaults to number of CPUs. Setting it to 1 disables parallelism. + + // Which pages to export. When unspecified, all document pages are exported. + // + // Pages to export are separated by commas, and can be either simple page numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any pages after it). + // + // Page numbers are one-indexed and correspond to real page numbers in the document (therefore not being affected by the document's page counter). + Pages string + + Format OutputFormat // The format of the output file, inferred from the extension by default. + PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144. + + // One (or multiple) PDF standards that Typst will enforce conformance with. + // + // See typst.PDFStandard for possible values. + PDFStandards []PDFStandard + + Custom []string // Custom command line options go here. +} + +// 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) + } + + for key, value := range o.Input { + result = append(result, "--input", key+"="+value) + } + + 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.NoPDFTags { + result = append(result, "--no-pdf-tags") + } + + if !o.CreationTime.IsZero() { + result = append(result, "--creation-timestamp", strconv.FormatInt(o.CreationTime.Unix(), 10)) + } + + if o.PackagePath != "" { + result = append(result, "--package-path", o.PackagePath) + } + + if o.PackageCachePath != "" { + result = append(result, "--package-cache-path", o.PackageCachePath) + } + + if o.Jobs > 0 { + result = append(result, "-j", strconv.FormatInt(int64(o.Jobs), 10)) + } + + if o.Pages != "" { + result = append(result, "--pages", o.Pages) + } + + 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 + result = append(result, "--features", "html") + } + } + + if o.PPI > 0 { + result = append(result, "--ppi", strconv.FormatInt(int64(o.PPI), 10)) + } + + if len(o.PDFStandards) > 0 { + var standards string + for i, standard := range o.PDFStandards { + if i > 0 { + standards += "," + } + standards += string(standard) + } + 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/readme_test.go b/readme_test.go index 0ae4480..bcf23d9 100644 --- a/readme_test.go +++ b/readme_test.go @@ -8,13 +8,83 @@ package typst_test import ( "bytes" "os" + "path/filepath" "testing" + "time" "github.com/Dadido3/go-typst" ) func TestREADME1(t *testing.T) { - r := bytes.NewBufferString(`#set page(width: 100mm, height: auto, margin: 5mm) + 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) { + 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. @@ -27,15 +97,52 @@ 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(markup, f, &typst.OptionsCompile{Format: typst.OutputFormatSVG}); err != nil { + t.Fatalf("Failed to compile document: %v.", err) + } +} + +func TestREADME8(t *testing.T) { + 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) + } + + // Add some Typst markup using the previously injected values. + markup.WriteString(`#set page(width: 100mm, height: auto, margin: 5mm) +#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 { + 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/test-files/Delius-Regular.ttf b/test-files/Delius-Regular.ttf new file mode 100644 index 0000000..2cd4c9a Binary files /dev/null and b/test-files/Delius-Regular.ttf differ 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) } }