Index: LICENSE.txt ================================================================== --- LICENSE.txt +++ LICENSE.txt @@ -1,6 +1,6 @@ -Copyright (c) 2021-2022 Detlef Stern +Copyright (c) 2021-present Detlef Stern Licensed under the EUPL Zettelstore client is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the Index: api/api.go ================================================================== --- api/api.go +++ api/api.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights @@ -93,10 +93,16 @@ Meta ZettelMeta `json:"meta"` Encoding string `json:"encoding"` Content string `json:"content"` Rights ZettelRights `json:"rights"` } + +// ZettelContentJSON contains all elements to transfer the content of a zettel. +type ZettelContentJSON struct { + Encoding string `json:"encoding"` + Content string `json:"content"` +} // ZettelListJSON contains data for a zettel list. type ZettelListJSON struct { Query string `json:"query"` Human string `json:"human"` Index: api/const.go ================================================================== --- api/const.go +++ api/const.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights @@ -98,10 +98,11 @@ KeyLicense = "license" KeyModified = "modified" KeyPrecursor = "precursor" KeyPredecessor = "predecessor" KeyPublished = "published" + KeyQuery = "query" KeyReadOnly = "read-only" KeySuccessors = "successors" KeySummary = "summary" KeyURL = "url" KeyUselessFiles = "useless-files" @@ -149,18 +150,20 @@ HeaderLocation = "Location" ) // Values for HTTP query parameter. const ( - QueryKeyCommand = "cmd" - QueryKeyCost = "cost" - QueryKeyDir = "dir" - QueryKeyEncoding = "enc" - QueryKeyLimit = "limit" - QueryKeyPart = "part" - QueryKeyPhrase = "phrase" - QueryKeyQuery = "q" + QueryKeyCommand = "cmd" + QueryKeyCost = "cost" + QueryKeyDir = "dir" + QueryKeyEncoding = "enc" + QueryKeyParseOnly = "parseonly" + QueryKeyLimit = "limit" + QueryKeyPart = "part" + QueryKeyPhrase = "phrase" + QueryKeyQuery = "q" + QueryKeySeed = "_seed" ) // Supported dir values. const ( DirBackward = "backward" @@ -170,22 +173,28 @@ // Supported encoding values. const ( EncodingHTML = "html" EncodingMD = "md" EncodingSexpr = "sexpr" + EncodingSHTML = "shtml" EncodingText = "text" - EncodingZJSON = "zjson" EncodingZMK = "zmk" + + EncodingPlain = "plain" + EncodingJson = "json" ) var mapEncodingEnum = map[string]EncodingEnum{ EncodingHTML: EncoderHTML, EncodingMD: EncoderMD, EncodingSexpr: EncoderSexpr, + EncodingSHTML: EncoderSHTML, EncodingText: EncoderText, - EncodingZJSON: EncoderZJSON, EncodingZMK: EncoderZmk, + + EncodingPlain: EncoderPlain, + EncodingJson: EncoderJson, } var mapEnumEncoding = map[EncodingEnum]string{} func init() { for k, v := range mapEncodingEnum { @@ -208,13 +217,16 @@ const ( EncoderUnknown EncodingEnum = iota EncoderHTML EncoderMD EncoderSexpr + EncoderSHTML EncoderText - EncoderZJSON EncoderZmk + + EncoderPlain + EncoderJson ) // String representation of an encoder key. func (e EncodingEnum) String() string { if f, ok := mapEnumEncoding[e]; ok { Index: api/urlbuilder.go ================================================================== --- api/urlbuilder.go +++ api/urlbuilder.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights @@ -111,10 +111,19 @@ return ub } // String produces a string value. func (ub *URLBuilder) String() string { + return ub.asString("&") +} + +// AttrString returns the string value of the URL suitable to be placed in a HTML attribute. +func (ub *URLBuilder) AttrString() string { + return ub.asString("&") +} + +func (ub *URLBuilder) asString(qsep string) string { var sb strings.Builder sb.WriteString(ub.prefix) if ub.key != '/' { sb.WriteByte(ub.key) @@ -135,15 +144,15 @@ } for i, q := range ub.query { if i == 0 { sb.WriteByte('?') } else { - sb.WriteByte('&') + sb.WriteString(qsep) } sb.WriteString(q.key) if val := q.val; val != "" { sb.WriteByte('=') sb.WriteString(url.QueryEscape(val)) } } return sb.String() } Index: attrs/attrs.go ================================================================== --- attrs/attrs.go +++ attrs/attrs.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights Index: attrs/attrs_test.go ================================================================== --- attrs/attrs_test.go +++ attrs/attrs_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-2022 Detlef Stern +// Copyright (c) 2020-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights Index: client/client.go ================================================================== --- client/client.go +++ client/client.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-2022 Detlef Stern +// Copyright (c) 2021-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights @@ -23,13 +23,12 @@ "strconv" "strings" "time" "codeberg.org/t73fde/sxpf" + "codeberg.org/t73fde/sxpf/reader" "zettelstore.de/c/api" - "zettelstore.de/c/sexpr" - "zettelstore.de/c/zjson" ) // Client contains all data to execute requests. type Client struct { base string @@ -234,11 +233,11 @@ func (c *Client) CreateZettelJSON(ctx context.Context, data *api.ZettelDataJSON) (api.ZettelID, error) { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return api.InvalidZID, err } - ub := c.newURLBuilder('j') + ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPost, ub, &buf, nil) if err != nil { return api.InvalidZID, err } defer resp.Body.Close() @@ -271,11 +270,14 @@ resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + default: return nil, statusToError(resp) } data, err := io.ReadAll(resp.Body) if err != nil { return nil, err @@ -287,11 +289,11 @@ return lines, nil } // ListZettelJSON returns a list of zettel. func (c *Client) ListZettelJSON(ctx context.Context, query string) (string, string, []api.ZidMetaJSON, error) { - ub := c.newURLBuilder('j').AppendQuery(query) + ub := c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson).AppendQuery(query) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return "", "", nil, err } defer resp.Body.Close() @@ -316,19 +318,24 @@ resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + default: return nil, statusToError(resp) } return io.ReadAll(resp.Body) } // GetZettelJSON returns a zettel as a JSON struct. func (c *Client) GetZettelJSON(ctx context.Context, zid api.ZettelID) (*api.ZettelDataJSON, error) { - ub := c.newURLBuilder('j').SetZid(zid) + ub := c.newURLBuilder('z').SetZid(zid) + ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) + ub.AppendKVQuery(api.QueryKeyPart, api.PartZettel) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() @@ -344,91 +351,74 @@ return &out, nil } // GetParsedZettel return a parsed zettel in a defined encoding. func (c *Client) GetParsedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { - return c.getZettelString(ctx, 'p', zid, enc) + return c.getZettelString(ctx, zid, enc, true) } // GetEvaluatedZettel return an evaluated zettel in a defined encoding. func (c *Client) GetEvaluatedZettel(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { - return c.getZettelString(ctx, 'v', zid, enc) + return c.getZettelString(ctx, zid, enc, false) } -func (c *Client) getZettelString(ctx context.Context, key byte, zid api.ZettelID, enc api.EncodingEnum) ([]byte, error) { - ub := c.newURLBuilder(key).SetZid(zid) +func (c *Client) getZettelString(ctx context.Context, zid api.ZettelID, enc api.EncodingEnum, parseOnly bool) ([]byte, error) { + ub := c.newURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, enc.String()) ub.AppendKVQuery(api.QueryKeyPart, api.PartContent) + if parseOnly { + ub.AppendKVQuery(api.QueryKeyParseOnly, "") + } resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusOK: + case http.StatusNoContent: + default: return nil, statusToError(resp) } return io.ReadAll(resp.Body) } -// GetParsedZettelZJSON returns an parsed zettel as a JSON-decoded data structure. -func (c *Client) GetParsedSexpr(ctx context.Context, zid api.ZettelID, part string) (sxpf.Value, error) { - return c.getSexpr(ctx, 'p', zid, part) +// GetParsedSexpr returns an parsed zettel as a Sexpr-decoded data structure. +func (c *Client) GetParsedSexpr(ctx context.Context, zid api.ZettelID, part string, sf sxpf.SymbolFactory) (sxpf.Object, error) { + return c.getSexpr(ctx, zid, part, true, sf) } -// GetEvaluatedZettelZJSON returns an evaluated zettel as a JSON-decoded data structure. -func (c *Client) GetEvaluatedSexpr(ctx context.Context, zid api.ZettelID, part string) (sxpf.Value, error) { - return c.getSexpr(ctx, 'v', zid, part) +// GetEvaluatedSexpr returns an evaluated zettel as a Sexpr-decoded data structure. +func (c *Client) GetEvaluatedSexpr(ctx context.Context, zid api.ZettelID, part string, sf sxpf.SymbolFactory) (sxpf.Object, error) { + return c.getSexpr(ctx, zid, part, false, sf) } -func (c *Client) getSexpr(ctx context.Context, key byte, zid api.ZettelID, part string) (sxpf.Value, error) { - ub := c.newURLBuilder(key).SetZid(zid) +func (c *Client) getSexpr(ctx context.Context, zid api.ZettelID, part string, parseOnly bool, sf sxpf.SymbolFactory) (sxpf.Object, error) { + ub := c.newURLBuilder('z').SetZid(zid) ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingSexpr) if part != "" { ub.AppendKVQuery(api.QueryKeyPart, part) } + if parseOnly { + ub.AppendKVQuery(api.QueryKeyParseOnly, "") + } resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, statusToError(resp) } - - return sxpf.ParseValue(sexpr.Smk, bufio.NewReaderSize(resp.Body, 8)) -} - -// GetParsedZettelZJSON returns an parsed zettel as a JSON-decoded data structure. -func (c *Client) GetParsedZJSON(ctx context.Context, zid api.ZettelID, part string) (zjson.Value, error) { - return c.getZJSON(ctx, 'p', zid, part) -} - -// GetEvaluatedZettelZJSON returns an evaluated zettel as a JSON-decoded data structure. -func (c *Client) GetEvaluatedZJSON(ctx context.Context, zid api.ZettelID, part string) (zjson.Value, error) { - return c.getZJSON(ctx, 'v', zid, part) -} - -func (c *Client) getZJSON(ctx context.Context, key byte, zid api.ZettelID, part string) (zjson.Value, error) { - ub := c.newURLBuilder(key).SetZid(zid) - ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingZJSON) - if part != "" { - ub.AppendKVQuery(api.QueryKeyPart, part) - } - resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return nil, statusToError(resp) - } - return zjson.Decode(resp.Body) + return reader.MakeReader(bufio.NewReaderSize(resp.Body, 8), reader.WithSymbolFactory(sf)).Read() } // GetMeta returns the metadata of a zettel. func (c *Client) GetMeta(ctx context.Context, zid api.ZettelID) (api.ZettelMeta, error) { - ub := c.newURLBuilder('m').SetZid(zid) + ub := c.newURLBuilder('z').SetZid(zid) + ub.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) + ub.AppendKVQuery(api.QueryKeyPart, api.PartMeta) resp, err := c.buildAndExecuteRequest(ctx, http.MethodGet, ub, nil, nil) if err != nil { return nil, err } defer resp.Body.Close() @@ -551,11 +541,11 @@ func (c *Client) UpdateZettelJSON(ctx context.Context, zid api.ZettelID, data *api.ZettelDataJSON) error { var buf bytes.Buffer if err := encodeZettelData(&buf, data); err != nil { return err } - ub := c.newURLBuilder('j').SetZid(zid) + ub := c.newURLBuilder('z').SetZid(zid).AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) resp, err := c.buildAndExecuteRequest(ctx, http.MethodPut, ub, &buf, nil) if err != nil { return err } defer resp.Body.Close() @@ -628,11 +618,11 @@ func (c *Client) QueryMapMeta(ctx context.Context, query string) (api.MapMeta, error) { err := c.updateToken(ctx) if err != nil { return nil, err } - req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('q').AppendQuery(query), nil) + req, err := c.newRequest(ctx, http.MethodGet, c.newURLBuilder('z').AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson).AppendQuery(query), nil) if err != nil { return nil, err } resp, err := c.executeRequest(req) if err != nil { Index: client/client_test.go ================================================================== --- client/client_test.go +++ client/client_test.go @@ -1,7 +1,7 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2022 Detlef Stern +// Copyright (c) 2022-present Detlef Stern // // This file is part of zettelstore-client. // // Zettelstore client is licensed under the latest version of the EUPL // (European Union Public License). Please see file LICENSE.txt for your rights @@ -11,19 +11,18 @@ package client_test import ( "context" "flag" - "log" "net/http" "net/url" "testing" "codeberg.org/t73fde/sxpf" "zettelstore.de/c/api" "zettelstore.de/c/client" - "zettelstore.de/c/zjson" + "zettelstore.de/c/sexpr" ) func TestZettelList(t *testing.T) { c := getClient() _, err := c.ListZettel(context.Background(), "") @@ -44,76 +43,24 @@ } return } } -func TestGetZJSONZettel(t *testing.T) { - c := getClient() - data, err := c.GetEvaluatedZJSON(context.Background(), api.ZidDefaultHome, api.PartContent) - if err != nil { - t.Error(err) - return - } - if data == nil { - t.Error("No data") - } - var v vis - zjson.WalkBlock(&v, data.(zjson.Array), -1) - // t.Error("Argh") -} - -type vis struct{} - -func (*vis) BlockArray(a zjson.Array, pos int) zjson.CloseFunc { - log.Println("SBLO", pos, a) - return nil -} -func (*vis) InlineArray(a zjson.Array, pos int) zjson.CloseFunc { - log.Println("SINL", pos, a) - return nil -} -func (*vis) ItemArray(a zjson.Array, pos int) zjson.CloseFunc { - log.Println("SITE", pos, a) - return nil -} -func (*vis) BlockObject(t string, obj zjson.Object, pos int) (bool, zjson.CloseFunc) { - log.Println("BOBJ", pos, t, obj) - return true, nil -} -func (*vis) InlineObject(t string, obj zjson.Object, pos int) (bool, zjson.CloseFunc) { - log.Println("IOBJ", pos, t, obj) - return true, nil -} -func (*vis) Unexpected(val zjson.Value, pos int, exp string) { log.Println("Expect", pos, exp, val) } - -func TestGetSexprZettel(t *testing.T) { - c := getClient() - value, err := c.GetEvaluatedSexpr(context.Background(), api.ZidDefaultHome, api.PartContent) - if err != nil { - t.Error(err) - return - } - if value == nil { - t.Error("No data") - } - var env testEnv - env.t = t - res, err := sxpf.Eval(&env, value) - if err != nil { - t.Error(res, err) - } -} - -type testEnv struct{ t *testing.T } - -func noneFn(sxpf.Environment, *sxpf.Pair, int) (sxpf.Value, error) { return sxpf.Nil(), nil } -func (*testEnv) LookupForm(*sxpf.Symbol) (sxpf.Form, error) { - return sxpf.NewBuiltin("none", false, 0, -1, noneFn), nil -} -func (*testEnv) EvalSymbol(sym *sxpf.Symbol) (sxpf.Value, error) { return sym, nil } -func (*testEnv) EvalOther(val sxpf.Value) (sxpf.Value, error) { return val, nil } -func (te *testEnv) EvalPair(p *sxpf.Pair) (sxpf.Value, error) { return sxpf.EvalCallOrSeq(te, p) } +func TestGetSexprZettel(t *testing.T) { + c := getClient() + sf := sxpf.MakeMappedFactory() + var zetSyms sexpr.ZettelSymbols + zetSyms.InitializeZettelSymbols(sf) + value, err := c.GetEvaluatedSexpr(context.Background(), api.ZidDefaultHome, api.PartContent, sf) + if err != nil { + t.Error(err) + return + } + if value.IsNil() { + t.Error("No data") + } +} var baseURL string func init() { flag.StringVar(&baseURL, "base-url", "http://localhost:23123/", "Base URL") Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,5 +1,8 @@ module zettelstore.de/c -go 1.19 +go 1.20 -require codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 +require ( + codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 + codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 +) Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,2 +1,4 @@ -codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 h1:viya/OgeF16+i8caBPJmcLQhGpZodPh+/nxtJzSSO1s= -codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0/go.mod h1:4fAHEF3VH+ofbZkF6NzqiItTNy2X11tVCnZX99jXouA= +codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197 h1:6kX7TY25agLFlHNvByO1Jc3GrBA7mu7aOa8tCOniUew= +codeberg.org/t73fde/sxhtml v0.0.0-20230317170051-24321195e197/go.mod h1:Dp3EwBSsE3TvdPw9QZ4Wm25ZragluVT2OayRFRiq6jk= +codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475 h1:0OTzV3FYY/Y7YsaVaSzF4Wd17pXzdH6DaSvMeqteJc4= +codeberg.org/t73fde/sxpf v0.0.0-20230319111333-7de220f3b475/go.mod h1:iSbMygOmtRQYp8pryNKYzRuMibYDSR80smU2b6qm1bc= DELETED html/html.go Index: html/html.go ================================================================== --- html/html.go +++ html/html.go @@ -1,106 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2022 Detlef Stern -// -// This file is part of zettelstore-client. -// -// Zettelstore client is licensed under the latest version of the EUPL -// (European Union Public License). Please see file LICENSE.txt for your rights -// and obligations under this license. -//----------------------------------------------------------------------------- - -// Package html provides types, constants and function to work with HTML. -package html - -import ( - "io" - "strings" -) - -const ( - htmlQuot = """ // longer than """, but often requested in standards - htmlAmp = "&" - htmlLt = "<" - htmlGt = ">" - htmlNull = "\uFFFD" - htmlLitSpace = "\u00a0" - htmlVisSpace = "\u2423" -) - -var ( - htmlEscapes = []string{`&`, htmlAmp, - `<`, htmlLt, - `>`, htmlGt, - `"`, htmlQuot, - "\000", htmlNull, - } - htmlEscaper = strings.NewReplacer(htmlEscapes...) - - htmlVisEscapes = append(append([]string{}, htmlEscapes...), - " ", htmlVisSpace, - htmlLitSpace, htmlVisSpace, - ) - htmlVisEscaper = strings.NewReplacer(htmlVisEscapes...) -) - -// Escape writes to w the escaped HTML equivalent of the given string. -func Escape(w io.Writer, s string) (int, error) { return htmlEscaper.WriteString(w, s) } - -// EscapeVisible writes to w the escaped HTML equivalent of the given string. -// Each space is written as U-2423. -func EscapeVisible(w io.Writer, s string) (int, error) { return htmlVisEscaper.WriteString(w, s) } - -var ( - escQuot = []byte(htmlQuot) // longer than """, but often requested in standards - escAmp = []byte(htmlAmp) - escNull = []byte(htmlNull) -) - -// AttributeEscape writes to w the escaped HTML equivalent of the given string to be used -// in attributes. -func AttributeEscape(w io.Writer, s string) (int, error) { - length := 0 - last := 0 - var html []byte - lenS := len(s) - for i := 0; i < lenS; i++ { - switch s[i] { - case '\000': - html = escNull - case '"': - html = escQuot - case '&': - html = escAmp - default: - continue - } - l, err := io.WriteString(w, s[last:i]) - length += l - if err != nil { - return length, err - } - l, err = w.Write(html) - length += l - if err != nil { - return length, err - } - last = i + 1 - } - l, err := io.WriteString(w, s[last:]) - return length + l, err -} - -var unsafeSnippets = []string{ - "Change Log + + +