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