mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-11-20 11:49:36 +00:00
Add Docker caller & Add support for the fonts command
This commit is contained in:
parent
a8a2466172
commit
37d4b485dd
16
caller.go
16
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
|
package typst
|
||||||
|
|
||||||
import "io"
|
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 Typst commands that are supported by this library.
|
||||||
|
|
||||||
// Caller contains all functions that can be
|
|
||||||
type Caller interface {
|
type Caller interface {
|
||||||
// VersionString returns the version string as returned by Typst.
|
// VersionString returns the version string as returned by Typst.
|
||||||
VersionString() (string, error)
|
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.
|
// 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.
|
// 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 *Options) error
|
||||||
|
|||||||
40
cli.go
40
cli.go
@ -6,12 +6,14 @@
|
|||||||
package typst
|
package typst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CLI allows you to invoke commands on a native Typst executable.
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used.
|
ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used.
|
||||||
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.
|
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
|
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.
|
// Compile takes a Typst document from input, and renders it into the output writer.
|
||||||
// The options parameter is optional.
|
// The options parameter is optional.
|
||||||
func (c CLI) Compile(input io.Reader, output io.Writer, options *Options) error {
|
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 {
|
if options != nil {
|
||||||
args = append(args, options.Args()...)
|
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.
|
// Get path of executable.
|
||||||
execPath := ExecutablePath
|
execPath := ExecutablePath
|
||||||
|
|||||||
12
cli_test.go
12
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.
|
// Test basic compile functionality.
|
||||||
func TestCLI_Compile(t *testing.T) {
|
func TestCLI_Compile(t *testing.T) {
|
||||||
const inches = 1
|
const inches = 1
|
||||||
|
|||||||
148
docker.go
Normal file
148
docker.go
Normal file
@ -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
|
||||||
|
}
|
||||||
94
docker_test.go
Normal file
94
docker_test.go
Normal file
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user