diff --git a/README.md b/README.md index 7d13c1b..93476f0 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@ The supported and tested versions right now are: ## Features -- PDF, SVG or PNG generation. -- All typst-cli parameters are available as a struct, which makes it easy to discover all available options. +- PDF, SVG and PNG generation. +- All typst-cli parameters are [available as a struct](cli-options.go), which makes it easy to discover all available options. - Encoder to convert go values into typst markup which can be injected into typst documents. - Any stderr will be returned as go error value, including line number, column and file path of the error. -- Uses stdio; No temporary files need to be created. +- Uses stdio; No temporary files will be created. - Good unit test coverage. ## Installation diff --git a/variable-encoder.go b/variable-encoder.go index 9b5f2fc..c08f1e9 100644 --- a/variable-encoder.go +++ b/variable-encoder.go @@ -16,6 +16,8 @@ import ( "time" ) +// TODO: Add simple marshal function that returns a byte slice + // VariableMarshaler can be implemented by types to support custom typst marshaling. type VariableMarshaler interface { MarshalTypstVariable() ([]byte, error) @@ -42,6 +44,10 @@ func (e *VariableEncoder) writeString(s string) error { return e.writeBytes([]byte(s)) } +func (e *VariableEncoder) writeRune(r rune) error { + return e.writeBytes([]byte{byte(r)}) +} + func (e *VariableEncoder) writeStringLiteral(s []byte) error { dst := make([]byte, 0, len(s)+5) @@ -147,7 +153,19 @@ func (e *VariableEncoder) marshal(v reflect.Value) error { 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)) + if v.Int() >= 0 { + err = e.writeString(strconv.FormatInt(v.Int(), 10)) + } else { + if err = e.writeRune('{'); err != nil { + break + } + if err = e.writeString(strconv.FormatInt(v.Int(), 10)); err != nil { + break + } + if err = e.writeRune('}'); err != nil { + break + } + } 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: @@ -158,7 +176,17 @@ func (e *VariableEncoder) marshal(v reflect.Value) error { case math.IsInf(f, 1): err = e.writeString("float.inf") case math.IsInf(f, -1): - err = e.writeString("-float.inf") + err = e.writeString("{-float.inf}") + case math.Signbit(f): + if err = e.writeRune('{'); err != nil { + break + } + if err = e.writeString(strconv.FormatFloat(f, 'e', -1, 64)); err != nil { + break + } + if err = e.writeRune('}'); err != nil { + break + } default: err = e.writeString(strconv.FormatFloat(f, 'e', -1, 64)) } @@ -224,7 +252,7 @@ func (e *VariableEncoder) encodeStruct(v reflect.Value, t reflect.Type) error { return err } - return e.writeString(")") + return e.writeRune(')') } func (e *VariableEncoder) resolveKeyName(v reflect.Value) (string, error) { @@ -290,7 +318,7 @@ func (e *VariableEncoder) encodeMap(v reflect.Value) error { return err } - return e.writeString(")") + return e.writeRune(')') } func (e *VariableEncoder) EncodeByteSlice(bb []byte) error { @@ -312,6 +340,12 @@ func (e *VariableEncoder) EncodeByteSlice(bb []byte) error { } } + if len(bb) == 1 { + if err := e.writeRune(','); err != nil { + return err + } + } + return e.writeString("))") } @@ -322,7 +356,7 @@ func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error { return e.EncodeByteSlice(v.Bytes()) } - if err := e.writeString("("); err != nil { + if err := e.writeRune('('); err != nil { return err } @@ -338,11 +372,17 @@ func (e *VariableEncoder) encodeSlice(v reflect.Value, t reflect.Type) error { } } - return e.writeString(")") + if n == 1 { + if err := e.writeRune(','); err != nil { + return err + } + } + + return e.writeRune(')') } func (e *VariableEncoder) encodeArray(v reflect.Value) error { - if err := e.writeString("("); err != nil { + if err := e.writeRune('('); err != nil { return err } @@ -358,7 +398,13 @@ func (e *VariableEncoder) encodeArray(v reflect.Value) error { } } - return e.writeString(")") + if n == 1 { + if err := e.writeRune(','); err != nil { + return err + } + } + + return e.writeRune(')') } func (e *VariableEncoder) encodeTime(t time.Time) error { diff --git a/variable-encoder_test.go b/variable-encoder_test.go index b5b59e6..14da265 100644 --- a/variable-encoder_test.go +++ b/variable-encoder_test.go @@ -3,22 +3,27 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT -package typst +package typst_test import ( "bytes" "fmt" "math" + "strings" "testing" "time" + "github.com/Dadido3/go-typst" "github.com/google/go-cmp/cmp" ) type VariableMarshalerType []byte func (v VariableMarshalerType) MarshalTypstVariable() ([]byte, error) { - return v, nil + result := append([]byte{'"'}, v...) + result = append(result, '"') + + return result, nil } type VariableMarshalerTypePointer []byte @@ -28,7 +33,10 @@ var variableMarshalerTypePointerNil = VariableMarshalerTypePointer(nil) func (v *VariableMarshalerTypePointer) MarshalTypstVariable() ([]byte, error) { if v != nil { - return *v, nil + result := append([]byte{'"'}, *v...) + result = append(result, '"') + + return result, nil } return nil, fmt.Errorf("no data") @@ -64,11 +72,16 @@ func TestVariableEncoder(t *testing.T) { {"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"}, + {"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"}, + {"int negative", int(-123), false, "{-123}"}, + {"int8 negative", int8(-123), false, "{-123}"}, + {"int16 negative", int16(-123), false, "{-123}"}, + {"int32 negative", int32(-123), false, "{-123}"}, + {"int64 negative", int64(-123), false, "{-123}"}, {"uint", uint(123), false, "123"}, {"uint8", uint8(123), false, "123"}, {"uint16", uint16(123), false, "123"}, @@ -76,11 +89,13 @@ func TestVariableEncoder(t *testing.T) { {"uint64", uint64(123), false, "123"}, {"float32", float32(1), false, "1e+00"}, {"float64", float64(1), false, "1e+00"}, + {"float32 negative", float32(-1), false, "{-1e+00}"}, + {"float64 negative", 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"}, + {"float64 -inf", float64(math.Inf(-1)), false, "{-float.inf}"}, {"string", "Hey!", false, `"Hey!"`}, - {"string escaped", "Hey!😀 \"This is quoted\"\nNew line!", false, `"Hey!😀 \"This is quoted\"\nNew line!"`}, + {"string escaped", "Hey!😀 \"This is quoted\"\nNew line!\tAnd a tab", false, `"Hey!😀 \"This is quoted\"\nNew line!\tAnd a tab"`}, {"struct", struct { Foo string Bar int @@ -91,16 +106,20 @@ func TestVariableEncoder(t *testing.T) { {"map string string empty", map[string]string{}, false, "()"}, {"map string string nil", map[string]string(nil), false, "()"}, {"string array", [5]string{"Foo", "Bar"}, false, `("Foo", "Bar", "", "", "")`}, + {"string array 1", [1]string{"Foo"}, false, `("Foo",)`}, {"string slice", []string{"Foo", "Bar"}, false, `("Foo", "Bar")`}, + {"string slice 1", []string{"Foo"}, false, `("Foo",)`}, {"string slice empty", []string{}, false, `()`}, {"string slice nil", []string(nil), false, `()`}, {"string slice pointer", &[]string{"Foo", "Bar"}, false, `("Foo", "Bar")`}, {"int slice", []int{1, 2, 3, 4, 5}, false, `(1, 2, 3, 4, 5)`}, + {"int slice negative", []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, ""}, + {"byte slice 1", []byte{1}, false, `bytes((1,))`}, + {"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, `""`}, @@ -113,12 +132,14 @@ func TestVariableEncoder(t *testing.T) { {"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`}, + {"time.Duration negative", -60 * time.Second, false, `duration(seconds: -60)`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + t.Parallel() - result := bytes.Buffer{} - vEnc := NewVariableEncoder(&result) + var result bytes.Buffer + vEnc := typst.NewVariableEncoder(&result) err := vEnc.Encode(tt.params) switch { @@ -131,6 +152,16 @@ func TestVariableEncoder(t *testing.T) { if !tt.wantErr && !cmp.Equal(result.String(), tt.want) { t.Errorf("Got the following diff in output: %s", cmp.Diff(tt.want, result.String())) } + + // Compile to test parsing. + if !tt.wantErr { + typstCLI := typst.CLI{} + input := strings.NewReader("#" + result.String()) + var output bytes.Buffer + if err := typstCLI.Render(input, &output, nil); err != nil { + t.Errorf("Compilation failed: %v", err) + } + } }) } }