mirror of
https://github.com/Dadido3/go-typst.git
synced 2025-04-04 09:13:17 +00:00
Initial commit
This commit is contained in:
commit
c730d437ef
76
.gitignore
vendored
Normal file
76
.gitignore
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,go
|
||||
|
||||
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
|
||||
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"Dadido",
|
||||
"Foogaloo",
|
||||
"typst"
|
||||
]
|
||||
}
|
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# go-typst
|
||||
|
||||
A library to generate documents via [typst].
|
||||
|
||||
## Installation
|
||||
|
||||
## Runtime requirements
|
||||
|
||||
## Usage
|
||||
|
||||
[typst]: https://typst.app/
|
37
cli.go
Normal file
37
cli.go
Normal file
@ -0,0 +1,37 @@
|
||||
package typst
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
//ExecutablePath string
|
||||
}
|
||||
|
||||
func (c CLI) Render(input io.Reader, output io.Writer) error {
|
||||
cmd := exec.Command(ExecutablePath, "c", "-", "-")
|
||||
cmd.Stdin = input
|
||||
cmd.Stdout = output
|
||||
|
||||
errBuffer := bytes.Buffer{}
|
||||
cmd.Stderr = &errBuffer
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
switch err := err.(type) {
|
||||
case *exec.ExitError:
|
||||
return NewError(errBuffer.String(), err)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c CLI) RenderWithVariables(input io.Reader, output io.Writer, variables map[string]any) error {
|
||||
reader := io.MultiReader(nil, input)
|
||||
|
||||
return c.Render(reader, output)
|
||||
}
|
58
errors.go
Normal file
58
errors.go
Normal file
@ -0,0 +1,58 @@
|
||||
package typst
|
||||
|
||||
import (
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var stderrRegex = regexp.MustCompile(`^error: (?<error>.+)\n ┌─ (?<path>.+):(?<line>\d+):(?<column>\d+)\n`)
|
||||
|
||||
// Error represents a typst error.
|
||||
type Error 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.
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
log.Printf("%#v", err)
|
||||
|
||||
return &err
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Raw
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Inner
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
||||
module github.com/Dadido3/go-typst
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require github.com/google/go-cmp v0.6.0
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
5
typst_unix.go
Normal file
5
typst_unix.go
Normal file
@ -0,0 +1,5 @@
|
||||
//go:build unix
|
||||
|
||||
package typst
|
||||
|
||||
var ExecutablePath = "typst"
|
8
typst_windows.go
Normal file
8
typst_windows.go
Normal file
@ -0,0 +1,8 @@
|
||||
//go:build windows
|
||||
|
||||
package typst
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// We assume the executable is in the current working directory.
|
||||
var ExecutablePath = "." + string(filepath.Separator) + filepath.Join("typst.exe")
|
255
variable-encoder.go
Normal file
255
variable-encoder.go
Normal file
@ -0,0 +1,255 @@
|
||||
package typst
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type VariableMarshaler interface {
|
||||
MarshalTypstVariable() ([]byte, error)
|
||||
}
|
||||
|
||||
type VariableEncoder struct {
|
||||
indentLevel int
|
||||
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
func NewVariableEncoder(w io.Writer) *VariableEncoder {
|
||||
return &VariableEncoder{
|
||||
writer: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) Encode(v any) error {
|
||||
return e.marshal(reflect.ValueOf(v))
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) WriteString(s string) {
|
||||
e.WriteBytes([]byte(s))
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) WriteBytes(b []byte) {
|
||||
e.writer.Write(b)
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) WriteIndentationCharacters() {
|
||||
e.WriteBytes(slices.Repeat([]byte{' ', ' '}, e.indentLevel))
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) marshal(v reflect.Value) error {
|
||||
if !v.IsValid() {
|
||||
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:
|
||||
e.WriteString(strconv.FormatFloat(v.Float(), 'e', -1, 32))
|
||||
case reflect.Float64:
|
||||
e.WriteString(strconv.FormatFloat(v.Float(), 'e', -1, 64))
|
||||
case reflect.String:
|
||||
return e.encodeString(v)
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
return e.marshal(v.Elem())
|
||||
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, '"')
|
||||
|
||||
for _, r := range src {
|
||||
switch r {
|
||||
case '\\', '"':
|
||||
dst = append(dst, '\\')
|
||||
dst = utf8.AppendRune(dst, r)
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
}
|
||||
dst = utf8.AppendRune(dst, r)
|
||||
}
|
||||
|
||||
dst = append(dst, '"')
|
||||
|
||||
e.WriteBytes(dst)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error {
|
||||
e.WriteString("(\n")
|
||||
|
||||
e.indentLevel++
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
ft, fv := t.Field(i), v.Field(i)
|
||||
if ft.PkgPath == "" { // Ignore unexported fields.
|
||||
e.WriteIndentationCharacters()
|
||||
e.WriteString(ft.Name + ": ")
|
||||
if err := e.marshal(fv); err != nil {
|
||||
return fmt.Errorf("failed to encode value of struct field %q", ft.Name)
|
||||
}
|
||||
e.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
e.indentLevel--
|
||||
|
||||
e.WriteIndentationCharacters()
|
||||
e.WriteString(")")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) {
|
||||
// From encoding/json/encode.go.
|
||||
if v.Kind() == reflect.String {
|
||||
return v.String(), nil
|
||||
}
|
||||
if tm, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||
if v.Kind() == reflect.Pointer && v.IsNil() {
|
||||
return "", nil
|
||||
}
|
||||
buf, err := tm.MarshalText()
|
||||
return string(buf), err
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(v.Int(), 10), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return strconv.FormatUint(v.Uint(), 10), nil
|
||||
}
|
||||
return "", fmt.Errorf("unsupported map key type %q", v.Type().String())
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) encodeMap(v reflect.Value) error {
|
||||
e.WriteString("(\n")
|
||||
|
||||
e.indentLevel++
|
||||
|
||||
mi := v.MapRange()
|
||||
for mi.Next() {
|
||||
mk, mv := mi.Key(), mi.Value()
|
||||
key, err := e.resolveKeyName(mk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.WriteIndentationCharacters()
|
||||
e.WriteString(key + ": ")
|
||||
if err := e.marshal(mv); err != nil {
|
||||
return fmt.Errorf("failed to encode map field %q", key)
|
||||
}
|
||||
|
||||
e.WriteString(",\n")
|
||||
}
|
||||
|
||||
e.indentLevel--
|
||||
|
||||
e.WriteIndentationCharacters()
|
||||
e.WriteString(")")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) encodeSlice(v reflect.Value) error {
|
||||
e.WriteString("(")
|
||||
|
||||
// TODO: Output byte slice as a base64 and use the typst based package to convert that into typst Bytes.
|
||||
|
||||
n := v.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
if i > 0 {
|
||||
e.WriteString(", ")
|
||||
}
|
||||
if err := e.marshal(v.Index(i)); err != nil {
|
||||
return fmt.Errorf("failed to encode slice element %d of %d", i+1, n+1)
|
||||
}
|
||||
}
|
||||
|
||||
e.WriteString(")")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *VariableEncoder) encodeArray(v reflect.Value) error {
|
||||
e.WriteString("(")
|
||||
|
||||
n := v.Len()
|
||||
for i := 0; i < n; i++ {
|
||||
if i > 0 {
|
||||
e.WriteString(", ")
|
||||
}
|
||||
if err := e.marshal(v.Index(i)); err != nil {
|
||||
return fmt.Errorf("failed to encode array element %d of %d", i+1, n+1)
|
||||
}
|
||||
}
|
||||
|
||||
e.WriteString(")")
|
||||
|
||||
return nil
|
||||
}
|
67
variable-encoder_test.go
Normal file
67
variable-encoder_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
package typst
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestVariableEncoder(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params any
|
||||
wantErr bool
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, false, "none"},
|
||||
{"bool false", false, false, "false"},
|
||||
{"bool true", true, false, "true"},
|
||||
{"int", int(-123), false, "-123"},
|
||||
{"int8", int8(-123), false, "-123"},
|
||||
{"int16", int16(-123), false, "-123"},
|
||||
{"int32", int32(-123), false, "-123"},
|
||||
{"int64", int64(-123), false, "-123"},
|
||||
{"uint", uint(123), false, "123"},
|
||||
{"uint8", uint8(123), false, "123"},
|
||||
{"uint16", uint16(123), false, "123"},
|
||||
{"uint32", uint32(123), false, "123"},
|
||||
{"uint64", uint64(123), false, "123"},
|
||||
{"float32", float32(1), false, "1e+00"},
|
||||
{"float64", float64(1), false, "1e+00"},
|
||||
{"float64 nan", float64(math.NaN()), false, "float.nan"},
|
||||
{"float64 +inf", float64(math.Inf(1)), false, "float.inf"},
|
||||
{"float64 -inf", float64(math.Inf(-1)), false, "-float.inf"},
|
||||
{"string", "Hey!", false, `"Hey!"`},
|
||||
{"struct", struct {
|
||||
Foo string
|
||||
Bar int
|
||||
}{"Hey!", 12345}, false, "(\n Foo: \"Hey!\",\n Bar: 12345,\n)"},
|
||||
{"map string string", map[string]string{"Foo": "Bar", "Foo2": "Electric Foogaloo"}, false, "(\n Foo: \"Bar\",\n Foo2: \"Electric Foogaloo\",\n)"},
|
||||
{"map string string nil", map[string]string(nil), false, "()"},
|
||||
{"string array", [5]string{"Foo", "Bar"}, false, `("Foo", "Bar", "", "", "")`},
|
||||
{"string slice", []string{"Foo", "Bar"}, false, `("Foo", "Bar")`},
|
||||
{"string slice pointer", &[]string{"Foo", "Bar"}, false, `("Foo", "Bar")`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
result := bytes.Buffer{}
|
||||
vEnc := NewVariableEncoder(&result)
|
||||
|
||||
err := vEnc.Encode(tt.params)
|
||||
switch {
|
||||
case err != nil && !tt.wantErr:
|
||||
t.Fatalf("Failed to encode typst variables: %v", err)
|
||||
case err == nil && tt.wantErr:
|
||||
t.Fatalf("Expected error, but got none")
|
||||
}
|
||||
|
||||
if !cmp.Equal(result.String(), tt.want) {
|
||||
t.Errorf("Got unexpected result: %s", cmp.Diff(result.String(), tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user