mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-04-19 15:43:15 +00:00
Compare commits
No commits in common. "main" and "v0.2.1" have entirely different histories.
29
.github/workflows/lint.yml
vendored
29
.github/workflows/lint.yml
vendored
@ -1,29 +0,0 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
# Optional: allow read access to pull request. Use with `only-new-issues` option.
|
||||
# pull-requests: read
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.64
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ['1.23.x']
|
||||
typst-version: ['0.12.0', '0.13.0', '0.13.1']
|
||||
typst-version: ['0.12.0', '0.13.0']
|
||||
|
||||
steps:
|
||||
- name: Install typst-cli ${{ matrix.typst-version }} from crates.io
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -2,7 +2,6 @@
|
||||
"cSpell.words": [
|
||||
"Dadido",
|
||||
"Foogaloo",
|
||||
"golangci",
|
||||
"typst",
|
||||
"Vogel"
|
||||
]
|
||||
|
@ -12,7 +12,6 @@ Supported Typst versions are tested by unit tests to ensure compatibility.
|
||||
|
||||
- Typst 0.12.0
|
||||
- Typst 0.13.0
|
||||
- Typst 0.13.1
|
||||
|
||||
While breaking changes may occur, i aim to minimize disruptions.
|
||||
Use at your own discretion for production systems.
|
||||
@ -21,7 +20,7 @@ Use at your own discretion for production systems.
|
||||
|
||||
- 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 (Including `image.Image` with a [wrapper](image.go)) into Typst documents via the provided encoder.
|
||||
- Go-to-Typst Object Encoder: Seamlessly inject any Go values (Including `image.Image` with a [wrapper](image.go)) into Typst documents via the provided encoder.
|
||||
- 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.
|
||||
- Good unit test coverage.
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -22,14 +22,6 @@ const (
|
||||
OutputFormatHTML OutputFormat = "html" // this format is only available since 0.13.0
|
||||
)
|
||||
|
||||
type PDFStandard string
|
||||
|
||||
const (
|
||||
PDFStandard1_7 PDFStandard = "1.7" // PDF 1.7
|
||||
PDFStandardA_2B PDFStandard = "a-2b" // PDF/A-2b
|
||||
PDFStandardA_3B PDFStandard = "a-3b" // PDF/A-3b (Available since Typst 0.13.0 and later)
|
||||
)
|
||||
|
||||
type CLIOptions struct {
|
||||
Root string // Configures the project root (for absolute paths).
|
||||
Input map[string]string // String key-value pairs visible through `sys.inputs`.
|
||||
@ -50,10 +42,13 @@ type CLIOptions struct {
|
||||
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.
|
||||
// One (or multiple comma-separated) PDF standards that Typst will enforce conformance with.
|
||||
//
|
||||
// See typst.PDFStandard for possible values.
|
||||
PDFStandards []PDFStandard
|
||||
// Possible values:
|
||||
//
|
||||
// - 1.7: PDF 1.7
|
||||
// - a-2b: PDF/A-2b
|
||||
PDFStandard string
|
||||
|
||||
Custom []string // Custom command line options go here.
|
||||
}
|
||||
@ -117,18 +112,9 @@ func (c *CLIOptions) Args() (result []string) {
|
||||
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)
|
||||
if c.PDFStandard != "" {
|
||||
result = append(result, "--pdf-standard", c.PDFStandard)
|
||||
}
|
||||
|
||||
result = append(result, c.Custom...)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package typst_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Dadido3/go-typst"
|
||||
)
|
||||
|
||||
func TestCliOptions(t *testing.T) {
|
||||
o := typst.CLIOptions{
|
||||
FontPaths: []string{"somepath/to/somewhere", "another/to/somewhere"},
|
||||
}
|
||||
args := o.Args()
|
||||
if len(args) != 2 {
|
||||
t.Errorf("wrong number of arguments, expected 2, got %d", len(args))
|
||||
}
|
||||
if "--font-path" != args[0] {
|
||||
t.Error("wrong font path option, expected --font-path, got", args[0])
|
||||
}
|
||||
if "somepath/to/somewhere"+string(os.PathListSeparator)+"another/to/somewhere" != args[1] {
|
||||
t.Error("wrong font path option, expected my two paths concatenated, got", args[1])
|
||||
}
|
||||
}
|
27
cli.go
27
cli.go
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -15,12 +15,12 @@ import (
|
||||
// TODO: Add docker support to CLI, by calling docker run instead
|
||||
|
||||
type CLI struct {
|
||||
ExecutablePath string // The Typst executable path can be overridden here. Otherwise the default path will be used.
|
||||
ExecutablePath string // The typst executable path can be overridden here. Otherwise the default path will be used.
|
||||
}
|
||||
|
||||
// TODO: Add method for querying the Typst version resulting in a semver object
|
||||
// TODO: Add method for querying the typst version resulting in a semver object
|
||||
|
||||
// VersionString returns the version string as returned by Typst.
|
||||
// VersionString returns the version string as returned by typst.
|
||||
func (c CLI) VersionString() (string, error) {
|
||||
// Get path of executable.
|
||||
execPath := ExecutablePath
|
||||
@ -46,7 +46,7 @@ func (c CLI) VersionString() (string, error) {
|
||||
return output.String(), nil
|
||||
}
|
||||
|
||||
// Compile takes a Typst document from input, and renders it into the output writer.
|
||||
// Compile takes a typst document from input, and renders it into the output writer.
|
||||
// The options parameter is optional.
|
||||
func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error {
|
||||
args := []string{"c"}
|
||||
@ -80,17 +80,22 @@ 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.
|
||||
// Compile 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.
|
||||
// Additionally this will inject the given map of variables into the global scope of the typst document.
|
||||
func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
|
||||
varBuffer := bytes.Buffer{}
|
||||
|
||||
if err := InjectValues(&varBuffer, variables); err != nil {
|
||||
return fmt.Errorf("failed to inject values into Typst markup: %w", err)
|
||||
// TODO: Use io.pipe instead of a bytes.Buffer
|
||||
|
||||
enc := NewVariableEncoder(&varBuffer)
|
||||
for k, v := range variables {
|
||||
varBuffer.WriteString("#let " + CleanIdentifier(k) + " = ")
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return fmt.Errorf("failed to encode variables with key %q: %w", k, err)
|
||||
}
|
||||
varBuffer.WriteRune('\n')
|
||||
}
|
||||
|
||||
reader := io.MultiReader(&varBuffer, input)
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -7,5 +7,5 @@
|
||||
|
||||
package typst
|
||||
|
||||
// The path to the Typst executable.
|
||||
// The path to the typst executable.
|
||||
var ExecutablePath = "typst"
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -7,9 +7,9 @@
|
||||
|
||||
package typst
|
||||
|
||||
// The path to the Typst executable.
|
||||
// The path to the typst executable.
|
||||
// We assume the executable is in the current working directory.
|
||||
//var ExecutablePath = "." + string(filepath.Separator) + filepath.Join("typst.exe")
|
||||
|
||||
// The path to the Typst executable.
|
||||
// The path to the typst executable.
|
||||
var ExecutablePath = "typst.exe"
|
||||
|
@ -14,13 +14,13 @@ import (
|
||||
// ErrorDetails contains the details of a typst.Error.
|
||||
type ErrorDetails struct {
|
||||
Message string // The parsed error message.
|
||||
Path string // Path of the Typst file where the error is located in. Zero value means that there is no further information.
|
||||
Path string // Path of the typst file where the error is located in. Zero value means that there is no further information.
|
||||
Line int // Line number of the error. Zero value means that there is no further information.
|
||||
Column int // Column of the error. Zero value means that there is no further information.
|
||||
}
|
||||
|
||||
// Error represents an error as returned by Typst.
|
||||
// This can contain multiple sub-errors or sub-warnings which are listed in the field Details.
|
||||
// Error represents a typst error.
|
||||
// This can contain multiple sub-errors or sub-warnings.
|
||||
type Error struct {
|
||||
Inner error
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
@ -9,7 +8,7 @@ import (
|
||||
"github.com/Dadido3/go-typst"
|
||||
)
|
||||
|
||||
// DataEntry contains data to be passed to Typst.
|
||||
// DataEntry contains fake data to be passed to typst.
|
||||
type DataEntry struct {
|
||||
Name string
|
||||
Size struct{ X, Y, Z float64 }
|
||||
@ -26,28 +25,21 @@ var TestData = []DataEntry{
|
||||
}
|
||||
|
||||
func main() {
|
||||
var markup bytes.Buffer
|
||||
typstCLI := typst.CLI{}
|
||||
|
||||
// Inject Go values as Typst markup.
|
||||
if err := typst.InjectValues(&markup, map[string]any{"data": TestData, "customText": "This data is coming from a Go application."}); err != nil {
|
||||
log.Panicf("Failed to inject values into Typst markup: %v.", err)
|
||||
r, err := os.Open("template.typ")
|
||||
if err != nil {
|
||||
log.Panicf("Failed to open template file for reading: %v.", err)
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Import the template and invoke the template function with the custom data.
|
||||
// Show is used to replace the current document with whatever content the template function in `template.typ` returns.
|
||||
markup.WriteString(`
|
||||
#import "template.typ": template
|
||||
#show: doc => template(data, customText)`)
|
||||
|
||||
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
|
||||
f, err := os.Create("output.pdf")
|
||||
if err != nil {
|
||||
log.Panicf("Failed to create output file: %v.", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
typstCLI := typst.CLI{}
|
||||
if err := typstCLI.Compile(&markup, f, nil); err != nil {
|
||||
if err := typstCLI.CompileWithVariables(r, f, nil, map[string]any{"Data": TestData}); err != nil {
|
||||
log.Panicf("Failed to compile document: %v.", err)
|
||||
}
|
||||
}
|
BIN
examples/passing-objects/output.pdf
Normal file
BIN
examples/passing-objects/output.pdf
Normal file
Binary file not shown.
@ -1,12 +1,5 @@
|
||||
// Prepare data that will be used as preview.
|
||||
#let data = (
|
||||
#let Data = (
|
||||
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (1, 2, 3)),
|
||||
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (10, 12, 15)),
|
||||
(Name: "Bell", Size: (X: 80, Y: 40, Z: 40), Created: datetime(year: 2010, month: 12, day: 1, hour: 12, minute: 13, second: 14), Numbers: (100, 109, 199, 200)),
|
||||
)
|
||||
|
||||
#let customText = "Hey, this is example data to test the template."
|
||||
|
||||
// Invoke the template with the preview data and replace this whole document with the result.
|
||||
#import "template.typ": template
|
||||
#show: doc => template(data, customText)
|
||||
)
|
33
examples/passing-objects/template.typ
Normal file
33
examples/passing-objects/template.typ
Normal file
@ -0,0 +1,33 @@
|
||||
#set page(paper: "a4")
|
||||
|
||||
// Uncomment to use test data.
|
||||
//#import "template-test-data.typ": Data
|
||||
|
||||
= List of items
|
||||
|
||||
#show table.cell.where(y: 0): strong
|
||||
#set table(
|
||||
stroke: (x, y) => if y == 0 {
|
||||
(bottom: 0.7pt + black)
|
||||
},
|
||||
)
|
||||
|
||||
#table(
|
||||
columns: 5,
|
||||
table.header(
|
||||
[Name],
|
||||
[Size],
|
||||
[Example box],
|
||||
[Created],
|
||||
[Numbers],
|
||||
),
|
||||
..for value in Data {
|
||||
(
|
||||
[#value.Name],
|
||||
[#value.Size],
|
||||
box(fill: black, width: 0.1mm * value.Size.X, height: 0.1mm * value.Size.Y),
|
||||
value.Created.display(),
|
||||
[#list(..for num in value.Numbers {([#num],)})],
|
||||
)
|
||||
}
|
||||
)
|
@ -1,13 +0,0 @@
|
||||
# Passing values example
|
||||
|
||||
This example demonstrates how to pass values to Typst, which can be useful in rendering custom documents such as reports, invoices, and more.
|
||||
|
||||
## How it works
|
||||
|
||||
This example follows the [template pattern](https://typst.app/docs/tutorial/making-a-template/) described in the Typst documentation.
|
||||
Here is a short overview of the files:
|
||||
|
||||
- [template.typ](template.typ) defines a Typst template function that constructs a document based on parameters.
|
||||
- [main.go](main.go) shows how to convert/encode Go values into Typst markup, and how to call/render the template with these converted values.
|
||||
- [template-preview.typ](template-preview.typ) also invokes the template while providing mock data.
|
||||
This is useful when you want to preview, update or debug the template.
|
@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Run the example as a test.
|
||||
func TestMain(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Error(r)
|
||||
}
|
||||
}()
|
||||
|
||||
main()
|
||||
}
|
Binary file not shown.
@ -1,36 +0,0 @@
|
||||
#let template(data, customText) = {
|
||||
set page(paper: "a4")
|
||||
|
||||
[= Example]
|
||||
|
||||
customText
|
||||
|
||||
[== List of items]
|
||||
|
||||
show table.cell.where(y: 0): strong
|
||||
set table(
|
||||
stroke: (x, y) => if y == 0 {
|
||||
(bottom: 0.7pt + black)
|
||||
},
|
||||
)
|
||||
|
||||
table(
|
||||
columns: 5,
|
||||
table.header(
|
||||
[Name],
|
||||
[Size],
|
||||
[Example box],
|
||||
[Created],
|
||||
[Numbers],
|
||||
),
|
||||
..for value in data {
|
||||
(
|
||||
[#value.Name],
|
||||
[#value.Size],
|
||||
box(fill: black, width: 0.1mm * value.Size.X, height: 0.1mm * value.Size.Y),
|
||||
value.Created.display(),
|
||||
[#list(..for num in value.Numbers {([#num],)})],
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
# Simple example
|
||||
|
||||
This example shows how to render Typst documents directly from strings in Go.
|
||||
|
||||
## The pros and cons
|
||||
|
||||
The main advantage of this method is that it's really easy to set up.
|
||||
In the most simple case you build your Typst markup by concatenating strings, or by using `fmt.Sprintf`.
|
||||
|
||||
The downside is that the final Typst markup is only generated on demand.
|
||||
This means that you can't easily use the existing Typst tooling to write, update or debug your Typst markup.
|
||||
Especially as your your documents get more complex, you should switch to other methods.
|
@ -1,37 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/Dadido3/go-typst"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Convert a time.Time value into Typst markup.
|
||||
date, err := typst.MarshalValue(time.Now())
|
||||
if err != nil {
|
||||
log.Panicf("Failed to marshal date into Typst markup: %v", err)
|
||||
}
|
||||
|
||||
// Write Typst markup into buffer.
|
||||
var markup bytes.Buffer
|
||||
fmt.Fprintf(&markup, `= Hello world
|
||||
|
||||
This document was created at #%s.display() using typst-go.`, date)
|
||||
|
||||
// Compile the prepared markup with Typst and write the result it into `output.pdf`.
|
||||
f, err := os.Create("output.pdf")
|
||||
if err != nil {
|
||||
log.Panicf("Failed to create output file: %v.", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
typstCLI := typst.CLI{}
|
||||
if err := typstCLI.Compile(&markup, f, nil); err != nil {
|
||||
log.Panic("failed to compile document: %w", err)
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Run the example as a test.
|
||||
func TestMain(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Error(r)
|
||||
}
|
||||
}()
|
||||
|
||||
main()
|
||||
}
|
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -11,7 +11,7 @@ import (
|
||||
"github.com/smasher164/xid"
|
||||
)
|
||||
|
||||
// CleanIdentifier will return the input cleaned up in a way so that it can safely be used as a Typst identifier.
|
||||
// CleanIdentifier will return the input cleaned up in a way so that it can safely be used as a typst identifier.
|
||||
// This function will replace all illegal characters, which means collisions are possible in some cases.
|
||||
//
|
||||
// See https://github.com/typst/typst/blob/76c24ee6e35715cd14bb892d7b6b8d775c680bf7/crates/typst-syntax/src/lexer.rs#L932 for details.
|
||||
|
15
image.go
15
image.go
@ -1,8 +1,3 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package typst
|
||||
|
||||
import (
|
||||
@ -13,12 +8,12 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Image can be used to encode any image.Image into a Typst image.
|
||||
// Image can be used to encode any image.Image into a typst image.
|
||||
//
|
||||
// For this, just wrap any image.Image with this type before passing it to MarshalValue or a ValueEncoder.
|
||||
// For this, just wrap any image.Image with this type before passing it to MarshalVariable or a VariableEncoder.
|
||||
type Image struct{ image.Image }
|
||||
|
||||
func (i Image) MarshalTypstValue() ([]byte, error) {
|
||||
func (i Image) MarshalTypstVariable() ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := png.Encode(&buffer, i); err != nil {
|
||||
@ -27,10 +22,8 @@ func (i Image) MarshalTypstValue() ([]byte, error) {
|
||||
|
||||
// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
|
||||
|
||||
// TODO: Consider using raw pixel encoding instead of PNG
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("image.decode(bytes((") // TODO: Pass bytes directly to image once Typst 0.12.0 is not supported anymore
|
||||
buf.WriteString("image.decode(bytes((")
|
||||
for _, b := range buffer.Bytes() {
|
||||
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
|
||||
}
|
||||
|
@ -33,26 +33,18 @@ func (p *testImage) Opaque() bool {
|
||||
}
|
||||
|
||||
func TestImage(t *testing.T) {
|
||||
img := &testImage{image.Rect(0, 0, 256, 256)}
|
||||
img := &testImage{image.Rect(0, 0, 255, 255)}
|
||||
|
||||
// Wrap image.
|
||||
typstImage := typst.Image{img}
|
||||
|
||||
cli := typst.CLI{}
|
||||
|
||||
var r bytes.Buffer
|
||||
r := bytes.NewBufferString(`= Image test
|
||||
|
||||
if err := typst.InjectValues(&r, map[string]any{"TestImage": typstImage}); err != nil {
|
||||
t.Fatalf("Failed to inject values into Typst markup: %v.", err)
|
||||
}
|
||||
#TestImage`)
|
||||
|
||||
r.WriteString(`= Image test
|
||||
|
||||
#TestImage
|
||||
|
||||
#assert(type(TestImage) == content, message: "TestImage is not of expected type: got " + str(type(TestImage)) + ", want content")`) // TODO: Add another assertion for the image width and height as soon as it's possible to query that
|
||||
|
||||
if err := cli.Compile(&r, io.Discard, nil); err != nil {
|
||||
if err := cli.CompileWithVariables(r, io.Discard, nil, map[string]any{"TestImage": typstImage}); err != nil {
|
||||
t.Fatalf("Failed to compile document: %v.", err)
|
||||
}
|
||||
}
|
||||
|
47
util.go
47
util.go
@ -1,47 +0,0 @@
|
||||
// Copyright (c) 2025 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package typst
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// InjectValues will write the given key-value pairs as Typst markup into output.
|
||||
// This can be used to inject Go values into Typst documents.
|
||||
//
|
||||
// Every key in values needs to be a valid identifier, otherwise this function will return an error.
|
||||
// Every value in values will be marshaled according to ValueEncoder into equivalent Typst markup.
|
||||
//
|
||||
// Passing {"foo": 1, "bar": 60 * time.Second} as values will produce the following output:
|
||||
//
|
||||
// #let foo = 1
|
||||
// #let bar = duration(seconds: 60)
|
||||
func InjectValues(output io.Writer, values map[string]any) error {
|
||||
enc := NewValueEncoder(output)
|
||||
|
||||
// We will have to iterate over the sorted list of map keys.
|
||||
// Otherwise the output is not deterministic, and tests will fail randomly.
|
||||
for _, k := range slices.Sorted(maps.Keys(values)) {
|
||||
v := values[k]
|
||||
if !IsIdentifier(k) {
|
||||
return fmt.Errorf("%q is not a valid identifier", k)
|
||||
}
|
||||
if _, err := output.Write([]byte("#let " + CleanIdentifier(k) + " = ")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return fmt.Errorf("failed to encode values with key %q: %w", k, err)
|
||||
}
|
||||
if _, err := output.Write([]byte("\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
36
util_test.go
36
util_test.go
@ -1,36 +0,0 @@
|
||||
package typst
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestInjectValues(t *testing.T) {
|
||||
type args struct {
|
||||
values map[string]any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantOutput string
|
||||
wantErr bool
|
||||
}{
|
||||
{"empty", args{values: nil}, "", false},
|
||||
{"nil", args{values: map[string]any{"foo": nil}}, "#let foo = none\n", false},
|
||||
{"example", args{values: map[string]any{"foo": 1, "bar": 60 * time.Second}}, "#let bar = duration(seconds: 60)\n#let foo = 1\n", false},
|
||||
{"invalid identifier", args{values: map[string]any{"foo😀": 1}}, "", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
output := &bytes.Buffer{}
|
||||
if err := InjectValues(output, tt.args.values); (err != nil) != tt.wantErr {
|
||||
t.Errorf("InjectValues() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotOutput := output.String(); gotOutput != tt.wantOutput {
|
||||
t.Errorf("InjectValues() = %v, want %v", gotOutput, tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// Copyright (c) 2025 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package typst
|
||||
|
||||
import "io"
|
||||
|
||||
// This exists for compatibility reasons.
|
||||
|
||||
// Deprecated: Use NewValueEncoder instead, as this will be removed in a future version.
|
||||
func NewVariableEncoder(w io.Writer) *ValueEncoder { return NewValueEncoder(w) }
|
||||
|
||||
// Deprecated: Use MarshalValue instead, as this will be removed in a future version.
|
||||
func MarshalVariable(v any) ([]byte, error) { return MarshalValue(v) }
|
||||
|
||||
// Deprecated: Use ValueMarshaler interface instead, as this will be removed in a future version.
|
||||
type VariableMarshaler interface {
|
||||
MarshalTypstVariable() ([]byte, error)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -18,11 +18,11 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// MarshalValue takes any Go type and returns a Typst markup representation as a byte slice.
|
||||
func MarshalValue(v any) ([]byte, error) {
|
||||
// MarshalVariable takes any go type and returns a typst markup representation as a byte slice.
|
||||
func MarshalVariable(v any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
enc := NewValueEncoder(&buf)
|
||||
enc := NewVariableEncoder(&buf)
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -30,37 +30,37 @@ func MarshalValue(v any) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// ValueMarshaler can be implemented by types to support custom Typst marshaling.
|
||||
type ValueMarshaler interface {
|
||||
MarshalTypstValue() ([]byte, error)
|
||||
// VariableMarshaler can be implemented by types to support custom typst marshaling.
|
||||
type VariableMarshaler interface {
|
||||
MarshalTypstVariable() ([]byte, error)
|
||||
}
|
||||
|
||||
type ValueEncoder struct {
|
||||
type VariableEncoder struct {
|
||||
indentLevel int
|
||||
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
// NewValueEncoder returns a new encoder that writes into w.
|
||||
func NewValueEncoder(w io.Writer) *ValueEncoder {
|
||||
return &ValueEncoder{
|
||||
// NewVariableEncoder returns a new encoder that writes into w.
|
||||
func NewVariableEncoder(w io.Writer) *VariableEncoder {
|
||||
return &VariableEncoder{
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) Encode(v any) error {
|
||||
func (e *VariableEncoder) Encode(v any) error {
|
||||
return e.marshal(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) writeString(s string) error {
|
||||
func (e *VariableEncoder) writeString(s string) error {
|
||||
return e.writeBytes([]byte(s))
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) writeRune(r rune) error {
|
||||
func (e *VariableEncoder) writeRune(r rune) error {
|
||||
return e.writeBytes([]byte{byte(r)})
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) writeStringLiteral(s []byte) error {
|
||||
func (e *VariableEncoder) writeStringLiteral(s []byte) error {
|
||||
dst := make([]byte, 0, len(s)+5)
|
||||
|
||||
dst = append(dst, '"')
|
||||
@ -85,7 +85,7 @@ func (e *ValueEncoder) writeStringLiteral(s []byte) error {
|
||||
return e.writeBytes(dst)
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) writeBytes(b []byte) error {
|
||||
func (e *VariableEncoder) writeBytes(b []byte) error {
|
||||
if _, err := e.writer.Write(b); err != nil {
|
||||
return fmt.Errorf("failed to write into writer: %w", err)
|
||||
}
|
||||
@ -93,11 +93,11 @@ func (e *ValueEncoder) writeBytes(b []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) writeIndentationCharacters() error {
|
||||
func (e *VariableEncoder) writeIndentationCharacters() error {
|
||||
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||
func (e *VariableEncoder) marshal(v reflect.Value) error {
|
||||
if !v.IsValid() {
|
||||
return e.writeString("none")
|
||||
//return fmt.Errorf("invalid reflect.Value %v", v)
|
||||
@ -113,7 +113,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||
return nil
|
||||
case *time.Time:
|
||||
if i == nil {
|
||||
return e.writeString("none")
|
||||
e.writeString("none")
|
||||
return nil
|
||||
}
|
||||
if err := e.encodeTime(*i); err != nil {
|
||||
return err
|
||||
@ -126,7 +127,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||
return nil
|
||||
case *time.Duration:
|
||||
if i == nil {
|
||||
return e.writeString("none")
|
||||
e.writeString("none")
|
||||
return nil
|
||||
}
|
||||
if err := e.encodeDuration(*i); err != nil {
|
||||
return err
|
||||
@ -134,18 +136,8 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if t.Implements(reflect.TypeFor[ValueMarshaler]()) {
|
||||
if m, ok := v.Interface().(ValueMarshaler); ok {
|
||||
bytes, err := m.MarshalTypstValue()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling MarshalTypstValue for type %s: %w", t.String(), err)
|
||||
}
|
||||
return e.writeBytes(bytes)
|
||||
}
|
||||
return e.writeString("none")
|
||||
}
|
||||
// TODO: Handle images, maybe create a wrapper type that does this
|
||||
|
||||
// TODO: Remove this in a future update, it's only here for compatibility reasons
|
||||
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
|
||||
if m, ok := v.Interface().(VariableMarshaler); ok {
|
||||
bytes, err := m.MarshalTypstVariable()
|
||||
@ -232,11 +224,11 @@ func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeString(v reflect.Value) error {
|
||||
func (e *VariableEncoder) encodeString(v reflect.Value) error {
|
||||
return e.writeStringLiteral([]byte(v.String()))
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||
if v.NumField() == 0 {
|
||||
return e.writeString("()")
|
||||
}
|
||||
@ -286,7 +278,7 @@ func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||
return e.writeRune(')')
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||
// From encoding/json/encode.go.
|
||||
if v.Kind() == reflect.String {
|
||||
return v.String(), nil
|
||||
@ -307,7 +299,7 @@ func (e *ValueEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||
return "", fmt.Errorf("unsupported map key type %q", v.Type().String())
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeMap(v reflect.Value) error {
|
||||
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
||||
if v.Len() == 0 {
|
||||
return e.writeString("()")
|
||||
}
|
||||
@ -367,12 +359,12 @@ func (e *ValueEncoder) encodeMap(v reflect.Value) error {
|
||||
return e.writeRune(')')
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
|
||||
func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
|
||||
if err := e.writeString("bytes(("); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Encode byte slice via base64 or similar and use a Typst package to convert it into the corresponding bytes type
|
||||
// TODO: Encode byte slice via base64 or similar and use a typst package to convert it into the corresponding bytes type
|
||||
|
||||
for i, b := range bb {
|
||||
if i > 0 {
|
||||
@ -395,7 +387,7 @@ func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
|
||||
return e.writeString("))")
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
||||
func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
||||
|
||||
// Special case for byte slices.
|
||||
if t.Elem().Kind() == reflect.Uint8 {
|
||||
@ -427,7 +419,7 @@ func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
||||
return e.writeRune(')')
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeArray(v reflect.Value) error {
|
||||
func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
||||
if err := e.writeRune('('); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -453,7 +445,7 @@ func (e *ValueEncoder) encodeArray(v reflect.Value) error {
|
||||
return e.writeRune(')')
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeTime(t time.Time) error {
|
||||
func (e *VariableEncoder) encodeTime(t time.Time) error {
|
||||
return e.writeString(fmt.Sprintf("datetime(year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d)",
|
||||
t.Year(),
|
||||
t.Month(),
|
||||
@ -464,6 +456,6 @@ func (e *ValueEncoder) encodeTime(t time.Time) error {
|
||||
))
|
||||
}
|
||||
|
||||
func (e *ValueEncoder) encodeDuration(d time.Duration) error {
|
||||
func (e *VariableEncoder) encodeDuration(d time.Duration) error {
|
||||
return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds()))))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2024-2025 David Vogel
|
||||
// Copyright (c) 2024 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -18,7 +18,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestMarshalValue(t *testing.T) {
|
||||
func TestMarshalVariable(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg any
|
||||
@ -31,33 +31,33 @@ func TestMarshalValue(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := typst.MarshalValue(tt.arg)
|
||||
got, err := typst.MarshalVariable(tt.arg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MarshalValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
t.Errorf("MarshalVariable() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MarshalValue() = %v, want %v", got, tt.want)
|
||||
t.Errorf("MarshalVariable() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type ValueMarshalerType []byte
|
||||
type VariableMarshalerType []byte
|
||||
|
||||
func (v ValueMarshalerType) MarshalTypstValue() ([]byte, error) {
|
||||
func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) {
|
||||
result := append([]byte{'"'}, v...)
|
||||
result = append(result, '"')
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type ValueMarshalerTypePointer []byte
|
||||
type VariableMarshalerTypePointer []byte
|
||||
|
||||
var valueMarshalerTypePointer = ValueMarshalerTypePointer("test")
|
||||
var valueMarshalerTypePointerNil = ValueMarshalerTypePointer(nil)
|
||||
var variableMarshalerTypePointer = VariableMarshalerTypePointer("test")
|
||||
var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil)
|
||||
|
||||
func (v *ValueMarshalerTypePointer) MarshalTypstValue() ([]byte, error) {
|
||||
func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) {
|
||||
if v != nil {
|
||||
result := append([]byte{'"'}, *v...)
|
||||
result = append(result, '"')
|
||||
@ -87,7 +87,7 @@ func (v *TextMarshalerTypePointer) MarshalText() ([]byte, error) {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
|
||||
func TestValueEncoder(t *testing.T) {
|
||||
func TestVariableEncoder(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -156,11 +156,11 @@ func TestValueEncoder(t *testing.T) {
|
||||
{"byte slice 1", []byte{1}, false, `bytes((1,))`},
|
||||
{"byte slice empty", []byte{}, false, `bytes(())`},
|
||||
{"byte slice nil", []byte(nil), false, `bytes(())`},
|
||||
{"MarshalTypstValue value", ValueMarshalerType("test"), false, `"test"`},
|
||||
{"MarshalTypstValue value nil", ValueMarshalerType(nil), false, `""`},
|
||||
{"MarshalTypstValue pointer", &valueMarshalerTypePointer, false, `"test"`},
|
||||
{"MarshalTypstValue pointer nil", &valueMarshalerTypePointerNil, false, `""`},
|
||||
{"MarshalTypstValue nil pointer", struct{ A *ValueMarshalerTypePointer }{nil}, true, ``},
|
||||
{"MarshalTypstVariable value", VariableMarshalerType("test"), false, `"test"`},
|
||||
{"MarshalTypstVariable value nil", VariableMarshalerType(nil), false, `""`},
|
||||
{"MarshalTypstVariable pointer", &variableMarshalerTypePointer, false, `"test"`},
|
||||
{"MarshalTypstVariable pointer nil", &variableMarshalerTypePointerNil, false, `""`},
|
||||
{"MarshalTypstVariable nil pointer", struct{ A *VariableMarshalerTypePointer }{nil}, true, ``},
|
||||
{"MarshalText value", TextMarshalerType("test"), false, `"test"`},
|
||||
{"MarshalText value nil", TextMarshalerType(nil), false, `""`},
|
||||
{"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`},
|
||||
@ -179,12 +179,12 @@ func TestValueEncoder(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var result bytes.Buffer
|
||||
vEnc := typst.NewValueEncoder(&result)
|
||||
vEnc := typst.NewVariableEncoder(&result)
|
||||
|
||||
err := vEnc.Encode(tt.params)
|
||||
switch {
|
||||
case err != nil && !tt.wantErr:
|
||||
t.Fatalf("Failed to encode Typst values: %v", err)
|
||||
t.Fatalf("Failed to encode typst variables: %v", err)
|
||||
case err == nil && tt.wantErr:
|
||||
t.Fatalf("Expected error, but got none")
|
||||
}
|
||||
@ -199,7 +199,7 @@ func TestValueEncoder(t *testing.T) {
|
||||
input := strings.NewReader("#" + result.String())
|
||||
var output bytes.Buffer
|
||||
if err := typstCLI.Compile(input, &output, nil); err != nil {
|
||||
t.Errorf("Failed to compile generated Typst markup: %v", err)
|
||||
t.Errorf("Failed to compile generated typst markup: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue
Block a user