mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-04-11 12:13:16 +00:00
Several changes
- Make VariableEncoder write* methods private - Add VariableEncoder method to write correctly escaped string literals - Fix error handling in VariableEncoder - Add support for time.Time and time.Duration - Fix MarshalText usage in VariableEncoder - Encode byte slice so that it is a valid typst bytes object - Extend tests - Add functions to clean and check typst identifiers - Split Error into Error and ErrorWithPath - Add CLIOptions
This commit is contained in:
parent
f4d625eab4
commit
755cee77ac
@ -8,4 +8,4 @@ A library to generate documents via [typst].
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
[typst]: https://typst.app/
|
[typst]: https://typst.app/
|
||||||
|
107
cli-options.go
Normal file
107
cli-options.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package typst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutputFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OutputFormatAuto OutputFormat = ""
|
||||||
|
|
||||||
|
OutputFormatPDF OutputFormat = "pdf"
|
||||||
|
OutputFormatPNG OutputFormat = "png"
|
||||||
|
OutputFormatSVG OutputFormat = "svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLIOptions struct {
|
||||||
|
Root string // Configures the project root (for absolute paths).
|
||||||
|
Input map[string]string // String key-value pairs visible through `sys.inputs`.
|
||||||
|
FontPaths []string // Adds additional directories that are recursively searched for fonts.
|
||||||
|
IgnoreSystemFonts bool // Ensures system fonts won't be searched, unless explicitly included via FontPaths.
|
||||||
|
CreationTime time.Time // The document's creation date. For more information, see https://reproducible-builds.org/specs/source-date-epoch/.
|
||||||
|
PackagePath string // Custom path to local packages, defaults to system-dependent location.
|
||||||
|
PackageCachePath string // Custom path to package cache, defaults to system-dependent location.
|
||||||
|
Jobs int // Number of parallel jobs spawned during compilation, defaults to number of CPUs. Setting it to 1 disables parallelism.
|
||||||
|
|
||||||
|
// Which pages to export. When unspecified, all document pages are exported.
|
||||||
|
//
|
||||||
|
// Pages to export are separated by commas, and can be either simple page numbers (e.g. '2,5' to export only pages 2 and 5) or page ranges (e.g. '2,3-6,8-' to export page 2, pages 3 to 6 (inclusive), page 8 and any pages after it).
|
||||||
|
//
|
||||||
|
// Page numbers are one-indexed and correspond to real page numbers in the document (therefore not being affected by the document's page counter).
|
||||||
|
Pages string
|
||||||
|
|
||||||
|
Format OutputFormat // The format of the output file, inferred from the extension by default.
|
||||||
|
PPI int // The PPI (pixels per inch) to use for PNG export. Defaults to 144.
|
||||||
|
|
||||||
|
// One (or multiple comma-separated) PDF standards that Typst will enforce conformance with.
|
||||||
|
//
|
||||||
|
// Possible values:
|
||||||
|
//
|
||||||
|
// - 1.7: PDF 1.7
|
||||||
|
// - a-2b: PDF/A-2b
|
||||||
|
PDFStandard string
|
||||||
|
|
||||||
|
Custom []string // Custom command line options go here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args returns a list of CLI arguments that should be passed to the executable.
|
||||||
|
func (c *CLIOptions) Args() (result []string) {
|
||||||
|
if c.Root != "" {
|
||||||
|
result = append(result, "--root", c.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range c.Input {
|
||||||
|
result = append(result, "--input", key+"="+value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.FontPaths) > 0 {
|
||||||
|
var paths string
|
||||||
|
for i, path := range c.FontPaths {
|
||||||
|
if i > 0 {
|
||||||
|
paths += string(os.PathListSeparator)
|
||||||
|
}
|
||||||
|
paths += path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.IgnoreSystemFonts {
|
||||||
|
result = append(result, "--ignore-system-fonts")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.CreationTime.IsZero() {
|
||||||
|
result = append(result, "--creation-timestamp", strconv.FormatInt(c.CreationTime.Unix(), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PackagePath != "" {
|
||||||
|
result = append(result, "--package-path", c.PackagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PackageCachePath != "" {
|
||||||
|
result = append(result, "--package-cache-path", c.PackageCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Jobs > 0 {
|
||||||
|
result = append(result, "-j", strconv.FormatInt(int64(c.Jobs), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Pages != "" {
|
||||||
|
result = append(result, "--pages", c.Pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Format != OutputFormatAuto {
|
||||||
|
result = append(result, "-f", string(c.Format))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PPI > 0 {
|
||||||
|
result = append(result, "--ppi", strconv.FormatInt(int64(c.PPI), 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PDFStandard != "" {
|
||||||
|
result = append(result, "--pdf-standard", c.PDFStandard)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
48
cli.go
48
cli.go
@ -2,16 +2,33 @@ package typst
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CLI struct {
|
type CLI struct {
|
||||||
//ExecutablePath string
|
ExecutablePath string // The typst executable path can be overridden here. Otherwise the default path will be used.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CLI) Render(input io.Reader, output io.Writer) error {
|
// TODO: Method for querying typst version
|
||||||
cmd := exec.Command(ExecutablePath, "c", "-", "-")
|
|
||||||
|
// Render takes a typst document from input, and renders it into the output writer.
|
||||||
|
// The options parameter is optional.
|
||||||
|
func (c CLI) Render(input io.Reader, output io.Writer, options *CLIOptions) error {
|
||||||
|
args := []string{"c"}
|
||||||
|
if options != nil {
|
||||||
|
args = append(args, options.Args()...)
|
||||||
|
}
|
||||||
|
args = append(args, "--diagnostic-format", "short", "-", "-")
|
||||||
|
|
||||||
|
// Get path of executable.
|
||||||
|
execPath := ExecutablePath
|
||||||
|
if c.ExecutablePath != "" {
|
||||||
|
execPath = c.ExecutablePath
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(execPath, args...)
|
||||||
cmd.Stdin = input
|
cmd.Stdin = input
|
||||||
cmd.Stdout = output
|
cmd.Stdout = output
|
||||||
|
|
||||||
@ -21,7 +38,7 @@ func (c CLI) Render(input io.Reader, output io.Writer) error {
|
|||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
switch err := err.(type) {
|
switch err := err.(type) {
|
||||||
case *exec.ExitError:
|
case *exec.ExitError:
|
||||||
return NewError(errBuffer.String(), err)
|
return ParseStderr(errBuffer.String(), err)
|
||||||
default:
|
default:
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -30,8 +47,25 @@ func (c CLI) Render(input io.Reader, output io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c CLI) RenderWithVariables(input io.Reader, output io.Writer, variables map[string]any) error {
|
// Render takes a typst document from input, and renders it into the output writer.
|
||||||
reader := io.MultiReader(nil, input)
|
// The options parameter is optional.
|
||||||
|
//
|
||||||
|
// Additionally this will inject the given map of variables into the global scope of the typst document.
|
||||||
|
func (c CLI) RenderWithVariables(input io.Reader, output io.Writer, options *CLIOptions, variables map[string]any) error {
|
||||||
|
varBuffer := bytes.Buffer{}
|
||||||
|
|
||||||
return c.Render(reader, output)
|
// 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)
|
||||||
|
|
||||||
|
return c.Render(reader, output, options)
|
||||||
}
|
}
|
||||||
|
48
cli_test.go
Normal file
48
cli_test.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package typst_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Dadido3/go-typst"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test basic render functionality.
|
||||||
|
func TestCLI_Render(t *testing.T) {
|
||||||
|
const inches = 1
|
||||||
|
const ppi = 144
|
||||||
|
|
||||||
|
cli := typst.CLI{}
|
||||||
|
|
||||||
|
r := bytes.NewBufferString(`#set page(width: ` + strconv.FormatInt(inches, 10) + `in, height: ` + strconv.FormatInt(inches, 10) + `in, margin: (x: 1mm, y: 1mm))
|
||||||
|
= Test
|
||||||
|
|
||||||
|
#lorem(5)`)
|
||||||
|
|
||||||
|
opts := typst.CLIOptions{
|
||||||
|
Format: typst.OutputFormatPNG,
|
||||||
|
PPI: ppi,
|
||||||
|
}
|
||||||
|
|
||||||
|
var w bytes.Buffer
|
||||||
|
if err := cli.Render(r, &w, &opts); err != nil {
|
||||||
|
t.Fatalf("Failed to render document: %v.", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgConf, imgType, err := image.DecodeConfig(&w)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to decode image: %v.", err)
|
||||||
|
}
|
||||||
|
if imgType != "png" {
|
||||||
|
t.Fatalf("Resulting image is of type %q, expected %q.", imgType, "png")
|
||||||
|
}
|
||||||
|
if imgConf.Width != inches*ppi {
|
||||||
|
t.Fatalf("Resulting image width is %d, expected %d.", imgConf.Width, inches*ppi)
|
||||||
|
}
|
||||||
|
if imgConf.Height != inches*ppi {
|
||||||
|
t.Fatalf("Resulting image height is %d, expected %d.", imgConf.Height, inches*ppi)
|
||||||
|
}
|
||||||
|
}
|
111
errors.go
111
errors.go
@ -5,51 +5,92 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stderrRegex = regexp.MustCompile(`^error: (?<error>.+)\n ┌─ (?<path>.+):(?<line>\d+):(?<column>\d+)\n`)
|
|
||||||
|
|
||||||
// Error represents a generic typst error.
|
// Error represents a generic typst error.
|
||||||
type Error struct {
|
type Error struct {
|
||||||
Inner error
|
Inner error
|
||||||
|
|
||||||
Raw string // The raw error string as returned by the executable.
|
Raw string // The raw output from stderr.
|
||||||
|
Message string // The parsed error message.
|
||||||
Message string // Error message from typst.
|
|
||||||
Path string // Path of the typst file where the error is located in.
|
|
||||||
Line int // Line number of the error.
|
|
||||||
Column int // Column of the error.
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewError returns a new error based on the stderr from the typst process.
|
|
||||||
func NewError(stderr string, inner error) *Error {
|
|
||||||
err := Error{
|
|
||||||
Raw: stderr,
|
|
||||||
Inner: inner,
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed := stderrRegex.FindStringSubmatch(stderr)
|
|
||||||
|
|
||||||
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
|
|
||||||
err.Message = parsed[i]
|
|
||||||
}
|
|
||||||
if i := stderrRegex.SubexpIndex("path"); i > 0 && i < len(parsed) {
|
|
||||||
err.Path = parsed[i]
|
|
||||||
}
|
|
||||||
if i := stderrRegex.SubexpIndex("line"); i > 0 && i < len(parsed) {
|
|
||||||
line, _ := strconv.ParseInt(parsed[i], 10, 0)
|
|
||||||
err.Line = int(line)
|
|
||||||
}
|
|
||||||
if i := stderrRegex.SubexpIndex("column"); i > 0 && i < len(parsed) {
|
|
||||||
column, _ := strconv.ParseInt(parsed[i], 10, 0)
|
|
||||||
err.Column = int(column)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
func (e *Error) Error() string {
|
||||||
|
if e.Message != "" {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
return e.Raw
|
return e.Raw
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Error) Unwrap() error {
|
func (e *Error) Unwrap() error {
|
||||||
return e.Inner
|
return e.Inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorWithPath represents a typst error that also contains information about its origin (filepath, line and column).
|
||||||
|
type ErrorWithPath struct {
|
||||||
|
Inner error
|
||||||
|
|
||||||
|
Raw string // The raw error string as returned by the executable.
|
||||||
|
Message string // Error message from typst.
|
||||||
|
|
||||||
|
Path string // Path of the typst file where the error is located in.
|
||||||
|
Line int // Line number of the error.
|
||||||
|
Column int // Column of the error.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorWithPath) Error() string {
|
||||||
|
return e.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorWithPath) Unwrap() error {
|
||||||
|
return e.Inner
|
||||||
|
}
|
||||||
|
|
||||||
|
var stderrRegex = regexp.MustCompile(`^error: (?<error>.+)\n`)
|
||||||
|
var stderrWithPathRegex = regexp.MustCompile(`^(?<path>.+):(?<line>\d+):(?<column>\d+): error: (?<error>.+)\n$`)
|
||||||
|
|
||||||
|
// ParseStderr will parse the given stderr output and return a suitable error object.
|
||||||
|
// Depending on the stderr message, this will return either a typst.Error or a typst.ErrorWithPath error.
|
||||||
|
func ParseStderr(stderr string, inner error) error {
|
||||||
|
if parsed := stderrWithPathRegex.FindStringSubmatch(stderr); parsed != nil {
|
||||||
|
err := ErrorWithPath{
|
||||||
|
Raw: stderr,
|
||||||
|
Inner: inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := stderrWithPathRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
|
||||||
|
err.Message = parsed[i]
|
||||||
|
}
|
||||||
|
if i := stderrWithPathRegex.SubexpIndex("path"); i > 0 && i < len(parsed) {
|
||||||
|
err.Path = parsed[i]
|
||||||
|
}
|
||||||
|
if i := stderrWithPathRegex.SubexpIndex("line"); i > 0 && i < len(parsed) {
|
||||||
|
line, _ := strconv.ParseInt(parsed[i], 10, 0)
|
||||||
|
err.Line = int(line)
|
||||||
|
}
|
||||||
|
if i := stderrWithPathRegex.SubexpIndex("column"); i > 0 && i < len(parsed) {
|
||||||
|
column, _ := strconv.ParseInt(parsed[i], 10, 0)
|
||||||
|
err.Column = int(column)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed := stderrRegex.FindStringSubmatch(stderr); parsed != nil {
|
||||||
|
err := Error{
|
||||||
|
Raw: stderr,
|
||||||
|
Inner: inner,
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := stderrRegex.SubexpIndex("error"); i > 0 && i < len(parsed) {
|
||||||
|
err.Message = parsed[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to the raw error message.
|
||||||
|
return &Error{
|
||||||
|
Raw: stderr,
|
||||||
|
Inner: inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
77
errors_test.go
Normal file
77
errors_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package typst_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Dadido3/go-typst"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrors0(t *testing.T) {
|
||||||
|
cli := typst.CLI{}
|
||||||
|
|
||||||
|
r := bytes.NewBufferString(`This is a test!`)
|
||||||
|
|
||||||
|
var w bytes.Buffer
|
||||||
|
if err := cli.Render(r, &w, nil); err != nil {
|
||||||
|
t.Fatalf("Failed to render document: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrors1(t *testing.T) {
|
||||||
|
cli := typst.CLI{}
|
||||||
|
|
||||||
|
r := bytes.NewBufferString(`This is a test!
|
||||||
|
|
||||||
|
#assert(1 < 1, message: "Test")`)
|
||||||
|
|
||||||
|
var w bytes.Buffer
|
||||||
|
if err := cli.Render(r, &w, nil); err == nil {
|
||||||
|
t.Fatalf("Expected error, but got nil")
|
||||||
|
} else {
|
||||||
|
var errWithPath *typst.ErrorWithPath
|
||||||
|
if errors.As(err, &errWithPath) {
|
||||||
|
if errWithPath.Message != "assertion failed: Test" {
|
||||||
|
t.Errorf("Expected error with error message %q, got %q", "assertion failed: Test", errWithPath.Message)
|
||||||
|
}
|
||||||
|
/*if errWithPath.Path != "" {
|
||||||
|
t.Errorf("Expected error to point to path %q, got path %q", "", errWithPath.Path)
|
||||||
|
}*/
|
||||||
|
if errWithPath.Line != 3 {
|
||||||
|
t.Errorf("Expected error to point at line %d, got line %d", 3, errWithPath.Line)
|
||||||
|
}
|
||||||
|
if errWithPath.Column != 1 {
|
||||||
|
t.Errorf("Expected error to point at column %d, got column %d", 1, errWithPath.Column)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected error type %T, got %T: %v", errWithPath, err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrors2(t *testing.T) {
|
||||||
|
cli := typst.CLI{}
|
||||||
|
|
||||||
|
opts := typst.CLIOptions{
|
||||||
|
Pages: "a",
|
||||||
|
}
|
||||||
|
|
||||||
|
r := bytes.NewBufferString(`This is a test!`)
|
||||||
|
|
||||||
|
var w bytes.Buffer
|
||||||
|
if err := cli.Render(r, &w, &opts); err == nil {
|
||||||
|
t.Fatalf("Expected error, but got nil")
|
||||||
|
} else {
|
||||||
|
var errTypst *typst.Error
|
||||||
|
if errors.As(err, &errTypst) {
|
||||||
|
// Don't check the specific error message, as that may change over time.
|
||||||
|
// The expected message should be similar to: invalid value 'a' for '--pages <PAGES>': not a valid page number.
|
||||||
|
if errTypst.Message == "" {
|
||||||
|
t.Errorf("Expected error message, got %q", errTypst.Message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected error type %T, got %T: %v", errTypst, err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
go.mod
7
go.mod
@ -2,4 +2,9 @@ module github.com/Dadido3/go-typst
|
|||||||
|
|
||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require github.com/google/go-cmp v0.6.0
|
require (
|
||||||
|
github.com/google/go-cmp v0.6.0
|
||||||
|
github.com/smasher164/xid v0.1.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/text v0.3.3 // indirect
|
||||||
|
5
go.sum
5
go.sum
@ -1,2 +1,7 @@
|
|||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/smasher164/xid v0.1.2 h1:erplXSdBRIIw+MrwjJ/m8sLN2XY16UGzpTA0E2Ru6HA=
|
||||||
|
github.com/smasher164/xid v0.1.2/go.mod h1:tgivm8CQl19fH1c5y+8F4mA+qY6n2i6qDRBlY/6nm+I=
|
||||||
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
75
identifier.go
Normal file
75
identifier.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package typst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"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.
|
||||||
|
// 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.
|
||||||
|
func CleanIdentifier(input string) string {
|
||||||
|
dst := make([]byte, 0, len(input))
|
||||||
|
|
||||||
|
for i, r := range input {
|
||||||
|
if i == 0 {
|
||||||
|
// Handle first rune of input.
|
||||||
|
switch {
|
||||||
|
case xid.Start(r), r == '_':
|
||||||
|
dst = utf8.AppendRune(dst, r)
|
||||||
|
default:
|
||||||
|
dst = append(dst, '_')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle all other runes of input.
|
||||||
|
switch {
|
||||||
|
case xid.Continue(r), r == '_', r == '-':
|
||||||
|
dst = utf8.AppendRune(dst, r)
|
||||||
|
default:
|
||||||
|
dst = append(dst, '_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow empty identifiers.
|
||||||
|
// We can't use a single placeholder ("_"), as it will cause errors when used in dictionaries.
|
||||||
|
result := string(dst)
|
||||||
|
if result == "_" || result == "" {
|
||||||
|
return "_invalid_"
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIdentifier will return whether input is a valid typst identifier.
|
||||||
|
//
|
||||||
|
// See https://github.com/typst/typst/blob/76c24ee6e35715cd14bb892d7b6b8d775c680bf7/crates/typst-syntax/src/lexer.rs#L932 for details.
|
||||||
|
func IsIdentifier(input string) bool {
|
||||||
|
// Identifiers can't be empty.
|
||||||
|
// We will also disallow a single underscore.
|
||||||
|
if input == "" || input == "_" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range input {
|
||||||
|
if i == 0 {
|
||||||
|
// Handle first rune of input.
|
||||||
|
switch {
|
||||||
|
case xid.Start(r), r == '_':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle all other runes of input.
|
||||||
|
switch {
|
||||||
|
case xid.Continue(r), r == '_', r == '-':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
49
identifier_test.go
Normal file
49
identifier_test.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
package typst
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCleanIdentifier(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"", "_invalid_"},
|
||||||
|
{"_", "_invalid_"},
|
||||||
|
{"_-", "_-"},
|
||||||
|
{"-foo-", "_foo-"},
|
||||||
|
{"foo", "foo"},
|
||||||
|
{"😊", "_invalid_"},
|
||||||
|
{"foo😊", "foo_"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
if got := CleanIdentifier(tt.input); got != tt.want {
|
||||||
|
t.Errorf("IsIdentifier() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsIdentifier(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"", false},
|
||||||
|
{"_", false},
|
||||||
|
{"_-", true},
|
||||||
|
{"-foo", false},
|
||||||
|
{"foo", true},
|
||||||
|
{"😊", false},
|
||||||
|
{"_😊", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.input, func(t *testing.T) {
|
||||||
|
if got := IsIdentifier(tt.input); got != tt.want {
|
||||||
|
t.Errorf("IsIdentifier() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -2,4 +2,5 @@
|
|||||||
|
|
||||||
package typst
|
package typst
|
||||||
|
|
||||||
|
// The path to the typst executable.
|
||||||
var ExecutablePath = "typst"
|
var ExecutablePath = "typst"
|
||||||
|
@ -4,5 +4,6 @@ package typst
|
|||||||
|
|
||||||
import "path/filepath"
|
import "path/filepath"
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
@ -8,9 +8,10 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"unicode/utf8"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// VariableMarshaler can be implemented by types to support custom typst marshaling.
|
||||||
type VariableMarshaler interface {
|
type VariableMarshaler interface {
|
||||||
MarshalTypstVariable() ([]byte, error)
|
MarshalTypstVariable() ([]byte, error)
|
||||||
}
|
}
|
||||||
@ -21,6 +22,7 @@ type VariableEncoder struct {
|
|||||||
writer io.Writer
|
writer io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewVariableEncoder returns a new encoder that writes into w.
|
||||||
func NewVariableEncoder(w io.Writer) *VariableEncoder {
|
func NewVariableEncoder(w io.Writer) *VariableEncoder {
|
||||||
return &VariableEncoder{
|
return &VariableEncoder{
|
||||||
writer: w,
|
writer: w,
|
||||||
@ -31,156 +33,193 @@ func (e *VariableEncoder) Encode(v any) error {
|
|||||||
return e.marshal(reflect.ValueOf(v))
|
return e.marshal(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) WriteString(s string) {
|
func (e *VariableEncoder) writeString(s string) error {
|
||||||
e.WriteBytes([]byte(s))
|
return e.writeBytes([]byte(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) WriteBytes(b []byte) {
|
func (e *VariableEncoder) writeStringLiteral(s []byte) error {
|
||||||
e.writer.Write(b)
|
dst := make([]byte, 0, len(s)+5)
|
||||||
}
|
|
||||||
|
|
||||||
func (e *VariableEncoder) WriteIndentationCharacters() {
|
|
||||||
e.WriteBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *VariableEncoder) marshal(v reflect.Value) error {
|
|
||||||
if !v.IsValid() {
|
|
||||||
e.WriteString("none")
|
|
||||||
return nil
|
|
||||||
//return fmt.Errorf("invalid reflect.Value %v", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := v.Type()
|
|
||||||
|
|
||||||
if (t.Kind() == reflect.Pointer || t.Kind() == reflect.Interface) && v.IsNil() {
|
|
||||||
e.WriteString("none")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
|
|
||||||
if m, ok := v.Interface().(VariableMarshaler); ok {
|
|
||||||
bytes, err := m.MarshalTypstVariable()
|
|
||||||
e.WriteBytes(bytes)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.WriteString("none")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Implements(reflect.TypeFor[encoding.TextMarshaler]()) {
|
|
||||||
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
|
|
||||||
bytes, err := m.MarshalText()
|
|
||||||
e.WriteBytes(bytes)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
e.WriteString("none")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle images
|
|
||||||
|
|
||||||
// TODO: Handle decimals
|
|
||||||
|
|
||||||
// TODO: Handle Time
|
|
||||||
|
|
||||||
// TODO: Handle durations
|
|
||||||
|
|
||||||
switch t.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
e.WriteString(strconv.FormatBool(v.Bool()))
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
e.WriteString(strconv.FormatInt(v.Int(), 10))
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
||||||
e.WriteString(strconv.FormatUint(v.Uint(), 10))
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
f := v.Float()
|
|
||||||
switch {
|
|
||||||
case math.IsNaN(f):
|
|
||||||
e.WriteString("float.nan")
|
|
||||||
case math.IsInf(f, 1):
|
|
||||||
e.WriteString("float.inf")
|
|
||||||
case math.IsInf(f, -1):
|
|
||||||
e.WriteString("-float.inf")
|
|
||||||
default:
|
|
||||||
e.WriteString(strconv.FormatFloat(f, 'e', -1, 64))
|
|
||||||
}
|
|
||||||
case reflect.String:
|
|
||||||
return e.encodeString(v)
|
|
||||||
case reflect.Interface, reflect.Pointer:
|
|
||||||
return e.marshal(v.Elem())
|
|
||||||
case reflect.Map:
|
|
||||||
return e.encodeMap(v)
|
|
||||||
case reflect.Struct:
|
|
||||||
return e.encodeStruct(v, t)
|
|
||||||
case reflect.Slice:
|
|
||||||
return e.encodeSlice(v)
|
|
||||||
case reflect.Array:
|
|
||||||
return e.encodeArray(v)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported type %q", t.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeString(v reflect.Value) error {
|
|
||||||
|
|
||||||
src := v.String()
|
|
||||||
|
|
||||||
dst := make([]byte, 0, len(src)+2)
|
|
||||||
|
|
||||||
dst = append(dst, '"')
|
dst = append(dst, '"')
|
||||||
|
|
||||||
for _, r := range src {
|
for _, r := range s {
|
||||||
switch r {
|
switch r {
|
||||||
case '\\', '"':
|
case '\\', '"':
|
||||||
dst = append(dst, '\\')
|
dst = append(dst, '\\', r)
|
||||||
dst = utf8.AppendRune(dst, r)
|
|
||||||
case '\n':
|
case '\n':
|
||||||
dst = append(dst, '\\', 'n')
|
dst = append(dst, '\\', 'n')
|
||||||
case '\r':
|
case '\r':
|
||||||
dst = append(dst, '\\', 'r')
|
dst = append(dst, '\\', 'r')
|
||||||
case '\t':
|
case '\t':
|
||||||
dst = append(dst, '\\', 't')
|
dst = append(dst, '\\', 't')
|
||||||
|
default:
|
||||||
|
dst = append(dst, r)
|
||||||
}
|
}
|
||||||
dst = utf8.AppendRune(dst, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dst = append(dst, '"')
|
dst = append(dst, '"')
|
||||||
|
|
||||||
e.WriteBytes(dst)
|
return e.writeBytes(dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
func (e *VariableEncoder) writeIndentationCharacters() error {
|
||||||
if v.NumField() == 0 {
|
return e.writeBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
||||||
e.WriteString("()")
|
}
|
||||||
|
|
||||||
|
func (e *VariableEncoder) marshal(v reflect.Value) error {
|
||||||
|
if !v.IsValid() {
|
||||||
|
return e.writeString("none")
|
||||||
|
//return fmt.Errorf("invalid reflect.Value %v", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
switch i := v.Interface().(type) {
|
||||||
|
case time.Time:
|
||||||
|
if err := e.encodeTime(i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *time.Time:
|
||||||
|
if i == nil {
|
||||||
|
e.writeString("none")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := e.encodeTime(*i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case time.Duration:
|
||||||
|
if err := e.encodeDuration(i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case *time.Duration:
|
||||||
|
if i == nil {
|
||||||
|
e.writeString("none")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := e.encodeDuration(*i); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteString("(\n")
|
// TODO: Handle images, maybe create a wrapper type that does this
|
||||||
|
|
||||||
|
if t.Implements(reflect.TypeFor[VariableMarshaler]()) {
|
||||||
|
if m, ok := v.Interface().(VariableMarshaler); ok {
|
||||||
|
bytes, err := m.MarshalTypstVariable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error calling MarshalTypstVariable for type %s: %w", t.String(), err)
|
||||||
|
}
|
||||||
|
return e.writeBytes(bytes)
|
||||||
|
}
|
||||||
|
return e.writeString("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Implements(reflect.TypeFor[encoding.TextMarshaler]()) {
|
||||||
|
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||||
|
b, err := m.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error calling MarshalText for type %s: %w", t.String(), err)
|
||||||
|
}
|
||||||
|
return e.writeStringLiteral(b)
|
||||||
|
}
|
||||||
|
return e.writeString("none")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
err = e.writeString(strconv.FormatBool(v.Bool()))
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
err = e.writeString(strconv.FormatInt(v.Int(), 10))
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
err = e.writeString(strconv.FormatUint(v.Uint(), 10))
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
f := v.Float()
|
||||||
|
switch {
|
||||||
|
case math.IsNaN(f):
|
||||||
|
err = e.writeString("float.nan")
|
||||||
|
case math.IsInf(f, 1):
|
||||||
|
err = e.writeString("float.inf")
|
||||||
|
case math.IsInf(f, -1):
|
||||||
|
err = e.writeString("-float.inf")
|
||||||
|
default:
|
||||||
|
err = e.writeString(strconv.FormatFloat(f, 'e', -1, 64))
|
||||||
|
}
|
||||||
|
case reflect.String:
|
||||||
|
return e.encodeString(v)
|
||||||
|
case reflect.Interface, reflect.Pointer:
|
||||||
|
if v.IsNil() {
|
||||||
|
return e.writeString("none")
|
||||||
|
}
|
||||||
|
return e.marshal(v.Elem())
|
||||||
|
case reflect.Map:
|
||||||
|
return e.encodeMap(v)
|
||||||
|
case reflect.Struct:
|
||||||
|
return e.encodeStruct(v, t)
|
||||||
|
case reflect.Slice:
|
||||||
|
return e.encodeSlice(v, t)
|
||||||
|
case reflect.Array:
|
||||||
|
return e.encodeArray(v)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported type %q", t.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *VariableEncoder) encodeString(v reflect.Value) error {
|
||||||
|
return e.writeStringLiteral([]byte(v.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||||
|
if v.NumField() == 0 {
|
||||||
|
return e.writeString("()")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.writeString("(\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
e.indentLevel++
|
e.indentLevel++
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
ft, fv := t.Field(i), v.Field(i)
|
ft, fv := t.Field(i), v.Field(i)
|
||||||
if ft.PkgPath == "" { // Ignore unexported fields.
|
if ft.PkgPath == "" { // Ignore unexported fields.
|
||||||
e.WriteIndentationCharacters()
|
if err := e.writeIndentationCharacters(); err != nil {
|
||||||
e.WriteString(ft.Name + ": ")
|
return err
|
||||||
if err := e.marshal(fv); err != nil {
|
}
|
||||||
return fmt.Errorf("failed to encode value of struct field %q", ft.Name)
|
// TODO: Allow name customization via struct tags
|
||||||
|
if err := e.writeString(CleanIdentifier(ft.Name) + ": "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := e.marshal(fv); err != nil {
|
||||||
|
return fmt.Errorf("failed to encode value of struct field %q: %w", ft.Name, err)
|
||||||
|
}
|
||||||
|
if err := e.writeString(",\n"); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
e.WriteString(",\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.indentLevel--
|
e.indentLevel--
|
||||||
|
|
||||||
e.WriteIndentationCharacters()
|
if err := e.writeIndentationCharacters(); err != nil {
|
||||||
e.WriteString(")")
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return e.writeString(")")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||||
@ -206,14 +245,17 @@ func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
|||||||
|
|
||||||
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
||||||
if v.Len() == 0 {
|
if v.Len() == 0 {
|
||||||
e.WriteString("()")
|
return e.writeString("()")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteString("(\n")
|
if err := e.writeString("(\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
e.indentLevel++
|
e.indentLevel++
|
||||||
|
|
||||||
|
// BUG: Map output needs to be sorted, otherwise this will cause the test to fail randomly
|
||||||
|
|
||||||
mi := v.MapRange()
|
mi := v.MapRange()
|
||||||
for mi.Next() {
|
for mi.Next() {
|
||||||
mk, mv := mi.Key(), mi.Value()
|
mk, mv := mi.Key(), mi.Value()
|
||||||
@ -222,57 +264,109 @@ func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteIndentationCharacters()
|
if err := e.writeIndentationCharacters(); err != nil {
|
||||||
e.WriteString(key + ": ")
|
return err
|
||||||
|
}
|
||||||
|
if err := e.writeString(CleanIdentifier(key) + ": "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := e.marshal(mv); err != nil {
|
if err := e.marshal(mv); err != nil {
|
||||||
return fmt.Errorf("failed to encode map field %q", key)
|
return fmt.Errorf("failed to encode map field %q: %w", key, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteString(",\n")
|
if err := e.writeString(",\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.indentLevel--
|
e.indentLevel--
|
||||||
|
|
||||||
e.WriteIndentationCharacters()
|
if err := e.writeIndentationCharacters(); err != nil {
|
||||||
e.WriteString(")")
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return e.writeString(")")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeSlice(v reflect.Value) error {
|
func (e *VariableEncoder) EncodeByteSlice(bb []byte) error {
|
||||||
e.WriteString("(")
|
if err := e.writeString("bytes(("); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Output byte slice as a base64 and use the typst based package to convert that into typst Bytes.
|
// 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 {
|
||||||
|
if err := e.writeString(", "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.writeString(strconv.FormatUint(uint64(b), 10)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.writeString("))")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error {
|
||||||
|
|
||||||
|
// Special case for byte slices.
|
||||||
|
if t.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return e.EncodeByteSlice(v.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.writeString("("); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
n := v.Len()
|
n := v.Len()
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
e.WriteString(", ")
|
if err := e.writeString(", "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := e.marshal(v.Index(i)); err != nil {
|
if err := e.marshal(v.Index(i)); err != nil {
|
||||||
return fmt.Errorf("failed to encode slice element %d of %d", i+1, n+1)
|
return fmt.Errorf("failed to encode slice element %d of %d: %w", i+1, n+1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteString(")")
|
return e.writeString(")")
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
||||||
e.WriteString("(")
|
if err := e.writeString("("); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
n := v.Len()
|
n := v.Len()
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
e.WriteString(", ")
|
if err := e.writeString(", "); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err := e.marshal(v.Index(i)); err != nil {
|
if err := e.marshal(v.Index(i)); err != nil {
|
||||||
return fmt.Errorf("failed to encode array element %d of %d", i+1, n+1)
|
return fmt.Errorf("failed to encode array element %d of %d: %w", i+1, n+1, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
e.WriteString(")")
|
return e.writeString(")")
|
||||||
|
}
|
||||||
return nil
|
|
||||||
|
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(),
|
||||||
|
t.Day(),
|
||||||
|
t.Hour(),
|
||||||
|
t.Minute(),
|
||||||
|
t.Second(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *VariableEncoder) encodeDuration(d time.Duration) error {
|
||||||
|
return e.writeString(fmt.Sprintf("duration(seconds: %d)", int(math.Round(d.Seconds()))))
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,52 @@ package typst
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type VariableMarshalerType []byte
|
||||||
|
|
||||||
|
func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type VariableMarshalerTypePointer []byte
|
||||||
|
|
||||||
|
var variableMarshalerTypePointer = VariableMarshalerTypePointer("test")
|
||||||
|
var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil)
|
||||||
|
|
||||||
|
func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) {
|
||||||
|
if v != nil {
|
||||||
|
return *v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no data")
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextMarshalerType []byte
|
||||||
|
|
||||||
|
func (v TextMarshalerType) MarshalText() ([]byte, error) {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextMarshalerTypePointer []byte
|
||||||
|
|
||||||
|
var textMarshalerTypePointer = TextMarshalerTypePointer("test")
|
||||||
|
var textMarshalerTypePointerNil = TextMarshalerTypePointer(nil)
|
||||||
|
|
||||||
|
func (v *TextMarshalerTypePointer) MarshalText() ([]byte, error) {
|
||||||
|
if v != nil {
|
||||||
|
return *v, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("no data")
|
||||||
|
}
|
||||||
|
|
||||||
func TestVariableEncoder(t *testing.T) {
|
func TestVariableEncoder(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -35,6 +75,7 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
{"float64 +inf", float64(math.Inf(1)), false, "float.inf"},
|
{"float64 +inf", float64(math.Inf(1)), false, "float.inf"},
|
||||||
{"float64 -inf", float64(math.Inf(-1)), false, "-float.inf"},
|
{"float64 -inf", float64(math.Inf(-1)), false, "-float.inf"},
|
||||||
{"string", "Hey!", false, `"Hey!"`},
|
{"string", "Hey!", false, `"Hey!"`},
|
||||||
|
{"string escaped", "Hey!😀 \"This is quoted\"\nNew line!", false, `"Hey!😀 \"This is quoted\"\nNew line!"`},
|
||||||
{"struct", struct {
|
{"struct", struct {
|
||||||
Foo string
|
Foo string
|
||||||
Bar int
|
Bar int
|
||||||
@ -49,6 +90,24 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
{"string slice empty", []string{}, false, `()`},
|
{"string slice empty", []string{}, false, `()`},
|
||||||
{"string slice nil", []string(nil), false, `()`},
|
{"string slice nil", []string(nil), false, `()`},
|
||||||
{"string slice pointer", &[]string{"Foo", "Bar"}, false, `("Foo", "Bar")`},
|
{"string slice pointer", &[]string{"Foo", "Bar"}, false, `("Foo", "Bar")`},
|
||||||
|
{"int slice", []int{1, 2, 3, 4, 5}, false, `(1, 2, 3, 4, 5)`},
|
||||||
|
{"byte slice", []byte{1, 2, 3, 4, 5}, false, `bytes((1, 2, 3, 4, 5))`},
|
||||||
|
{"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"`},
|
||||||
|
{"MarshalText pointer nil", &textMarshalerTypePointerNil, false, `""`},
|
||||||
|
{"MarshalText nil pointer", struct{ A *TextMarshalerTypePointer }{nil}, true, ``},
|
||||||
|
{"time.Time", time.Date(2024, 12, 14, 12, 34, 56, 0, time.UTC), false, `datetime(year: 2024, month: 12, day: 14, hour: 12, minute: 34, second: 56)`},
|
||||||
|
{"time.Time pointer", &[]time.Time{time.Date(2024, 12, 14, 12, 34, 56, 0, time.UTC)}[0], false, `datetime(year: 2024, month: 12, day: 14, hour: 12, minute: 34, second: 56)`},
|
||||||
|
{"time.Time pointer nil", (*time.Time)(nil), false, `none`},
|
||||||
|
{"time.Duration", 60 * time.Second, false, `duration(seconds: 60)`},
|
||||||
|
{"time.Duration pointer", &[]time.Duration{60 * time.Second}[0], false, `duration(seconds: 60)`},
|
||||||
|
{"time.Duration pointer nil", (*time.Duration)(nil), false, `none`},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -64,8 +123,8 @@ func TestVariableEncoder(t *testing.T) {
|
|||||||
t.Fatalf("Expected error, but got none")
|
t.Fatalf("Expected error, but got none")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cmp.Equal(result.String(), tt.want) {
|
if !tt.wantErr && !cmp.Equal(result.String(), tt.want) {
|
||||||
t.Errorf("Got unexpected result: %s", cmp.Diff(result.String(), tt.want))
|
t.Errorf("Got the following diff in output: %s", cmp.Diff(tt.want, result.String()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user