mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-04-19 15:43:15 +00:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
bdbda5f874 | ||
e1879e4d36 | |||
ab3cee4666 | |||
c3876b340b | |||
7c87e3fee8 | |||
4ee94ec233 | |||
6ecfb61e5b | |||
2a1d2990e7 | |||
9268a6691e | |||
69bd0ed5b5 | |||
112898d1d7 | |||
0668d15070 | |||
c2d62b2373 | |||
|
d7fc966a42 | ||
|
2b21d37fce | ||
|
fb12dfb9fa |
29
.github/workflows/lint.yml
vendored
Normal file
29
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: ['1.23.x']
|
go-version: ['1.23.x']
|
||||||
typst-version: ['0.12.0', '0.13.0']
|
typst-version: ['0.12.0', '0.13.0', '0.13.1']
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install typst-cli ${{ matrix.typst-version }} from crates.io
|
- name: Install typst-cli ${{ matrix.typst-version }} from crates.io
|
||||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -2,6 +2,7 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Dadido",
|
"Dadido",
|
||||||
"Foogaloo",
|
"Foogaloo",
|
||||||
|
"golangci",
|
||||||
"typst",
|
"typst",
|
||||||
"Vogel"
|
"Vogel"
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,7 @@ Supported Typst versions are tested by unit tests to ensure compatibility.
|
|||||||
|
|
||||||
- Typst 0.12.0
|
- Typst 0.12.0
|
||||||
- Typst 0.13.0
|
- Typst 0.13.0
|
||||||
|
- Typst 0.13.1
|
||||||
|
|
||||||
While breaking changes may occur, i aim to minimize disruptions.
|
While breaking changes may occur, i aim to minimize disruptions.
|
||||||
Use at your own discretion for production systems.
|
Use at your own discretion for production systems.
|
||||||
@ -20,7 +21,7 @@ Use at your own discretion for production systems.
|
|||||||
|
|
||||||
- PDF, SVG and PNG generation.
|
- PDF, SVG and PNG generation.
|
||||||
- All Typst parameters are discoverable and documented in [cli-options.go](cli-options.go).
|
- All Typst parameters are discoverable and documented in [cli-options.go](cli-options.go).
|
||||||
- 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.
|
- 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.
|
||||||
- Errors from Typst CLI are returned as structured Go error objects with detailed information, such as line numbers and file paths.
|
- 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.
|
- Uses stdio; No temporary files will be created.
|
||||||
- Good unit test coverage.
|
- Good unit test coverage.
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -22,6 +22,14 @@ const (
|
|||||||
OutputFormatHTML OutputFormat = "html" // this format is only available since 0.13.0
|
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 {
|
type CLIOptions struct {
|
||||||
Root string // Configures the project root (for absolute paths).
|
Root string // Configures the project root (for absolute paths).
|
||||||
Input map[string]string // String key-value pairs visible through `sys.inputs`.
|
Input map[string]string // String key-value pairs visible through `sys.inputs`.
|
||||||
@ -42,13 +50,10 @@ type CLIOptions struct {
|
|||||||
Format OutputFormat // The format of the output file, inferred from the extension by default.
|
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.
|
PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144.
|
||||||
|
|
||||||
// One (or multiple comma-separated) PDF standards that Typst will enforce conformance with.
|
// One (or multiple) PDF standards that Typst will enforce conformance with.
|
||||||
//
|
//
|
||||||
// Possible values:
|
// See typst.PDFStandard for possible values.
|
||||||
//
|
PDFStandards []PDFStandard
|
||||||
// - 1.7: PDF 1.7
|
|
||||||
// - a-2b: PDF/A-2b
|
|
||||||
PDFStandard string
|
|
||||||
|
|
||||||
Custom []string // Custom command line options go here.
|
Custom []string // Custom command line options go here.
|
||||||
}
|
}
|
||||||
@ -112,9 +117,18 @@ func (c *CLIOptions) Args() (result []string) {
|
|||||||
result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10))
|
result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.PDFStandard != "" {
|
if len(c.PDFStandards) > 0 {
|
||||||
result = append(result, "--pdf-standard", c.PDFStandard)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
24
cli-options_test.go
Normal file
24
cli-options_test.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -15,12 +15,12 @@ import (
|
|||||||
// TODO: Add docker support to CLI, by calling docker run instead
|
// TODO: Add docker support to CLI, by calling docker run instead
|
||||||
|
|
||||||
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.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (c CLI) VersionString() (string, error) {
|
||||||
// Get path of executable.
|
// Get path of executable.
|
||||||
execPath := ExecutablePath
|
execPath := ExecutablePath
|
||||||
@ -46,7 +46,7 @@ func (c CLI) VersionString() (string, error) {
|
|||||||
return output.String(), nil
|
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.
|
// The options parameter is optional.
|
||||||
func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error {
|
func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) error {
|
||||||
args := []string{"c"}
|
args := []string{"c"}
|
||||||
@ -80,22 +80,17 @@ func (c CLI) Compile(input io.Reader, output io.Writer, options *CLIOptions) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile takes a typst document from input, and renders it into the output writer.
|
// CompileWithVariables takes a Typst document from input, and renders it into the output writer.
|
||||||
// The options parameter is optional.
|
// The options parameter is optional.
|
||||||
//
|
//
|
||||||
// Additionally this will inject the given map of variables into the global scope of the typst document.
|
// 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 {
|
func (c CLI) CompileWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
|
||||||
varBuffer := bytes.Buffer{}
|
varBuffer := bytes.Buffer{}
|
||||||
|
|
||||||
// TODO: Use io.pipe instead of a bytes.Buffer
|
if err := InjectValues(&varBuffer, variables); err != nil {
|
||||||
|
return fmt.Errorf("failed to inject values into Typst markup: %w", err)
|
||||||
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)
|
reader := io.MultiReader(&varBuffer, input)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -7,5 +7,5 @@
|
|||||||
|
|
||||||
package typst
|
package typst
|
||||||
|
|
||||||
// The path to the typst executable.
|
// The path to the Typst executable.
|
||||||
var ExecutablePath = "typst"
|
var ExecutablePath = "typst"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
package typst
|
package typst
|
||||||
|
|
||||||
// The path to the typst executable.
|
// The path to the Typst executable.
|
||||||
// We assume the executable is in the current working directory.
|
// We assume the executable is in the current working directory.
|
||||||
//var ExecutablePath = "." + string(filepath.Separator) + filepath.Join("typst.exe")
|
//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"
|
var ExecutablePath = "typst.exe"
|
||||||
|
@ -14,13 +14,13 @@ import (
|
|||||||
// ErrorDetails contains the details of a typst.Error.
|
// ErrorDetails contains the details of a typst.Error.
|
||||||
type ErrorDetails struct {
|
type ErrorDetails struct {
|
||||||
Message string // The parsed error message.
|
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.
|
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.
|
Column int // Column of the error. Zero value means that there is no further information.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error represents a typst error.
|
// Error represents an error as returned by Typst.
|
||||||
// This can contain multiple sub-errors or sub-warnings.
|
// This can contain multiple sub-errors or sub-warnings which are listed in the field Details.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Inner error
|
Inner error
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,33 +0,0 @@
|
|||||||
#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],)})],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
13
examples/passing-values/README.md
Normal file
13
examples/passing-values/README.md
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# 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,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@ -8,7 +9,7 @@ import (
|
|||||||
"github.com/Dadido3/go-typst"
|
"github.com/Dadido3/go-typst"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DataEntry contains fake data to be passed to typst.
|
// DataEntry contains data to be passed to Typst.
|
||||||
type DataEntry struct {
|
type DataEntry struct {
|
||||||
Name string
|
Name string
|
||||||
Size struct{ X, Y, Z float64 }
|
Size struct{ X, Y, Z float64 }
|
||||||
@ -25,21 +26,28 @@ var TestData = []DataEntry{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
typstCLI := typst.CLI{}
|
var markup bytes.Buffer
|
||||||
|
|
||||||
r, err := os.Open("template.typ")
|
// Inject Go values as Typst markup.
|
||||||
if err != nil {
|
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 open template file for reading: %v.", err)
|
log.Panicf("Failed to inject values into Typst markup: %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")
|
f, err := os.Create("output.pdf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("Failed to create output file: %v.", err)
|
log.Panicf("Failed to create output file: %v.", err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
if err := typstCLI.CompileWithVariables(r, f, nil, map[string]any{"Data": TestData}); err != nil {
|
typstCLI := typst.CLI{}
|
||||||
|
if err := typstCLI.Compile(&markup, f, nil); err != nil {
|
||||||
log.Panicf("Failed to compile document: %v.", err)
|
log.Panicf("Failed to compile document: %v.", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
16
examples/passing-values/main_test.go
Normal file
16
examples/passing-values/main_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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()
|
||||||
|
}
|
BIN
examples/passing-values/output.pdf
Normal file
BIN
examples/passing-values/output.pdf
Normal file
Binary file not shown.
@ -1,5 +1,12 @@
|
|||||||
#let Data = (
|
// Prepare data that will be used as preview.
|
||||||
|
#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: (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: (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)),
|
(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)
|
36
examples/passing-values/template.typ
Normal file
36
examples/passing-values/template.typ
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#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],)})],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
12
examples/simple/README.md
Normal file
12
examples/simple/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 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.
|
37
examples/simple/main.go
Normal file
37
examples/simple/main.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
16
examples/simple/main_test.go
Normal file
16
examples/simple/main_test.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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()
|
||||||
|
}
|
BIN
examples/simple/output.pdf
Normal file
BIN
examples/simple/output.pdf
Normal file
Binary file not shown.
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/smasher164/xid"
|
"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.
|
// 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.
|
// See https://github.com/typst/typst/blob/76c24ee6e35715cd14bb892d7b6b8d775c680bf7/crates/typst-syntax/src/lexer.rs#L932 for details.
|
||||||
|
15
image.go
15
image.go
@ -1,3 +1,8 @@
|
|||||||
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
package typst
|
package typst
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,12 +13,12 @@ import (
|
|||||||
"strconv"
|
"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 MarshalVariable or a VariableEncoder.
|
// For this, just wrap any image.Image with this type before passing it to MarshalValue or a ValueEncoder.
|
||||||
type Image struct{ image.Image }
|
type Image struct{ image.Image }
|
||||||
|
|
||||||
func (i Image) MarshalTypstVariable() ([]byte, error) {
|
func (i Image) MarshalTypstValue() ([]byte, error) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
if err := png.Encode(&buffer, i); err != nil {
|
if err := png.Encode(&buffer, i); err != nil {
|
||||||
@ -22,8 +27,10 @@ func (i Image) MarshalTypstVariable() ([]byte, error) {
|
|||||||
|
|
||||||
// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
|
// TODO: Make image encoding more efficient: Use reader/writer, baseXX encoding
|
||||||
|
|
||||||
|
// TODO: Consider using raw pixel encoding instead of PNG
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
buf.WriteString("image.decode(bytes((")
|
buf.WriteString("image.decode(bytes((") // TODO: Pass bytes directly to image once Typst 0.12.0 is not supported anymore
|
||||||
for _, b := range buffer.Bytes() {
|
for _, b := range buffer.Bytes() {
|
||||||
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
|
buf.WriteString(strconv.FormatUint(uint64(b), 10) + ",")
|
||||||
}
|
}
|
||||||
|
@ -33,18 +33,26 @@ func (p *testImage) Opaque() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImage(t *testing.T) {
|
func TestImage(t *testing.T) {
|
||||||
img := &testImage{image.Rect(0, 0, 255, 255)}
|
img := &testImage{image.Rect(0, 0, 256, 256)}
|
||||||
|
|
||||||
// Wrap image.
|
// Wrap image.
|
||||||
typstImage := typst.Image{img}
|
typstImage := typst.Image{img}
|
||||||
|
|
||||||
cli := typst.CLI{}
|
cli := typst.CLI{}
|
||||||
|
|
||||||
r := bytes.NewBufferString(`= Image test
|
var r bytes.Buffer
|
||||||
|
|
||||||
#TestImage`)
|
if err := typst.InjectValues(&r, map[string]any{"TestImage": typstImage}); err != nil {
|
||||||
|
t.Fatalf("Failed to inject values into Typst markup: %v.", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := cli.CompileWithVariables(r, io.Discard, nil, map[string]any{"TestImage": typstImage}); err != nil {
|
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 {
|
||||||
t.Fatalf("Failed to compile document: %v.", err)
|
t.Fatalf("Failed to compile document: %v.", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
util.go
Normal file
47
util.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// 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
Normal file
36
util_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -18,11 +18,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarshalVariable takes any go type and returns a typst markup representation as a byte slice.
|
// MarshalValue takes any Go type and returns a Typst markup representation as a byte slice.
|
||||||
func MarshalVariable(v any) ([]byte, error) {
|
func MarshalValue(v any) ([]byte, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
enc := NewVariableEncoder(&buf)
|
enc := NewValueEncoder(&buf)
|
||||||
if err := enc.Encode(v); err != nil {
|
if err := enc.Encode(v); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -30,37 +30,37 @@ func MarshalVariable(v any) ([]byte, error) {
|
|||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// VariableMarshaler can be implemented by types to support custom typst marshaling.
|
// ValueMarshaler can be implemented by types to support custom Typst marshaling.
|
||||||
type VariableMarshaler interface {
|
type ValueMarshaler interface {
|
||||||
MarshalTypstVariable() ([]byte, error)
|
MarshalTypstValue() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VariableEncoder struct {
|
type ValueEncoder struct {
|
||||||
indentLevel int
|
indentLevel int
|
||||||
|
|
||||||
writer io.Writer
|
writer io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVariableEncoder returns a new encoder that writes into w.
|
// NewValueEncoder returns a new encoder that writes into w.
|
||||||
func NewVariableEncoder(w io.Writer) *VariableEncoder {
|
func NewValueEncoder(w io.Writer) *ValueEncoder {
|
||||||
return &VariableEncoder{
|
return &ValueEncoder{
|
||||||
writer: w,
|
writer: w,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) Encode(v any) error {
|
func (e *ValueEncoder) Encode(v any) error {
|
||||||
return e.marshal(reflect.ValueOf(v))
|
return e.marshal(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) writeString(s string) error {
|
func (e *ValueEncoder) writeString(s string) error {
|
||||||
return e.writeBytes([]byte(s))
|
return e.writeBytes([]byte(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) writeRune(r rune) error {
|
func (e *ValueEncoder) writeRune(r rune) error {
|
||||||
return e.writeBytes([]byte{byte(r)})
|
return e.writeBytes([]byte{byte(r)})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) writeStringLiteral(s []byte) error {
|
func (e *ValueEncoder) writeStringLiteral(s []byte) error {
|
||||||
dst := make([]byte, 0, len(s)+5)
|
dst := make([]byte, 0, len(s)+5)
|
||||||
|
|
||||||
dst = append(dst, '"')
|
dst = append(dst, '"')
|
||||||
@ -85,7 +85,7 @@ func (e *VariableEncoder) writeStringLiteral(s []byte) error {
|
|||||||
return e.writeBytes(dst)
|
return e.writeBytes(dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) writeBytes(b []byte) error {
|
func (e *ValueEncoder) writeBytes(b []byte) error {
|
||||||
if _, err := e.writer.Write(b); err != nil {
|
if _, err := e.writer.Write(b); err != nil {
|
||||||
return fmt.Errorf("failed to write into writer: %w", err)
|
return fmt.Errorf("failed to write into writer: %w", err)
|
||||||
}
|
}
|
||||||
@ -93,11 +93,11 @@ func (e *VariableEncoder) writeBytes(b []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) writeIndentationCharacters() error {
|
func (e *ValueEncoder) writeIndentationCharacters() error {
|
||||||
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) marshal(v reflect.Value) error {
|
func (e *ValueEncoder) marshal(v reflect.Value) error {
|
||||||
if !v.IsValid() {
|
if !v.IsValid() {
|
||||||
return e.writeString("none")
|
return e.writeString("none")
|
||||||
//return fmt.Errorf("invalid reflect.Value %v", v)
|
//return fmt.Errorf("invalid reflect.Value %v", v)
|
||||||
@ -113,8 +113,7 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
|
|||||||
return nil
|
return nil
|
||||||
case *time.Time:
|
case *time.Time:
|
||||||
if i == nil {
|
if i == nil {
|
||||||
e.writeString("none")
|
return e.writeString("none")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if err := e.encodeTime(*i); err != nil {
|
if err := e.encodeTime(*i); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -127,8 +126,7 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
|
|||||||
return nil
|
return nil
|
||||||
case *time.Duration:
|
case *time.Duration:
|
||||||
if i == nil {
|
if i == nil {
|
||||||
e.writeString("none")
|
return e.writeString("none")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
if err := e.encodeDuration(*i); err != nil {
|
if err := e.encodeDuration(*i); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -136,8 +134,18 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Handle images, maybe create a wrapper type that does this
|
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: Remove this in a future update, it's only here for compatibility reasons
|
||||||
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
|
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
|
||||||
if m, ok := v.Interface().(VariableMarshaler); ok {
|
if m, ok := v.Interface().(VariableMarshaler); ok {
|
||||||
bytes, err := m.MarshalTypstVariable()
|
bytes, err := m.MarshalTypstVariable()
|
||||||
@ -224,11 +232,11 @@ func (e *VariableEncoder) marshal(v reflect.Value) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeString(v reflect.Value) error {
|
func (e *ValueEncoder) encodeString(v reflect.Value) error {
|
||||||
return e.writeStringLiteral([]byte(v.String()))
|
return e.writeStringLiteral([]byte(v.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
func (e *ValueEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||||
if v.NumField() == 0 {
|
if v.NumField() == 0 {
|
||||||
return e.writeString("()")
|
return e.writeString("()")
|
||||||
}
|
}
|
||||||
@ -278,7 +286,7 @@ func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
|||||||
return e.writeRune(')')
|
return e.writeRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
func (e *ValueEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||||
// From encoding/json/encode.go.
|
// From encoding/json/encode.go.
|
||||||
if v.Kind() == reflect.String {
|
if v.Kind() == reflect.String {
|
||||||
return v.String(), nil
|
return v.String(), nil
|
||||||
@ -299,7 +307,7 @@ func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
|||||||
return "", fmt.Errorf("unsupported map key type %q", v.Type().String())
|
return "", fmt.Errorf("unsupported map key type %q", v.Type().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
func (e *ValueEncoder) encodeMap(v reflect.Value) error {
|
||||||
if v.Len() == 0 {
|
if v.Len() == 0 {
|
||||||
return e.writeString("()")
|
return e.writeString("()")
|
||||||
}
|
}
|
||||||
@ -359,12 +367,12 @@ func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
|||||||
return e.writeRune(')')
|
return e.writeRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
|
func (e *ValueEncoder) EncodeByteSlice(bb []byte) error {
|
||||||
if err := e.writeString("bytes(("); err != nil {
|
if err := e.writeString("bytes(("); err != nil {
|
||||||
return err
|
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 {
|
for i, b := range bb {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@ -387,7 +395,7 @@ func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
|
|||||||
return e.writeString("))")
|
return e.writeString("))")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
func (e *ValueEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
||||||
|
|
||||||
// Special case for byte slices.
|
// Special case for byte slices.
|
||||||
if t.Elem().Kind() == reflect.Uint8 {
|
if t.Elem().Kind() == reflect.Uint8 {
|
||||||
@ -419,7 +427,7 @@ func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
|||||||
return e.writeRune(')')
|
return e.writeRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
func (e *ValueEncoder) encodeArray(v reflect.Value) error {
|
||||||
if err := e.writeRune('('); err != nil {
|
if err := e.writeRune('('); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -445,7 +453,7 @@ func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
|||||||
return e.writeRune(')')
|
return e.writeRune(')')
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeTime(t time.Time) error {
|
func (e *ValueEncoder) encodeTime(t time.Time) error {
|
||||||
return e.writeString(fmt.Sprintf("datetime(year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d)",
|
return e.writeString(fmt.Sprintf("datetime(year: %d, month: %d, day: %d, hour: %d, minute: %d, second: %d)",
|
||||||
t.Year(),
|
t.Year(),
|
||||||
t.Month(),
|
t.Month(),
|
||||||
@ -456,6 +464,6 @@ func (e *VariableEncoder) encodeTime(t time.Time) error {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeDuration(d time.Duration) error {
|
func (e *ValueEncoder) encodeDuration(d time.Duration) error {
|
||||||
return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds()))))
|
return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds()))))
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2024 David Vogel
|
// Copyright (c) 2024-2025 David Vogel
|
||||||
//
|
//
|
||||||
// This software is released under the MIT License.
|
// This software is released under the MIT License.
|
||||||
// https://opensource.org/licenses/MIT
|
// https://opensource.org/licenses/MIT
|
||||||
@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMarshalVariable(t *testing.T) {
|
func TestMarshalValue(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
arg any
|
arg any
|
||||||
@ -31,33 +31,33 @@ func TestMarshalVariable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got, err := typst.MarshalVariable(tt.arg)
|
got, err := typst.MarshalValue(tt.arg)
|
||||||
if (err != nil) != tt.wantErr {
|
if (err != nil) != tt.wantErr {
|
||||||
t.Errorf("MarshalVariable() error = %v, wantErr %v", err, tt.wantErr)
|
t.Errorf("MarshalValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
t.Errorf("MarshalVariable() = %v, want %v", got, tt.want)
|
t.Errorf("MarshalValue() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type VariableMarshalerType []byte
|
type ValueMarshalerType []byte
|
||||||
|
|
||||||
func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) {
|
func (v ValueMarshalerType) MarshalTypstValue() ([]byte, error) {
|
||||||
result := append([]byte{'"'}, v...)
|
result := append([]byte{'"'}, v...)
|
||||||
result = append(result, '"')
|
result = append(result, '"')
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type VariableMarshalerTypePointer []byte
|
type ValueMarshalerTypePointer []byte
|
||||||
|
|
||||||
var variableMarshalerTypePointer = VariableMarshalerTypePointer("test")
|
var valueMarshalerTypePointer = ValueMarshalerTypePointer("test")
|
||||||
var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil)
|
var valueMarshalerTypePointerNil = ValueMarshalerTypePointer(nil)
|
||||||
|
|
||||||
func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) {
|
func (v *ValueMarshalerTypePointer) MarshalTypstValue() ([]byte, error) {
|
||||||
if v != nil {
|
if v != nil {
|
||||||
result := append([]byte{'"'}, *v...)
|
result := append([]byte{'"'}, *v...)
|
||||||
result = append(result, '"')
|
result = append(result, '"')
|
||||||
@ -87,7 +87,7 @@ func (v *TextMarshalerTypePointer) MarshalText() ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("no data")
|
return nil, fmt.Errorf("no data")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVariableEncoder(t *testing.T) {
|
func TestValueEncoder(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -156,11 +156,11 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
{"byte slice 1", []byte{1}, false, `bytes((1,))`},
|
{"byte slice 1", []byte{1}, false, `bytes((1,))`},
|
||||||
{"byte slice empty", []byte{}, false, `bytes(())`},
|
{"byte slice empty", []byte{}, false, `bytes(())`},
|
||||||
{"byte slice nil", []byte(nil), false, `bytes(())`},
|
{"byte slice nil", []byte(nil), false, `bytes(())`},
|
||||||
{"MarshalTypstVariable value", VariableMarshalerType("test"), false, `"test"`},
|
{"MarshalTypstValue value", ValueMarshalerType("test"), false, `"test"`},
|
||||||
{"MarshalTypstVariable value nil", VariableMarshalerType(nil), false, `""`},
|
{"MarshalTypstValue value nil", ValueMarshalerType(nil), false, `""`},
|
||||||
{"MarshalTypstVariable pointer", &variableMarshalerTypePointer, false, `"test"`},
|
{"MarshalTypstValue pointer", &valueMarshalerTypePointer, false, `"test"`},
|
||||||
{"MarshalTypstVariable pointer nil", &variableMarshalerTypePointerNil, false, `""`},
|
{"MarshalTypstValue pointer nil", &valueMarshalerTypePointerNil, false, `""`},
|
||||||
{"MarshalTypstVariable nil pointer", struct{ A *VariableMarshalerTypePointer }{nil}, true, ``},
|
{"MarshalTypstValue nil pointer", struct{ A *ValueMarshalerTypePointer }{nil}, true, ``},
|
||||||
{"MarshalText value", TextMarshalerType("test"), false, `"test"`},
|
{"MarshalText value", TextMarshalerType("test"), false, `"test"`},
|
||||||
{"MarshalText value nil", TextMarshalerType(nil), false, `""`},
|
{"MarshalText value nil", TextMarshalerType(nil), false, `""`},
|
||||||
{"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`},
|
{"MarshalText pointer", &textMarshalerTypePointer, false, `"test"`},
|
||||||
@ -179,12 +179,12 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
vEnc := typst.NewVariableEncoder(&result)
|
vEnc := typst.NewValueEncoder(&result)
|
||||||
|
|
||||||
err := vEnc.Encode(tt.params)
|
err := vEnc.Encode(tt.params)
|
||||||
switch {
|
switch {
|
||||||
case err != nil && !tt.wantErr:
|
case err != nil && !tt.wantErr:
|
||||||
t.Fatalf("Failed to encode typst variables: %v", err)
|
t.Fatalf("Failed to encode Typst values: %v", err)
|
||||||
case err == nil && tt.wantErr:
|
case err == nil && tt.wantErr:
|
||||||
t.Fatalf("Expected error, but got none")
|
t.Fatalf("Expected error, but got none")
|
||||||
}
|
}
|
||||||
@ -199,7 +199,7 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
input := strings.NewReader("#" + result.String())
|
input := strings.NewReader("#" + result.String())
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
if err := typstCLI.Compile(input, &output, nil); err != nil {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
21
variable-encoder-compat.go
Normal file
21
variable-encoder-compat.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user