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{ - "") -} - -func (env *EncEnvironment) WriteEndTag(tag string) { - env.WriteStrings("") -} - -func (env *EncEnvironment) WriteImage(args *sxpf.Pair) { - ref := env.GetPair(args.GetTail()) - env.WriteImageWithSource(args, env.GetString(ref.GetTail())) -} - -func (env *EncEnvironment) WriteImageWithSource(args *sxpf.Pair, src string) { - a := env.GetAttributes(args) - a = a.Set("src", src) - if title := args.GetTail().GetTail().GetTail(); !title.IsNil() { - a = a.Set("title", text.EvaluateInlineString(title)) - } - env.WriteStartTag("img", a) -} - -func (env *EncEnvironment) LookupForm(sym *sxpf.Symbol) (sxpf.Form, error) { - return env.Builtins.LookupForm(sym) -} - -func (env *EncEnvironment) EvalOther(val sxpf.Value) (sxpf.Value, error) { - if strVal, ok := val.(*sxpf.String); ok { - env.WriteEscaped(strVal.GetValue()) - return nil, nil - } - return val, nil -} - -func (env *EncEnvironment) EvalSymbol(val *sxpf.Symbol) (sxpf.Value, error) { - env.WriteEscaped(val.GetValue()) - return nil, nil -} - -func (env *EncEnvironment) EvalPair(p *sxpf.Pair) (sxpf.Value, error) { - return sxpf.EvalCallOrSeq(env, p) -} - -func EvaluateInline(baseEnv *EncEnvironment, value sxpf.Value, withFootnotes, noLinks bool) string { - var buf bytes.Buffer - env := EncEnvironment{w: &buf, noLinks: noLinks} - if baseEnv != nil { - env.Builtins = baseEnv.Builtins - env.writeFootnotes = withFootnotes && baseEnv.writeFootnotes - env.footnotes = baseEnv.footnotes - } else { - env.Builtins = buildBuiltins() - } - _, err := sxpf.Eval(&env, value) - if err != nil { - return err.Error() - } - if baseEnv != nil { - baseEnv.footnotes = env.footnotes - } - return buf.String() -} - -func (env *EncEnvironment) WriteEndnotes() { - if len(env.footnotes) == 0 { - return - } - env.WriteString("
    ") - for i := 0; i < len(env.footnotes); i++ { - fni := env.footnotes[i] - n := strconv.Itoa(i + 1) - un := env.unique + n - a := fni.attrs.Clone().AddClass("zs-endnote").Set("value", n) - if _, found := a.Get("id"); !found { - a = a.Set("id", "fn:"+un) - } - if _, found := a.Get("role"); !found { - a = a.Set("role", "doc-endnote") - } - env.WriteStartTag("li", a) - sxpf.EvalSequence(env, fni.note) // may add more footnotes - env.WriteStrings( - ` ↩︎") - } - env.footnotes = nil - env.WriteString("
") -} - -type encodingFunc func(env *EncEnvironment, args *sxpf.Pair) - -var defaultEncodingFunctions = []struct { - sym *sxpf.Symbol - minArgs int - fn encodingFunc -}{ - {sexpr.SymPara, 0, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteString("

") - sxpf.EvalSequence(env, args) - env.WriteString("

") - }}, - {sexpr.SymHeading, 5, func(env *EncEnvironment, args *sxpf.Pair) { - nLevel := env.GetInteger(args) - if nLevel <= 0 { - return - } - level := strconv.FormatInt(nLevel+int64(env.headingOffset), 10) - - argAttr := args.GetTail() - a := env.GetAttributes(argAttr) - argFragment := argAttr.GetTail().GetTail() - if fragment := env.GetString(argFragment); fragment != "" { - a = a.Set("id", fragment) - } - - env.WriteStrings("") - sxpf.EvalSequence(env, argFragment.GetTail()) - env.WriteStrings("") - }}, - {sexpr.SymThematic, 0, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteString("") - }}, - {sexpr.SymListUnordered, 0, makeListFn("ul")}, - {sexpr.SymListOrdered, 0, makeListFn("ol")}, - {sexpr.SymListQuote, 0, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteString("
") - if !args.IsNil() && args.GetFirst().IsNil() { - sxpf.Eval(env, env.GetPair(args)) - } else { - for elem := args; !elem.IsNil(); elem = elem.GetTail() { - env.WriteString("

") - sxpf.Eval(env, env.GetPair(elem)) - env.WriteString("

") - } - } - env.WriteString("
") - }}, - {sexpr.SymDescription, 0, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteString("
") - for elem := args; !elem.IsNil(); elem = elem.GetTail() { - env.WriteString("
") - sxpf.Eval(env, elem.GetFirst()) - env.WriteString("
") - elem = elem.GetTail() - if elem.IsNil() { - break - } - ddlist, err := elem.GetPair() - if err != nil { - continue - } - for dditem := ddlist; !dditem.IsNil(); dditem = dditem.GetTail() { - env.WriteString("
") - sxpf.Eval(env, dditem.GetFirst()) - env.WriteString("
") - } - } - env.WriteString("
") - }}, - {sexpr.SymTable, 1, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteString("") - if header := env.GetPair(args); !header.IsNil() { - env.WriteString("") - env.writeTableRow(header) - env.WriteString("") - } - if argBody := args.GetTail(); !argBody.IsNil() { - env.WriteString("") - for row := argBody; !row.IsNil(); row = row.GetTail() { - env.writeTableRow(env.GetPair(row)) - } - env.WriteString("") - } - env.WriteString("
") - }}, - {sexpr.SymCell, 0, makeCellFn("")}, - {sexpr.SymCellCenter, 0, makeCellFn("center")}, - {sexpr.SymCellLeft, 0, makeCellFn("left")}, - {sexpr.SymCellRight, 0, makeCellFn("right")}, - {sexpr.SymRegionBlock, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := env.GetAttributes(args) - if val, found := a.Get(""); found { - a = a.Remove("").AddClass(val) - } - env.writeRegion(args, a, "div") - }}, - {sexpr.SymRegionQuote, 2, func(env *EncEnvironment, args *sxpf.Pair) { - env.writeRegion(args, nil, "blockquote") - }}, - {sexpr.SymRegionVerse, 2, func(env *EncEnvironment, args *sxpf.Pair) { - env.writeRegion(args, nil, "div") - }}, - {sexpr.SymVerbatimComment, 1, func(env *EncEnvironment, args *sxpf.Pair) { - if env.GetAttributes(args).HasDefault() { - if s := env.GetString(args.GetTail()); s != "" { - env.WriteString("") - } - } - }}, - {sexpr.SymVerbatimEval, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := env.GetAttributes(args).AddClass("zs-eval") - env.writeVerbatim(args, a) - }}, - {sexpr.SymVerbatimHTML, 2, execHTML}, - {sexpr.SymVerbatimMath, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := env.GetAttributes(args).AddClass("zs-math") - env.writeVerbatim(args, a) - }}, - {sexpr.SymVerbatimProg, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := setProgLang(env.GetAttributes(args)) - oldVisible := env.visibleSpace - if a.HasDefault() { - a = a.RemoveDefault() - env.visibleSpace = true - } - env.writeVerbatim(args, a) - env.visibleSpace = oldVisible - }}, - {sexpr.SymVerbatimZettel, 0, DoNothingFn}, - {sexpr.SymBLOB, 3, func(env *EncEnvironment, args *sxpf.Pair) { - argSyntax := args.GetTail() - env.writeBLOB(env.GetString(args), env.GetString(argSyntax), env.GetString(argSyntax.GetTail())) - }}, - {sexpr.SymTransclude, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := sexpr.GetAttributes(env.GetPair(args)) - ref := env.GetPair(args.GetTail()) - refKind := env.GetSymbol(ref) - if refKind == nil { - return - } - if refValue := env.GetString(ref.GetTail()); refValue != "" { - if sexpr.SymRefStateExternal.Equal(refKind) { - a = a.Set("src", refValue).AddClass("external") - env.WriteString("

") - return - } - env.WriteStrings("") - return - } - if env.err == nil { - _, env.err = fmt.Fprintf(env.w, "%v\n", args) - } - log.Println("TRAN", args) - }}, - {sexpr.SymText, 0, func(env *EncEnvironment, args *sxpf.Pair) { - if !sxpf.IsNil(args) { - env.WriteEscaped(env.GetString(args)) - } - }}, - {sexpr.SymSpace, 0, func(env *EncEnvironment, args *sxpf.Pair) { - if sxpf.IsNil(args) { - env.WriteString(" ") - return - } - env.WriteEscaped(env.GetString(args)) - }}, - {sexpr.SymSoft, 0, func(env *EncEnvironment, _ *sxpf.Pair) { env.WriteString(" ") }}, - {sexpr.SymHard, 0, func(env *EncEnvironment, _ *sxpf.Pair) { env.WriteString("
") }}, - {sexpr.SymLinkInvalid, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteAsSpan(env, args) }}, - {sexpr.SymLinkZettel, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteHRefLink(env, args) }}, - {sexpr.SymLinkSelf, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteHRefLink(env, args) }}, - {sexpr.SymLinkFound, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteHRefLink(env, args) }}, - {sexpr.SymLinkBroken, 2, func(env *EncEnvironment, args *sxpf.Pair) { - if a, refValue, ok := PrepareLink(env, args); ok { - WriteLink(env, args, a.AddClass("broken"), refValue, "") - } - }}, - {sexpr.SymLinkHosted, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteHRefLink(env, args) }}, - {sexpr.SymLinkBased, 2, func(env *EncEnvironment, args *sxpf.Pair) { WriteHRefLink(env, args) }}, - {sexpr.SymLinkQuery, 2, func(env *EncEnvironment, args *sxpf.Pair) { - if a, refValue, ok := PrepareLink(env, args); ok { - query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue) - WriteLink(env, args, a.Set("href", query), refValue, "") - } - }}, - {sexpr.SymLinkExternal, 2, func(env *EncEnvironment, args *sxpf.Pair) { - if a, refValue, ok := PrepareLink(env, args); ok { - WriteLink(env, args, a.Set("href", refValue).AddClass("external"), refValue, "") - } - }}, - {sexpr.SymEmbed, 3, func(env *EncEnvironment, args *sxpf.Pair) { - argRef := args.GetTail() - if syntax := env.GetString(argRef.GetTail()); syntax == api.ValueSyntaxSVG { - ref := env.GetPair(argRef) - env.WriteStrings( - `
") - } else { - env.WriteImage(args) - } - }}, - {sexpr.SymEmbedBLOB, 3, func(env *EncEnvironment, args *sxpf.Pair) { - argSyntax := args.GetTail() - a, syntax, data := env.GetAttributes(args), env.GetString(argSyntax), env.GetString(argSyntax.GetTail()) - title, _ := a.Get("title") - env.writeBLOB(title, syntax, data) - }}, - {sexpr.SymCite, 2, func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteStartTag("span", env.GetAttributes(args)) - argKey := args.GetTail() - if key := env.GetString(argKey); key != "" { - env.WriteEscaped(key) - if text := argKey.GetTail(); !text.IsNil() { - env.WriteString(", ") - sxpf.EvalSequence(env, text) - } - } - env.WriteString("") - }}, - {sexpr.SymMark, 3, func(env *EncEnvironment, args *sxpf.Pair) { - if env.noLinks { - sxpf.Eval(env, sxpf.NewPair(sexpr.SymFormatSpan, args)) - return - } - argFragment := args.GetTail().GetTail() - if fragment := env.GetString(argFragment); fragment != "" { - env.WriteString(``) - sxpf.EvalSequence(env, argFragment.GetTail()) - env.WriteString("") - } else { - sxpf.EvalSequence(env, argFragment.GetTail()) - } - }}, - {sexpr.SymFootnote, 1, func(env *EncEnvironment, args *sxpf.Pair) { - if env.writeFootnotes { - a := env.GetAttributes(args) - env.footnotes = append(env.footnotes, sfootnodeInfo{args.GetTail(), a}) - n := strconv.Itoa(len(env.footnotes)) - un := env.unique + n - env.WriteStrings( - ``, n, ``) - } - }}, - {sexpr.SymFormatDelete, 1, makeFormatFn("del")}, - {sexpr.SymFormatEmph, 1, makeFormatFn("em")}, - {sexpr.SymFormatInsert, 1, makeFormatFn("ins")}, - {sexpr.SymFormatQuote, 1, writeQuote}, - {sexpr.SymFormatSpan, 1, makeFormatFn("span")}, - {sexpr.SymFormatStrong, 1, makeFormatFn("strong")}, - {sexpr.SymFormatSub, 1, makeFormatFn("sub")}, - {sexpr.SymFormatSuper, 1, makeFormatFn("sup")}, - {sexpr.SymLiteralComment, 1, func(env *EncEnvironment, args *sxpf.Pair) { - if env.GetAttributes(args).HasDefault() { - if s := env.GetString(args.GetTail()); s != "" { - env.WriteString("") - } - } - }}, - {sexpr.SymLiteralHTML, 2, execHTML}, - {sexpr.SymLiteralInput, 2, func(env *EncEnvironment, args *sxpf.Pair) { - env.writeLiteral(args, nil, "kbd") - }}, - {sexpr.SymLiteralMath, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := env.GetAttributes(args).AddClass("zs-math") - env.writeLiteral(args, a, "code") - }}, - {sexpr.SymLiteralOutput, 2, func(env *EncEnvironment, args *sxpf.Pair) { - env.writeLiteral(args, nil, "samp") - }}, - {sexpr.SymLiteralProg, 2, func(env *EncEnvironment, args *sxpf.Pair) { - a := setProgLang(env.GetAttributes(args)) - env.writeLiteral(args, a, "code") - }}, - {sexpr.SymLiteralZettel, 0, DoNothingFn}, -} - -// DoNothingFn is a function that does nothing. -func DoNothingFn(*EncEnvironment, *sxpf.Pair) { /* Should really do nothing */ } - -func makeListFn(tag string) encodingFunc { - return func(env *EncEnvironment, args *sxpf.Pair) { - env.WriteStartTag(tag, nil) - for elem := args; !elem.IsNil(); elem = elem.GetTail() { - env.WriteStartTag("li", nil) - sxpf.Eval(env, elem.GetFirst()) - env.WriteEndTag("li") - } - env.WriteEndTag(tag) - } -} - -func (env *EncEnvironment) writeTableRow(cells *sxpf.Pair) { - if !cells.IsNil() { - env.WriteString("") - for cell := cells; !cell.IsNil(); cell = cell.GetTail() { - sxpf.Eval(env, cell.GetFirst()) - } - env.WriteString("") - } -} -func makeCellFn(align string) encodingFunc { - return func(env *EncEnvironment, args *sxpf.Pair) { - if align == "" { - env.WriteString("") - } else { - env.WriteStrings(``) - } - sxpf.EvalSequence(env, args) - env.WriteString("") - } -} - -func (env *EncEnvironment) writeRegion(args *sxpf.Pair, a attrs.Attributes, tag string) { - if a == nil { - a = env.GetAttributes(args) - } - env.WriteStartTag(tag, a) - sxpf.Eval(env, env.GetPair(args.GetTail())) - if cite := env.GetPair(args.GetTail().GetTail()); !cite.IsNil() { - env.WriteString("") - sxpf.EvalSequence(env, cite) - env.WriteString("") - } - env.WriteEndTag(tag) -} - -func (env *EncEnvironment) writeVerbatim(args *sxpf.Pair, a attrs.Attributes) { - env.WriteString("
")
-	env.WriteStartTag("code", a)
-	env.WriteEscapedOrVisible(env.GetString(args.GetTail()))
-	env.WriteString("
") -} - -func execHTML(env *EncEnvironment, args *sxpf.Pair) { - if s := env.GetString(args.GetTail()); s != "" && IsSafe(s) { - env.WriteString(s) - } -} - -func (env *EncEnvironment) writeBLOB(title, syntax, data string) { - if data == "" { - return - } - switch syntax { - case "": - case api.ValueSyntaxSVG: - // TODO: add title as description - env.WriteStrings("

", data, "

") - default: - env.WriteStrings(`

`) - } else { - env.WriteString(`">

`) - } - } -} - -func PrepareLink(env *EncEnvironment, args *sxpf.Pair) (attrs.Attributes, string, bool) { - if env.noLinks { - WriteAsSpan(env, args) - return nil, "", false - } - return env.GetAttributes(args), env.GetString(args.GetTail()), true -} - -func WriteAsSpan(env *EncEnvironment, args *sxpf.Pair) { - if args.Length() > 2 { - sxpf.Eval(env, sxpf.NewPair(sexpr.SymFormatSpan, sxpf.NewPair(args.GetFirst(), args.GetTail().GetTail()))) - } -} - -func WriteLink(env *EncEnvironment, args *sxpf.Pair, a attrs.Attributes, refValue, suffix string) { - env.WriteString("") - - if args.Length() > 2 { - sxpf.EvalSequence(env, args.GetTail().GetTail()) - } else { - env.WriteString(refValue) - } - env.WriteStrings("", suffix) -} - -func WriteHRefLink(env *EncEnvironment, args *sxpf.Pair) { - if a, refValue, ok := PrepareLink(env, args); ok { - WriteLink(env, args, a.Set("href", refValue), refValue, "") - } -} - -func makeFormatFn(tag string) encodingFunc { - return func(env *EncEnvironment, args *sxpf.Pair) { - a := env.GetAttributes(args) - if val, found := a.Get(""); found { - a = a.Remove("").AddClass(val) - } - env.WriteStartTag(tag, a) - sxpf.EvalSequence(env, args.GetTail()) - env.WriteEndTag(tag) - } -} - -func writeQuote(env *EncEnvironment, args *sxpf.Pair) { - const langAttr = "lang" - a := env.GetAttributes(args) - lang, hasLang := a.Get(langAttr) - if hasLang { - a = a.Remove(langAttr) - env.WriteStartTag("span", attrs.Attributes{}.Set(langAttr, lang)) - } - env.WriteStartTag("q", a) - sxpf.EvalSequence(env, args.GetTail()) - env.WriteEndTag("q") - if hasLang { - env.WriteEndTag("span") - } -} - -func (env *EncEnvironment) writeLiteral(args *sxpf.Pair, a attrs.Attributes, tag string) { - if a == nil { - a = env.GetAttributes(args) - } - oldVisible := env.visibleSpace - if a.HasDefault() { - env.visibleSpace = true - a = a.RemoveDefault() - } - env.WriteStartTag(tag, a) - env.WriteEscapedOrVisible(env.GetString(args.GetTail())) - env.visibleSpace = oldVisible - env.WriteEndTag(tag) -} - -func setProgLang(a attrs.Attributes) attrs.Attributes { - if val, found := a.Get(""); found { - a = a.AddClass("language-" + val).Remove("") - } - return a -} Index: maps/maps.go ================================================================== --- maps/maps.go +++ maps/maps.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 Index: maps/maps_test.go ================================================================== --- maps/maps_test.go +++ maps/maps_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 DELETED sexpr/attrs.go Index: sexpr/attrs.go ================================================================== --- sexpr/attrs.go +++ sexpr/attrs.go @@ -1,36 +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 sexpr - -import ( - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/attrs" -) - -// GetAttributes traverses a s-expression list and returns an attribute structure. -func GetAttributes(seq *sxpf.Pair) (result attrs.Attributes) { - for elem := seq; !elem.IsNil(); elem = elem.GetTail() { - attr, err := elem.GetPair() - if err != nil { - continue - } - key, err := attr.GetString() - if err != nil { - continue - } - val, err := attr.GetTail().GetString() - if err != nil { - continue - } - result = result.Set(key, val) - } - return result -} Index: sexpr/const.go ================================================================== --- sexpr/const.go +++ sexpr/const.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 @@ -12,95 +12,286 @@ import "codeberg.org/t73fde/sxpf" // Various constants for Zettel data. Some of them are technically variables. -// Symbols for Zettel node types. -var ( - Smk = sxpf.NewTrivialSymbolMaker() - SymBLOB = Smk.MakeSymbol("BLOB") - SymCell = Smk.MakeSymbol("CELL") - SymCellCenter = Smk.MakeSymbol("CELL-CENTER") - SymCellLeft = Smk.MakeSymbol("CELL-LEFT") - SymCellRight = Smk.MakeSymbol("CELL-RIGHT") - SymCite = Smk.MakeSymbol("CITE") - SymDescription = Smk.MakeSymbol("DESCRIPTION") - SymEmbed = Smk.MakeSymbol("EMBED") - SymEmbedBLOB = Smk.MakeSymbol("EMBED-BLOB") - SymFootnote = Smk.MakeSymbol("FOOTNOTE") - SymFormatEmph = Smk.MakeSymbol("FORMAT-EMPH") - SymFormatDelete = Smk.MakeSymbol("FORMAT-DELETE") - SymFormatInsert = Smk.MakeSymbol("FORMAT-INSERT") - SymFormatQuote = Smk.MakeSymbol("FORMAT-QUOTE") - SymFormatSpan = Smk.MakeSymbol("FORMAT-SPAN") - SymFormatSub = Smk.MakeSymbol("FORMAT-SUB") - SymFormatSuper = Smk.MakeSymbol("FORMAT-SUPER") - SymFormatStrong = Smk.MakeSymbol("FORMAT-STRONG") - SymHard = Smk.MakeSymbol("HARD") - SymHeading = Smk.MakeSymbol("HEADING") - SymLinkInvalid = Smk.MakeSymbol("LINK-INVALID") - SymLinkZettel = Smk.MakeSymbol("LINK-ZETTEL") - SymLinkSelf = Smk.MakeSymbol("LINK-SELF") - SymLinkFound = Smk.MakeSymbol("LINK-FOUND") - SymLinkBroken = Smk.MakeSymbol("LINK-BROKEN") - SymLinkHosted = Smk.MakeSymbol("LINK-HOSTED") - SymLinkBased = Smk.MakeSymbol("LINK-BASED") - SymLinkQuery = Smk.MakeSymbol("LINK-QUERY") - SymLinkExternal = Smk.MakeSymbol("LINK-EXTERNAL") - SymListOrdered = Smk.MakeSymbol("ORDERED") - SymListUnordered = Smk.MakeSymbol("UNORDERED") - SymListQuote = Smk.MakeSymbol("QUOTATION") - SymLiteralProg = Smk.MakeSymbol("LITERAL-CODE") - SymLiteralComment = Smk.MakeSymbol("LITERAL-COMMENT") - SymLiteralHTML = Smk.MakeSymbol("LITERAL-HTML") - SymLiteralInput = Smk.MakeSymbol("LITERAL-INPUT") - SymLiteralMath = Smk.MakeSymbol("LITERAL-MATH") - SymLiteralOutput = Smk.MakeSymbol("LITERAL-OUTPUT") - SymLiteralZettel = Smk.MakeSymbol("LITERAL-ZETTEL") - SymMark = Smk.MakeSymbol("MARK") - SymPara = Smk.MakeSymbol("PARA") - SymRegionBlock = Smk.MakeSymbol("REGION-BLOCK") - SymRegionQuote = Smk.MakeSymbol("REGION-QUOTE") - SymRegionVerse = Smk.MakeSymbol("REGION-VERSE") - SymSoft = Smk.MakeSymbol("SOFT") - SymSpace = Smk.MakeSymbol("SPACE") - SymTable = Smk.MakeSymbol("TABLE") - SymText = Smk.MakeSymbol("TEXT") - SymThematic = Smk.MakeSymbol("THEMATIC") - SymTransclude = Smk.MakeSymbol("TRANSCLUDE") - SymUnknown = Smk.MakeSymbol("UNKNOWN-NODE") - SymVerbatimComment = Smk.MakeSymbol("VERBATIM-COMMENT") - SymVerbatimEval = Smk.MakeSymbol("VERBATIM-EVAL") - SymVerbatimHTML = Smk.MakeSymbol("VERBATIM-HTML") - SymVerbatimMath = Smk.MakeSymbol("VERBATIM-MATH") - SymVerbatimProg = Smk.MakeSymbol("VERBATIM-CODE") - SymVerbatimZettel = Smk.MakeSymbol("VERBATIM-ZETTEL") -) - -// Constant symbols for reference states. -var ( - SymRefStateInvalid = Smk.MakeSymbol("INVALID") - SymRefStateZettel = Smk.MakeSymbol("ZETTEL") - SymRefStateSelf = Smk.MakeSymbol("SELF") - SymRefStateFound = Smk.MakeSymbol("FOUND") - SymRefStateBroken = Smk.MakeSymbol("BROKEN") - SymRefStateHosted = Smk.MakeSymbol("HOSTED") - SymRefStateBased = Smk.MakeSymbol("BASED") - SymRefStateQuery = Smk.MakeSymbol("QUERY") - SymRefStateExternal = Smk.MakeSymbol("EXTERNAL") -) - -// Symbols for metadata types -var ( - SymTypeCredential = Smk.MakeSymbol("CREDENTIAL") - SymTypeEmpty = Smk.MakeSymbol("EMPTY-STRING") - SymTypeID = Smk.MakeSymbol("ZID") - SymTypeIDSet = Smk.MakeSymbol("ZID-SET") - SymTypeNumber = Smk.MakeSymbol("NUMBER") - SymTypeString = Smk.MakeSymbol("STRING") - SymTypeTagSet = Smk.MakeSymbol("TAG-SET") - SymTypeTimestamp = Smk.MakeSymbol("TIMESTAMP") - SymTypeURL = Smk.MakeSymbol("URL") - SymTypeWord = Smk.MakeSymbol("WORD") - SymTypeWordSet = Smk.MakeSymbol("WORD-SET") - SymTypeZettelmarkup = Smk.MakeSymbol("ZETTELMARKUP") -) +const ( + // Symbols for Metanodes + NameSymBlock = "BLOCK" + NameSymInline = "INLINE" + NameSymList = "LIST" + NameSymMeta = "META" + NameSymQuote = "quote" + + // Symbols for Zettel node types. + NameSymBLOB = "BLOB" + NameSymCell = "CELL" + NameSymCellCenter = "CELL-CENTER" + NameSymCellLeft = "CELL-LEFT" + NameSymCellRight = "CELL-RIGHT" + NameSymCite = "CITE" + NameSymDescription = "DESCRIPTION" + NameSymEmbed = "EMBED" + NameSymEmbedBLOB = "EMBED-BLOB" + NameSymEndnote = "ENDNOTE" + NameSymFormatEmph = "FORMAT-EMPH" + NameSymFormatDelete = "FORMAT-DELETE" + NameSymFormatInsert = "FORMAT-INSERT" + NameSymFormatQuote = "FORMAT-QUOTE" + NameSymFormatSpan = "FORMAT-SPAN" + NameSymFormatSub = "FORMAT-SUB" + NameSymFormatSuper = "FORMAT-SUPER" + NameSymFormatStrong = "FORMAT-STRONG" + NameSymHard = "HARD" + NameSymHeading = "HEADING" + NameSymLinkInvalid = "LINK-INVALID" + NameSymLinkZettel = "LINK-ZETTEL" + NameSymLinkSelf = "LINK-SELF" + NameSymLinkFound = "LINK-FOUND" + NameSymLinkBroken = "LINK-BROKEN" + NameSymLinkHosted = "LINK-HOSTED" + NameSymLinkBased = "LINK-BASED" + NameSymLinkQuery = "LINK-QUERY" + NameSymLinkExternal = "LINK-EXTERNAL" + NameSymListOrdered = "ORDERED" + NameSymListUnordered = "UNORDERED" + NameSymListQuote = "QUOTATION" + NameSymLiteralProg = "LITERAL-CODE" + NameSymLiteralComment = "LITERAL-COMMENT" + NameSymLiteralHTML = "LITERAL-HTML" + NameSymLiteralInput = "LITERAL-INPUT" + NameSymLiteralMath = "LITERAL-MATH" + NameSymLiteralOutput = "LITERAL-OUTPUT" + NameSymLiteralZettel = "LITERAL-ZETTEL" + NameSymMark = "MARK" + NameSymPara = "PARA" + NameSymRegionBlock = "REGION-BLOCK" + NameSymRegionQuote = "REGION-QUOTE" + NameSymRegionVerse = "REGION-VERSE" + NameSymSoft = "SOFT" + NameSymSpace = "SPACE" + NameSymTable = "TABLE" + NameSymText = "TEXT" + NameSymThematic = "THEMATIC" + NameSymTransclude = "TRANSCLUDE" + NameSymUnknown = "UNKNOWN-NODE" + NameSymVerbatimComment = "VERBATIM-COMMENT" + NameSymVerbatimEval = "VERBATIM-EVAL" + NameSymVerbatimHTML = "VERBATIM-HTML" + NameSymVerbatimMath = "VERBATIM-MATH" + NameSymVerbatimProg = "VERBATIM-CODE" + NameSymVerbatimZettel = "VERBATIM-ZETTEL" + + // Constant symbols for reference states. + NameSymRefStateInvalid = "INVALID" + NameSymRefStateZettel = "ZETTEL" + NameSymRefStateSelf = "SELF" + NameSymRefStateFound = "FOUND" + NameSymRefStateBroken = "BROKEN" + NameSymRefStateHosted = "HOSTED" + NameSymRefStateBased = "BASED" + NameSymRefStateQuery = "QUERY" + NameSymRefStateExternal = "EXTERNAL" + + // Symbols for metadata types. + NameSymTypeCredential = "CREDENTIAL" + NameSymTypeEmpty = "EMPTY-STRING" + NameSymTypeID = "ZID" + NameSymTypeIDSet = "ZID-SET" + NameSymTypeNumber = "NUMBER" + NameSymTypeString = "STRING" + NameSymTypeTagSet = "TAG-SET" + NameSymTypeTimestamp = "TIMESTAMP" + NameSymTypeURL = "URL" + NameSymTypeWord = "WORD" + NameSymTypeWordSet = "WORD-SET" + NameSymTypeZettelmarkup = "ZETTELMARKUP" +) + +// ZettelSymbols collect all symbols needed to represent zettel data. +type ZettelSymbols struct { + // Symbols for Metanodes + SymBlock *sxpf.Symbol + SymInline *sxpf.Symbol + SymList *sxpf.Symbol + SymMeta *sxpf.Symbol + SymQuote *sxpf.Symbol + + // Symbols for Zettel node types. + SymBLOB *sxpf.Symbol + SymCell *sxpf.Symbol + SymCellCenter *sxpf.Symbol + SymCellLeft *sxpf.Symbol + SymCellRight *sxpf.Symbol + SymCite *sxpf.Symbol + SymDescription *sxpf.Symbol + SymEmbed *sxpf.Symbol + SymEmbedBLOB *sxpf.Symbol + SymEndnote *sxpf.Symbol + SymFormatEmph *sxpf.Symbol + SymFormatDelete *sxpf.Symbol + SymFormatInsert *sxpf.Symbol + SymFormatQuote *sxpf.Symbol + SymFormatSpan *sxpf.Symbol + SymFormatSub *sxpf.Symbol + SymFormatSuper *sxpf.Symbol + SymFormatStrong *sxpf.Symbol + SymHard *sxpf.Symbol + SymHeading *sxpf.Symbol + SymLinkInvalid *sxpf.Symbol + SymLinkZettel *sxpf.Symbol + SymLinkSelf *sxpf.Symbol + SymLinkFound *sxpf.Symbol + SymLinkBroken *sxpf.Symbol + SymLinkHosted *sxpf.Symbol + SymLinkBased *sxpf.Symbol + SymLinkQuery *sxpf.Symbol + SymLinkExternal *sxpf.Symbol + SymListOrdered *sxpf.Symbol + SymListUnordered *sxpf.Symbol + SymListQuote *sxpf.Symbol + SymLiteralProg *sxpf.Symbol + SymLiteralComment *sxpf.Symbol + SymLiteralHTML *sxpf.Symbol + SymLiteralInput *sxpf.Symbol + SymLiteralMath *sxpf.Symbol + SymLiteralOutput *sxpf.Symbol + SymLiteralZettel *sxpf.Symbol + SymMark *sxpf.Symbol + SymPara *sxpf.Symbol + SymRegionBlock *sxpf.Symbol + SymRegionQuote *sxpf.Symbol + SymRegionVerse *sxpf.Symbol + SymSoft *sxpf.Symbol + SymSpace *sxpf.Symbol + SymTable *sxpf.Symbol + SymText *sxpf.Symbol + SymThematic *sxpf.Symbol + SymTransclude *sxpf.Symbol + SymUnknown *sxpf.Symbol + SymVerbatimComment *sxpf.Symbol + SymVerbatimEval *sxpf.Symbol + SymVerbatimHTML *sxpf.Symbol + SymVerbatimMath *sxpf.Symbol + SymVerbatimProg *sxpf.Symbol + SymVerbatimZettel *sxpf.Symbol + + // Constant symbols for reference states. + + SymRefStateInvalid *sxpf.Symbol + SymRefStateZettel *sxpf.Symbol + SymRefStateSelf *sxpf.Symbol + SymRefStateFound *sxpf.Symbol + SymRefStateBroken *sxpf.Symbol + SymRefStateHosted *sxpf.Symbol + SymRefStateBased *sxpf.Symbol + SymRefStateQuery *sxpf.Symbol + SymRefStateExternal *sxpf.Symbol + + // Symbols for metadata types + + SymTypeCredential *sxpf.Symbol + SymTypeEmpty *sxpf.Symbol + SymTypeID *sxpf.Symbol + SymTypeIDSet *sxpf.Symbol + SymTypeNumber *sxpf.Symbol + SymTypeString *sxpf.Symbol + SymTypeTagSet *sxpf.Symbol + SymTypeTimestamp *sxpf.Symbol + SymTypeURL *sxpf.Symbol + SymTypeWord *sxpf.Symbol + SymTypeWordSet *sxpf.Symbol + SymTypeZettelmarkup *sxpf.Symbol +} + +func (zs *ZettelSymbols) InitializeZettelSymbols(sf sxpf.SymbolFactory) { + // Symbols for Metanodes + zs.SymBlock = sf.MustMake(NameSymBlock) + zs.SymInline = sf.MustMake(NameSymInline) + zs.SymList = sf.MustMake(NameSymList) + zs.SymMeta = sf.MustMake(NameSymMeta) + zs.SymQuote = sf.MustMake(NameSymQuote) + + // Symbols for Zettel node types. + zs.SymBLOB = sf.MustMake(NameSymBLOB) + zs.SymCell = sf.MustMake(NameSymCell) + zs.SymCellCenter = sf.MustMake(NameSymCellCenter) + zs.SymCellLeft = sf.MustMake(NameSymCellLeft) + zs.SymCellRight = sf.MustMake(NameSymCellRight) + zs.SymCite = sf.MustMake(NameSymCite) + zs.SymDescription = sf.MustMake(NameSymDescription) + zs.SymEmbed = sf.MustMake(NameSymEmbed) + zs.SymEmbedBLOB = sf.MustMake(NameSymEmbedBLOB) + zs.SymEndnote = sf.MustMake(NameSymEndnote) + zs.SymFormatEmph = sf.MustMake(NameSymFormatEmph) + zs.SymFormatDelete = sf.MustMake(NameSymFormatDelete) + zs.SymFormatInsert = sf.MustMake(NameSymFormatInsert) + zs.SymFormatQuote = sf.MustMake(NameSymFormatQuote) + zs.SymFormatSpan = sf.MustMake(NameSymFormatSpan) + zs.SymFormatSub = sf.MustMake(NameSymFormatSub) + zs.SymFormatSuper = sf.MustMake(NameSymFormatSuper) + zs.SymFormatStrong = sf.MustMake(NameSymFormatStrong) + zs.SymHard = sf.MustMake(NameSymHard) + zs.SymHeading = sf.MustMake(NameSymHeading) + zs.SymLinkInvalid = sf.MustMake(NameSymLinkInvalid) + zs.SymLinkZettel = sf.MustMake(NameSymLinkZettel) + zs.SymLinkSelf = sf.MustMake(NameSymLinkSelf) + zs.SymLinkFound = sf.MustMake(NameSymLinkFound) + zs.SymLinkBroken = sf.MustMake(NameSymLinkBroken) + zs.SymLinkHosted = sf.MustMake(NameSymLinkHosted) + zs.SymLinkBased = sf.MustMake(NameSymLinkBased) + zs.SymLinkQuery = sf.MustMake(NameSymLinkQuery) + zs.SymLinkExternal = sf.MustMake(NameSymLinkExternal) + zs.SymListOrdered = sf.MustMake(NameSymListOrdered) + zs.SymListUnordered = sf.MustMake(NameSymListUnordered) + zs.SymListQuote = sf.MustMake(NameSymListQuote) + zs.SymLiteralProg = sf.MustMake(NameSymLiteralProg) + zs.SymLiteralComment = sf.MustMake(NameSymLiteralComment) + zs.SymLiteralHTML = sf.MustMake(NameSymLiteralHTML) + zs.SymLiteralInput = sf.MustMake(NameSymLiteralInput) + zs.SymLiteralMath = sf.MustMake(NameSymLiteralMath) + zs.SymLiteralOutput = sf.MustMake(NameSymLiteralOutput) + zs.SymLiteralZettel = sf.MustMake(NameSymLiteralZettel) + zs.SymMark = sf.MustMake(NameSymMark) + zs.SymPara = sf.MustMake(NameSymPara) + zs.SymRegionBlock = sf.MustMake(NameSymRegionBlock) + zs.SymRegionQuote = sf.MustMake(NameSymRegionQuote) + zs.SymRegionVerse = sf.MustMake(NameSymRegionVerse) + zs.SymSoft = sf.MustMake(NameSymSoft) + zs.SymSpace = sf.MustMake(NameSymSpace) + zs.SymTable = sf.MustMake(NameSymTable) + zs.SymText = sf.MustMake(NameSymText) + zs.SymThematic = sf.MustMake(NameSymThematic) + zs.SymTransclude = sf.MustMake(NameSymTransclude) + zs.SymUnknown = sf.MustMake(NameSymUnknown) + zs.SymVerbatimComment = sf.MustMake(NameSymVerbatimComment) + zs.SymVerbatimEval = sf.MustMake(NameSymVerbatimEval) + zs.SymVerbatimHTML = sf.MustMake(NameSymVerbatimHTML) + zs.SymVerbatimMath = sf.MustMake(NameSymVerbatimMath) + zs.SymVerbatimProg = sf.MustMake(NameSymVerbatimProg) + zs.SymVerbatimZettel = sf.MustMake(NameSymVerbatimZettel) + + // Constant symbols for reference states. + zs.SymRefStateInvalid = sf.MustMake(NameSymRefStateInvalid) + zs.SymRefStateZettel = sf.MustMake(NameSymRefStateZettel) + zs.SymRefStateSelf = sf.MustMake(NameSymRefStateSelf) + zs.SymRefStateFound = sf.MustMake(NameSymRefStateFound) + zs.SymRefStateBroken = sf.MustMake(NameSymRefStateBroken) + zs.SymRefStateHosted = sf.MustMake(NameSymRefStateHosted) + zs.SymRefStateBased = sf.MustMake(NameSymRefStateBased) + zs.SymRefStateQuery = sf.MustMake(NameSymRefStateQuery) + zs.SymRefStateExternal = sf.MustMake(NameSymRefStateExternal) + + // Symbols for metadata types. + zs.SymTypeCredential = sf.MustMake(NameSymTypeCredential) + zs.SymTypeEmpty = sf.MustMake(NameSymTypeEmpty) + zs.SymTypeID = sf.MustMake(NameSymTypeID) + zs.SymTypeIDSet = sf.MustMake(NameSymTypeIDSet) + zs.SymTypeNumber = sf.MustMake(NameSymTypeNumber) + zs.SymTypeString = sf.MustMake(NameSymTypeString) + zs.SymTypeTagSet = sf.MustMake(NameSymTypeTagSet) + zs.SymTypeTimestamp = sf.MustMake(NameSymTypeTimestamp) + zs.SymTypeURL = sf.MustMake(NameSymTypeURL) + zs.SymTypeWord = sf.MustMake(NameSymTypeWord) + zs.SymTypeWordSet = sf.MustMake(NameSymTypeWordSet) + zs.SymTypeZettelmarkup = sf.MustMake(NameSymTypeZettelmarkup) +} ADDED sexpr/const_test.go Index: sexpr/const_test.go ================================================================== --- sexpr/const_test.go +++ sexpr/const_test.go @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-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 +// and obligations under this license. +//----------------------------------------------------------------------------- + +package sexpr_test + +import ( + "testing" + + "codeberg.org/t73fde/sxpf" + "zettelstore.de/c/sexpr" +) + +func BenchmarkInitializeZettelSymbols(b *testing.B) { + sf := sxpf.MakeMappedFactory() + for i := 0; i < b.N; i++ { + var zs sexpr.ZettelSymbols + zs.InitializeZettelSymbols(sf) + } +} Index: sexpr/sexpr.go ================================================================== --- sexpr/sexpr.go +++ sexpr/sexpr.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 @@ -8,28 +8,45 @@ // and obligations under this license. //----------------------------------------------------------------------------- package sexpr -import "codeberg.org/t73fde/sxpf" +import ( + "codeberg.org/t73fde/sxpf" + "zettelstore.de/c/attrs" +) -func MakeString(val sxpf.Value) string { - if strVal, ok := val.(*sxpf.String); ok { - return strVal.GetValue() +// GetAttributes traverses a s-expression list and returns an attribute structure. +func GetAttributes(seq *sxpf.List) (result attrs.Attributes) { + for elem := seq; elem != nil; elem = elem.Tail() { + p, ok := elem.Car().(*sxpf.List) + if !ok || p == nil { + continue + } + key := p.Car() + if !sxpf.IsAtom(key) { + continue + } + val := p.Cdr() + if tail, ok2 := val.(*sxpf.List); ok2 { + val = tail.Car() + } + if !sxpf.IsAtom(val) { + continue + } + result = result.Set(key.String(), val.String()) } - return "" + return result } // GetMetaContent returns the metadata and the content of a sexpr encoded zettel. -func GetMetaContent(zettel sxpf.Value) (Meta, *sxpf.Pair) { - if pair, ok := zettel.(*sxpf.Pair); ok { - m := pair.GetFirst() - if s := pair.GetSecond(); s != nil { - if p, ok := s.(*sxpf.Pair); ok { - if content, err := p.GetPair(); err == nil { - return MakeMeta(m), content - } +func GetMetaContent(zettel sxpf.Object) (Meta, *sxpf.List) { + if pair, ok := zettel.(*sxpf.List); ok { + m := pair.Car() + if s := pair.Tail(); s != nil { + if content, ok2 := s.Car().(*sxpf.List); ok2 { + return MakeMeta(m), content } } return MakeMeta(m), nil } return nil, nil @@ -37,73 +54,73 @@ type Meta map[string]MetaValue type MetaValue struct { Type string Key string - Value sxpf.Value + Value sxpf.Object } -func MakeMeta(val sxpf.Value) Meta { +func MakeMeta(val sxpf.Object) Meta { if result := doMakeMeta(val); len(result) > 0 { return result } return nil } -func doMakeMeta(val sxpf.Value) Meta { - result := make(map[string]MetaValue) - for { - if val == nil { - return result - } - pair, ok := val.(*sxpf.Pair) - if !ok { - return result - } - if mv, ok := makeMetaValue(pair); ok { - result[mv.Key] = mv - } - val = pair.GetSecond() - } -} -func makeMetaValue(pair *sxpf.Pair) (MetaValue, bool) { - var result MetaValue - typePair, ok := pair.GetFirst().(*sxpf.Pair) - if !ok { - return result, false - } - typeVal, ok := typePair.GetFirst().(*sxpf.Symbol) - if !ok { - return result, false - } - keyPair, ok := typePair.GetSecond().(*sxpf.Pair) - if !ok { - return result, false - } - keyStr, ok := keyPair.GetFirst().(*sxpf.String) - if !ok { - return result, false - } - valPair, ok := keyPair.GetSecond().(*sxpf.Pair) - if !ok { - return result, false - } - result.Type = typeVal.GetValue() - result.Key = keyStr.GetValue() - result.Value = valPair.GetFirst() +func doMakeMeta(val sxpf.Object) Meta { + result := make(map[string]MetaValue) + for { + if sxpf.IsNil(val) { + return result + } + lst, ok := val.(*sxpf.List) + if !ok { + return result + } + if mv, ok2 := makeMetaValue(lst); ok2 { + result[mv.Key] = mv + } + val = lst.Cdr() + } +} +func makeMetaValue(pair *sxpf.List) (MetaValue, bool) { + var result MetaValue + typePair, ok := pair.Car().(*sxpf.List) + if !ok { + return result, false + } + typeVal, ok := typePair.Car().(*sxpf.Symbol) + if !ok { + return result, false + } + keyPair, ok := typePair.Cdr().(*sxpf.List) + if !ok { + return result, false + } + keyStr, ok := keyPair.Car().(sxpf.String) + if !ok { + return result, false + } + valPair, ok := keyPair.Cdr().(*sxpf.List) + if !ok { + return result, false + } + result.Type = typeVal.CanonicalName() + result.Key = keyStr.String() + result.Value = valPair.Car() return result, true } func (m Meta) GetString(key string) string { if v, found := m[key]; found { - return MakeString(v.Value) + return v.Value.String() } return "" } -func (m Meta) GetPair(key string) *sxpf.Pair { +func (m Meta) GetList(key string) *sxpf.List { if mv, found := m[key]; found { - if seq, ok := mv.Value.(*sxpf.Pair); ok && !seq.IsEmpty() { + if seq, ok := mv.Value.(*sxpf.List); ok { return seq } } return nil } ADDED shtml/shtml.go Index: shtml/shtml.go ================================================================== --- shtml/shtml.go +++ shtml/shtml.go @@ -0,0 +1,896 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-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 +// and obligations under this license. +//----------------------------------------------------------------------------- + +// Package shtml transforms a s-expr encoded zettel AST into a s-expr representation of HTML. +package shtml + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + "codeberg.org/t73fde/sxhtml" + "codeberg.org/t73fde/sxpf" + "codeberg.org/t73fde/sxpf/builtins/quote" + "codeberg.org/t73fde/sxpf/eval" + "zettelstore.de/c/api" + "zettelstore.de/c/attrs" + "zettelstore.de/c/sexpr" + "zettelstore.de/c/text" +) + +// Transformer will transform a s-expression that encodes the zettel AST into an s-expression +// that represents HTML. +type Transformer struct { + sf sxpf.SymbolFactory + rebinder RebindProc + headingOffset int64 + unique string + endnotes []endnoteInfo + noLinks bool // true iff output must not include links + symAttr *sxpf.Symbol + symClass *sxpf.Symbol + symMeta *sxpf.Symbol + symA *sxpf.Symbol + symSpan *sxpf.Symbol +} + +type endnoteInfo struct { + noteAST *sxpf.List // Endnote as AST + noteHx *sxpf.List // Endnote as SxHTML + attrs *sxpf.List // attrs a-list +} + +// NewTransformer creates a new transformer object. +func NewTransformer(headingOffset int, sf sxpf.SymbolFactory) *Transformer { + if sf == nil { + sf = sxpf.MakeMappedFactory() + } + return &Transformer{ + sf: sf, + rebinder: nil, + headingOffset: int64(headingOffset), + symAttr: sf.MustMake(sxhtml.NameSymAttr), + symClass: sf.MustMake("class"), + symMeta: sf.MustMake("meta"), + symA: sf.MustMake("a"), + symSpan: sf.MustMake("span"), + } +} + +// SymbolFactory returns the symbol factory to create HTML symbols. +func (tr *Transformer) SymbolFactory() sxpf.SymbolFactory { return tr.sf } + +// SetUnique sets a prefix to make several HTML ids unique. +func (tr *Transformer) SetUnique(s string) { tr.unique = s } + +// IsValidName returns true, if name is a valid symbol name. +func (tr *Transformer) IsValidName(s string) bool { return tr.sf.IsValidName(s) } + +// Make a new HTML symbol. +func (tr *Transformer) Make(s string) *sxpf.Symbol { return tr.sf.MustMake(s) } + +// RebindProc is a procedure which is called every time before a tranformation takes place. +type RebindProc func(*TransformEnv) + +// SetRebinder sets the rebinder procedure. +func (tr *Transformer) SetRebinder(rb RebindProc) { tr.rebinder = rb } + +// TransformAttrbute transforms the given attributes into a HTML s-expression. +func (tr *Transformer) TransformAttrbute(a attrs.Attributes) *sxpf.List { + if len(a) == 0 { + return sxpf.Nil() + } + plist := sxpf.Nil() + keys := a.Keys() + for i := len(keys) - 1; i >= 0; i-- { + key := keys[i] + if key != attrs.DefaultAttribute && tr.IsValidName(key) { + plist = plist.Cons(sxpf.Cons(tr.Make(key), sxpf.MakeString(a[key]))) + } + } + if plist == nil { + return sxpf.Nil() + } + return plist.Cons(tr.symAttr) +} + +// TransformMeta creates a HTML meta s-expression +func (tr *Transformer) TransformMeta(a attrs.Attributes) *sxpf.List { + return sxpf.Nil().Cons(tr.TransformAttrbute(a)).Cons(tr.symMeta) +} + +// Transform an AST s-expression into a list of HTML s-expressions. +func (tr *Transformer) Transform(lst *sxpf.List) (*sxpf.List, error) { + astSF := sxpf.FindSymbolFactory(lst) + if astSF != nil { + if astSF == tr.sf { + panic("Invalid AST SymbolFactory") + } + } else { + astSF = sxpf.MakeMappedFactory() + } + astEnv := sxpf.MakeRootEnvironment() + engine := eval.MakeEngine(astSF, astEnv, eval.MakeDefaultParser(), eval.MakeSimpleExecutor()) + quote.InstallQuote(engine, sexpr.NameSymQuote, nil, 0) + te := TransformEnv{ + tr: tr, + astSF: astSF, + astEnv: astEnv, + err: nil, + textEnc: text.NewEncoder(astSF), + } + te.initialize() + if rb := tr.rebinder; rb != nil { + rb(&te) + } + + val, err := engine.Eval(te.astEnv, lst) + if err != nil { + return sxpf.Nil(), err + } + res, ok := val.(*sxpf.List) + if !ok { + panic("Result is not a list") + } + for i := 0; i < len(tr.endnotes); i++ { + // May extend tr.endnotes + val, err = engine.Eval(te.astEnv, tr.endnotes[i].noteAST) + if err != nil { + return res, err + } + en, ok2 := val.(*sxpf.List) + if !ok2 { + panic("Endnote is not a list") + } + tr.endnotes[i].noteHx = en + } + return res, err + +} + +// Endnotes returns a SHTML object with all collected endnotes. +func (tr *Transformer) Endnotes() *sxpf.List { + if len(tr.endnotes) == 0 { + return nil + } + result := sxpf.Nil().Cons(tr.Make("ol")) + currResult := result.AppendBang(sxpf.Nil().Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnotes"))).Cons(tr.symAttr)) + for i, fni := range tr.endnotes { + noteNum := strconv.Itoa(i + 1) + noteID := tr.unique + noteNum + + attrs := fni.attrs.Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnote"))). + Cons(sxpf.Cons(tr.Make("value"), sxpf.MakeString(noteNum))). + Cons(sxpf.Cons(tr.Make("id"), sxpf.MakeString("fn:"+noteID))). + Cons(sxpf.Cons(tr.Make("role"), sxpf.MakeString("doc-endnote"))). + Cons(tr.symAttr) + + backref := sxpf.Nil().Cons(sxpf.MakeString("\u21a9\ufe0e")). + Cons(sxpf.Nil(). + Cons(sxpf.Cons(tr.symClass, sxpf.MakeString("zs-endnote-backref"))). + Cons(sxpf.Cons(tr.Make("href"), sxpf.MakeString("#fnref:"+noteID))). + Cons(sxpf.Cons(tr.Make("role"), sxpf.MakeString("doc-backlink"))). + Cons(tr.symAttr)). + Cons(tr.symA) + + li := sxpf.Nil().Cons(tr.Make("li")) + li.AppendBang(attrs). + ExtendBang(fni.noteHx). + AppendBang(sxpf.MakeString(" ")).AppendBang(backref) + currResult = currResult.AppendBang(li) + } + tr.endnotes = nil + return result +} + +// TransformEnv is the environment where the actual transformation takes places. +type TransformEnv struct { + tr *Transformer + astSF sxpf.SymbolFactory + astEnv sxpf.Environment + err error + textEnc *text.Encoder + symNoEscape *sxpf.Symbol + symAttr *sxpf.Symbol + symMeta *sxpf.Symbol + symA *sxpf.Symbol + symSpan *sxpf.Symbol + symP *sxpf.Symbol +} + +func (te *TransformEnv) initialize() { + te.symNoEscape = te.Make(sxhtml.NameSymNoEscape) + te.symAttr = te.tr.symAttr + te.symMeta = te.tr.symMeta + te.symA = te.tr.symA + te.symSpan = te.tr.symSpan + te.symP = te.Make("p") + + te.bind(sexpr.NameSymList, 0, listArgs) + te.bindMetadata() + te.bindBlocks() + te.bindInlines() +} + +func listArgs(args *sxpf.List) sxpf.Object { return args } + +func (te *TransformEnv) bindMetadata() { + te.bind(sexpr.NameSymMeta, 0, listArgs) + te.bind(sexpr.NameSymTypeZettelmarkup, 2, func(args *sxpf.List) sxpf.Object { + a := make(attrs.Attributes, 2). + Set("name", te.getString(args).String()). + Set("content", te.textEnc.Encode(te.getList(args.Tail()))) + return te.transformMeta(a) + }) + metaString := func(args *sxpf.List) sxpf.Object { + a := make(attrs.Attributes, 2). + Set("name", te.getString(args).String()). + Set("content", te.getString(args.Tail()).String()) + return te.transformMeta(a) + } + te.bind(sexpr.NameSymTypeCredential, 2, metaString) + te.bind(sexpr.NameSymTypeEmpty, 2, metaString) + te.bind(sexpr.NameSymTypeID, 2, metaString) + te.bind(sexpr.NameSymTypeNumber, 2, metaString) + te.bind(sexpr.NameSymTypeString, 2, metaString) + te.bind(sexpr.NameSymTypeTimestamp, 2, metaString) + te.bind(sexpr.NameSymTypeURL, 2, metaString) + te.bind(sexpr.NameSymTypeWord, 2, metaString) + metaSet := func(args *sxpf.List) sxpf.Object { + var sb strings.Builder + for elem := te.getList(args.Tail()); elem != nil; elem = elem.Tail() { + sb.WriteByte(' ') + sb.WriteString(te.getString(elem).String()) + } + s := sb.String() + if len(s) > 0 { + s = s[1:] + } + a := make(attrs.Attributes, 2). + Set("name", te.getString(args).String()). + Set("content", s) + return te.transformMeta(a) + } + te.bind(sexpr.NameSymTypeIDSet, 2, metaSet) + te.bind(sexpr.NameSymTypeTagSet, 2, metaSet) + te.bind(sexpr.NameSymTypeWordSet, 2, metaSet) +} + +func (te *TransformEnv) bindBlocks() { + te.bind(sexpr.NameSymBlock, 0, listArgs) + te.bind(sexpr.NameSymPara, 0, func(args *sxpf.List) sxpf.Object { + for ; args != nil; args = args.Tail() { + lst, ok := sxpf.GetList(args.Car()) + if !ok || lst != nil { + break + } + } + return args.Cons(te.symP) + }) + te.bind(sexpr.NameSymHeading, 5, func(args *sxpf.List) sxpf.Object { + nLevel := te.getInt64(args) + if nLevel <= 0 { + te.err = fmt.Errorf("%v is a negative level", nLevel) + return sxpf.Nil() + } + level := strconv.FormatInt(nLevel+te.tr.headingOffset, 10) + + argAttr := args.Tail() + a := te.getAttributes(argAttr) + argFragment := argAttr.Tail().Tail() + if fragment := te.getString(argFragment).String(); fragment != "" { + a = a.Set("id", te.tr.unique+fragment) + } + + if result, ok := sxpf.GetList(argFragment.Tail().Car()); ok && result != nil { + if len(a) > 0 { + result = result.Cons(te.transformAttribute(a)) + } + return result.Cons(te.Make("h" + level)) + } + return sxpf.MakeList(te.Make("h"+level), sxpf.MakeString("")) + }) + te.bind(sexpr.NameSymThematic, 0, func(args *sxpf.List) sxpf.Object { + result := sxpf.Nil() + if args != nil { + if attrList := te.getList(args); attrList != nil { + result = result.Cons(te.transformAttribute(sexpr.GetAttributes(attrList))) + } + } + return result.Cons(te.Make("hr")) + }) + te.bind(sexpr.NameSymListOrdered, 0, te.makeListFn("ol")) + te.bind(sexpr.NameSymListUnordered, 0, te.makeListFn("ul")) + te.bind(sexpr.NameSymDescription, 0, func(args *sxpf.List) sxpf.Object { + if args == nil { + return sxpf.Nil() + } + items := sxpf.Nil().Cons(te.Make("dl")) + curItem := items + for elem := args; elem != nil; elem = elem.Tail() { + term := te.getList(elem) + curItem = curItem.AppendBang(term.Cons(te.Make("dt"))) + elem = elem.Tail() + if elem == nil { + break + } + ddBlock := te.getList(elem) + if ddBlock == nil { + break + } + for ddlst := ddBlock; ddlst != nil; ddlst = ddlst.Tail() { + dditem := te.getList(ddlst) + curItem = curItem.AppendBang(dditem.Cons(te.Make("dd"))) + } + } + return items + }) + + te.bind(sexpr.NameSymListQuote, 0, func(args *sxpf.List) sxpf.Object { + if args == nil { + return sxpf.Nil() + } + result := sxpf.Nil().Cons(te.Make("blockquote")) + currResult := result + for elem := args; elem != nil; elem = elem.Tail() { + if quote, ok := elem.Car().(*sxpf.List); ok { + currResult = currResult.AppendBang(quote.Cons(te.symP)) + } + } + return result + }) + + te.bind(sexpr.NameSymTable, 1, func(args *sxpf.List) sxpf.Object { + thead := sxpf.Nil() + if header := te.getList(args); header != nil { + thead = sxpf.Nil().Cons(te.transformTableRow(header)).Cons(te.Make("thead")) + } + + tbody := sxpf.Nil() + if argBody := args.Tail(); argBody != nil { + tbody = sxpf.Nil().Cons(te.Make("tbody")) + curBody := tbody + for row := argBody; row != nil; row = row.Tail() { + curBody = curBody.AppendBang(te.transformTableRow(te.getList(row))) + } + } + + table := sxpf.Nil() + if tbody != nil { + table = table.Cons(tbody) + } + if thead != nil { + table = table.Cons(thead) + } + if table == nil { + return sxpf.Nil() + } + return table.Cons(te.Make("table")) + }) + te.bind(sexpr.NameSymCell, 0, te.makeCellFn("")) + te.bind(sexpr.NameSymCellCenter, 0, te.makeCellFn("center")) + te.bind(sexpr.NameSymCellLeft, 0, te.makeCellFn("left")) + te.bind(sexpr.NameSymCellRight, 0, te.makeCellFn("right")) + + te.bind(sexpr.NameSymRegionBlock, 2, te.makeRegionFn(te.Make("div"), true)) + te.bind(sexpr.NameSymRegionQuote, 2, te.makeRegionFn(te.Make("blockquote"), false)) + te.bind(sexpr.NameSymRegionVerse, 2, te.makeRegionFn(te.Make("div"), false)) + + te.bind(sexpr.NameSymVerbatimComment, 1, func(args *sxpf.List) sxpf.Object { + if te.getAttributes(args).HasDefault() { + if s := te.getString(args.Tail()); s != "" { + t := sxpf.MakeString(s.String()) + return sxpf.Nil().Cons(t).Cons(te.Make(sxhtml.NameSymBlockComment)) + } + } + return nil + }) + + te.bind(sexpr.NameSymVerbatimEval, 2, func(args *sxpf.List) sxpf.Object { + return te.transformVerbatim(te.getAttributes(args).AddClass("zs-eval"), te.getString(args.Tail())) + }) + te.bind(sexpr.NameSymVerbatimHTML, 2, te.transformHTML) + te.bind(sexpr.NameSymVerbatimMath, 2, func(args *sxpf.List) sxpf.Object { + return te.transformVerbatim(te.getAttributes(args).AddClass("zs-math"), te.getString(args.Tail())) + }) + te.bind(sexpr.NameSymVerbatimProg, 2, func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + content := te.getString(args.Tail()) + if a.HasDefault() { + content = sxpf.MakeString(visibleReplacer.Replace(content.String())) + } + return te.transformVerbatim(a, content) + }) + te.bind(sexpr.NameSymVerbatimZettel, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil() }) + + te.bind(sexpr.NameSymBLOB, 3, func(args *sxpf.List) sxpf.Object { + argSyntax := args.Tail() + return te.transformBLOB(te.getList(args), te.getString(argSyntax), te.getString(argSyntax.Tail())) + }) + + te.bind(sexpr.NameSymTransclude, 2, func(args *sxpf.List) sxpf.Object { + ref, ok := args.Tail().Car().(*sxpf.List) + if !ok { + return sxpf.Nil() + } + refKind := ref.Car() + if sxpf.IsNil(refKind) { + return sxpf.Nil() + } + if refValue := te.getString(ref.Tail()); refValue != "" { + if te.astSF.MustMake(sexpr.NameSymRefStateExternal).IsEqual(refKind) { + a := te.getAttributes(args).Set("src", refValue.String()).AddClass("external") + return sxpf.Nil().Cons(sxpf.Nil().Cons(te.transformAttribute(a)).Cons(te.Make("img"))).Cons(te.symP) + } + return sxpf.MakeList( + te.Make(sxhtml.NameSymInlineComment), + sxpf.MakeString("transclude"), + refKind, + sxpf.MakeString("->"), + refValue, + ) + } + return args + }) +} + +func (te *TransformEnv) makeListFn(tag string) transformFn { + sym := te.Make(tag) + return func(args *sxpf.List) sxpf.Object { + result := sxpf.Nil().Cons(sym) + last := result + for elem := args; elem != nil; elem = elem.Tail() { + item := sxpf.Nil().Cons(te.Make("li")) + if res, ok := elem.Car().(*sxpf.List); ok { + item.ExtendBang(res) + } + last = last.AppendBang(item) + } + return result + } +} +func (te *TransformEnv) transformTableRow(cells *sxpf.List) *sxpf.List { + row := sxpf.Nil().Cons(te.Make("tr")) + if cells == nil { + return sxpf.Nil() + } + curRow := row + for cell := cells; cell != nil; cell = cell.Tail() { + curRow = curRow.AppendBang(cell.Car()) + } + return row +} + +func (te *TransformEnv) makeCellFn(align string) transformFn { + return func(args *sxpf.List) sxpf.Object { + tdata := args + if align != "" { + tdata = tdata.Cons(te.transformAttribute(attrs.Attributes{"class": align})) + } + return tdata.Cons(te.Make("td")) + } +} + +func (te *TransformEnv) makeRegionFn(sym *sxpf.Symbol, genericToClass bool) transformFn { + return func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + if genericToClass { + if val, found := a.Get(""); found { + a = a.Remove("").AddClass(val) + } + } + result := sxpf.Nil() + if len(a) > 0 { + result = result.Cons(te.transformAttribute(a)) + } + result = result.Cons(sym) + currResult := result.Last() + blockArg := args.Tail() + if region, ok := blockArg.Car().(*sxpf.List); ok { + currResult = currResult.ExtendBang(region) + } + if citeArg := blockArg.Tail(); citeArg != nil { + if cite, ok := citeArg.Car().(*sxpf.List); ok && cite != nil { + currResult.AppendBang(cite.Cons(te.Make("cite"))) + } + } + return result + } +} + +func (te *TransformEnv) transformVerbatim(a attrs.Attributes, s sxpf.String) sxpf.Object { + a = setProgLang(a) + code := sxpf.Nil().Cons(s) + if al := te.transformAttribute(a); al != nil { + code = code.Cons(al) + } + code = code.Cons(te.Make("code")) + return sxpf.Nil().Cons(code).Cons(te.Make("pre")) +} + +func (te *TransformEnv) bindInlines() { + te.bind(sexpr.NameSymInline, 0, listArgs) + te.bind(sexpr.NameSymText, 1, func(args *sxpf.List) sxpf.Object { return te.getString(args) }) + te.bind(sexpr.NameSymSpace, 0, func(args *sxpf.List) sxpf.Object { + if args.IsNil() { + return sxpf.MakeString(" ") + } + return te.getString(args) + }) + te.bind(sexpr.NameSymSoft, 0, func(*sxpf.List) sxpf.Object { return sxpf.MakeString(" ") }) + brSym := te.Make("br") + te.bind(sexpr.NameSymHard, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil().Cons(brSym) }) + + te.bind(sexpr.NameSymLinkInvalid, 2, func(args *sxpf.List) sxpf.Object { + // a := te.getAttributes(args) + refArg := args.Tail() + inline := refArg.Tail() + if inline == nil { + inline = sxpf.Nil().Cons(refArg.Car()) + } + return inline.Cons(te.symSpan) + }) + transformHREF := func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + refValue := te.getString(args.Tail()) + return te.transformLink(a.Set("href", refValue.String()), refValue, args.Tail().Tail()) + } + te.bind(sexpr.NameSymLinkZettel, 2, transformHREF) + te.bind(sexpr.NameSymLinkSelf, 2, transformHREF) + te.bind(sexpr.NameSymLinkFound, 2, transformHREF) + te.bind(sexpr.NameSymLinkBroken, 2, func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + refValue := te.getString(args.Tail()) + return te.transformLink(a.AddClass("broken"), refValue, args.Tail().Tail()) + }) + te.bind(sexpr.NameSymLinkHosted, 2, transformHREF) + te.bind(sexpr.NameSymLinkBased, 2, transformHREF) + te.bind(sexpr.NameSymLinkQuery, 2, func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + refValue := te.getString(args.Tail()) + query := "?" + api.QueryKeyQuery + "=" + url.QueryEscape(refValue.String()) + return te.transformLink(a.Set("href", query), refValue, args.Tail().Tail()) + }) + te.bind(sexpr.NameSymLinkExternal, 2, func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + refValue := te.getString(args.Tail()) + return te.transformLink(a.Set("href", refValue.String()).AddClass("external"), refValue, args.Tail().Tail()) + }) + + te.bind(sexpr.NameSymEmbed, 3, func(args *sxpf.List) sxpf.Object { + argRef := args.Tail() + ref := te.getList(argRef) + syntax := te.getString(argRef.Tail()) + if syntax == api.ValueSyntaxSVG { + embedAttr := sxpf.MakeList( + te.symAttr, + sxpf.Cons(te.Make("type"), sxpf.MakeString("image/svg+xml")), + sxpf.Cons(te.Make("src"), sxpf.MakeString("/"+te.getString(ref.Tail()).String()+".svg")), + ) + return sxpf.MakeList( + te.Make("figure"), + sxpf.MakeList( + te.Make("embed"), + embedAttr, + ), + ) + } + a := te.getAttributes(args) + a = a.Set("src", string(te.getString(ref.Tail()))) + var sb strings.Builder + te.flattenText(&sb, ref.Tail().Tail().Tail()) + if d := sb.String(); d != "" { + a = a.Set("alt", d) + } + return sxpf.MakeList(te.Make("img"), te.transformAttribute(a)) + }) + te.bind(sexpr.NameSymEmbedBLOB, 3, func(args *sxpf.List) sxpf.Object { + argSyntax := args.Tail() + a, syntax, data := te.getAttributes(args), te.getString(argSyntax), te.getString(argSyntax.Tail()) + summary, _ := a.Get(api.KeySummary) + return te.transformBLOB( + sxpf.MakeList(te.astSF.MustMake(sexpr.NameSymInline), sxpf.MakeString(summary)), + syntax, + data, + ) + }) + + te.bind(sexpr.NameSymCite, 2, func(args *sxpf.List) sxpf.Object { + result := sxpf.Nil() + argKey := args.Tail() + if key := te.getString(argKey); key != "" { + if text := argKey.Tail(); text != nil { + result = text.Cons(sxpf.MakeString(", ")) + } + result = result.Cons(key) + } + if a := te.getAttributes(args); len(a) > 0 { + result = result.Cons(te.transformAttribute(a)) + } + if result == nil { + return nil + } + return result.Cons(te.symSpan) + }) + + te.bind(sexpr.NameSymMark, 3, func(args *sxpf.List) sxpf.Object { + argFragment := args.Tail().Tail() + result := argFragment.Tail() + if !te.tr.noLinks { + if fragment := te.getString(argFragment); fragment != "" { + a := attrs.Attributes{"id": fragment.String() + te.tr.unique} + return result.Cons(te.transformAttribute(a)).Cons(te.symA) + } + } + return result.Cons(te.symSpan) + }) + + te.bind(sexpr.NameSymEndnote, 1, func(args *sxpf.List) sxpf.Object { + attrPlist := sxpf.Nil() + if a := te.getAttributes(args); len(a) > 0 { + if attrs := te.transformAttribute(a); attrs != nil { + attrPlist = attrs.Tail() + } + } + + text, ok := args.Tail().Car().(*sxpf.List) + if !ok { + return sxpf.Nil() + } + te.tr.endnotes = append(te.tr.endnotes, endnoteInfo{noteAST: text, noteHx: nil, attrs: attrPlist}) + noteNum := strconv.Itoa(len(te.tr.endnotes)) + noteID := te.tr.unique + noteNum + hrefAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("role"), sxpf.MakeString("doc-noteref"))). + Cons(sxpf.Cons(te.Make("href"), sxpf.MakeString("#fn:"+noteID))). + Cons(sxpf.Cons(te.tr.symClass, sxpf.MakeString("zs-noteref"))). + Cons(te.symAttr) + href := sxpf.Nil().Cons(sxpf.MakeString(noteNum)).Cons(hrefAttr).Cons(te.symA) + supAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("id"), sxpf.MakeString("fnref:"+noteID))).Cons(te.symAttr) + return sxpf.Nil().Cons(href).Cons(supAttr).Cons(te.Make("sup")) + }) + + te.bind(sexpr.NameSymFormatDelete, 1, te.makeFormatFn("del")) + te.bind(sexpr.NameSymFormatEmph, 1, te.makeFormatFn("em")) + te.bind(sexpr.NameSymFormatInsert, 1, te.makeFormatFn("ins")) + te.bind(sexpr.NameSymFormatQuote, 1, te.transformQuote) + te.bind(sexpr.NameSymFormatSpan, 1, te.makeFormatFn("span")) + te.bind(sexpr.NameSymFormatStrong, 1, te.makeFormatFn("strong")) + te.bind(sexpr.NameSymFormatSub, 1, te.makeFormatFn("sub")) + te.bind(sexpr.NameSymFormatSuper, 1, te.makeFormatFn("sup")) + + te.bind(sexpr.NameSymLiteralComment, 1, func(args *sxpf.List) sxpf.Object { + if te.getAttributes(args).HasDefault() { + if s := te.getString(args.Tail()); s != "" { + return sxpf.Nil().Cons(s).Cons(te.Make(sxhtml.NameSymInlineComment)) + } + } + return sxpf.Nil() + }) + te.bind(sexpr.NameSymLiteralHTML, 2, te.transformHTML) + kbdSym := te.Make("kbd") + te.bind(sexpr.NameSymLiteralInput, 2, func(args *sxpf.List) sxpf.Object { + return te.transformLiteral(args, nil, kbdSym) + }) + codeSym := te.Make("code") + te.bind(sexpr.NameSymLiteralMath, 2, func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args).AddClass("zs-math") + return te.transformLiteral(args, a, codeSym) + }) + sampSym := te.Make("samp") + te.bind(sexpr.NameSymLiteralOutput, 2, func(args *sxpf.List) sxpf.Object { + return te.transformLiteral(args, nil, sampSym) + }) + te.bind(sexpr.NameSymLiteralProg, 2, func(args *sxpf.List) sxpf.Object { + return te.transformLiteral(args, nil, codeSym) + }) + + te.bind(sexpr.NameSymLiteralZettel, 0, func(*sxpf.List) sxpf.Object { return sxpf.Nil() }) +} + +func (te *TransformEnv) makeFormatFn(tag string) transformFn { + sym := te.Make(tag) + return func(args *sxpf.List) sxpf.Object { + a := te.getAttributes(args) + if val, found := a.Get(""); found { + a = a.Remove("").AddClass(val) + } + res := args.Tail() + if len(a) > 0 { + res = res.Cons(te.transformAttribute(a)) + } + return res.Cons(sym) + } +} +func (te *TransformEnv) transformQuote(args *sxpf.List) sxpf.Object { + const langAttr = "lang" + a := te.getAttributes(args) + langVal, found := a.Get(langAttr) + if found { + a = a.Remove(langAttr) + } + if val, found2 := a.Get(""); found2 { + a = a.Remove("").AddClass(val) + } + res := args.Tail() + if len(a) > 0 { + res = res.Cons(te.transformAttribute(a)) + } + res = res.Cons(te.Make("q")) + if found { + res = sxpf.Nil().Cons(res).Cons(te.transformAttribute(attrs.Attributes{}.Set(langAttr, langVal))).Cons(te.symSpan) + } + return res +} + +var visibleReplacer = strings.NewReplacer(" ", "\u2423") + +func (te *TransformEnv) transformLiteral(args *sxpf.List, a attrs.Attributes, sym *sxpf.Symbol) sxpf.Object { + if a == nil { + a = te.getAttributes(args) + } + a = setProgLang(a) + literal := te.getString(args.Tail()).String() + if a.HasDefault() { + a = a.RemoveDefault() + literal = visibleReplacer.Replace(literal) + } + res := sxpf.Nil().Cons(sxpf.MakeString(literal)) + if len(a) > 0 { + res = res.Cons(te.transformAttribute(a)) + } + return res.Cons(sym) +} + +func setProgLang(a attrs.Attributes) attrs.Attributes { + if val, found := a.Get(""); found { + a = a.AddClass("language-" + val).Remove("") + } + return a +} + +func (te *TransformEnv) transformHTML(args *sxpf.List) sxpf.Object { + if s := te.getString(args.Tail()); s != "" && IsSafe(s.String()) { + return sxpf.Nil().Cons(s).Cons(te.symNoEscape) + } + return nil +} + +func (te *TransformEnv) transformBLOB(description *sxpf.List, syntax, data sxpf.String) sxpf.Object { + if data == "" { + return sxpf.Nil() + } + switch syntax { + case "": + return sxpf.Nil() + case api.ValueSyntaxSVG: + return sxpf.Nil().Cons(sxpf.Nil().Cons(data).Cons(te.symNoEscape)).Cons(te.symP) + default: + imgAttr := sxpf.Nil().Cons(sxpf.Cons(te.Make("src"), sxpf.MakeString("data:image/"+syntax.String()+";base64,"+data.String()))) + var sb strings.Builder + te.flattenText(&sb, description) + if d := sb.String(); d != "" { + imgAttr = imgAttr.Cons(sxpf.Cons(te.Make("alt"), sxpf.MakeString(d))) + } + return sxpf.Nil().Cons(sxpf.Nil().Cons(imgAttr.Cons(te.symAttr)).Cons(te.Make("img"))).Cons(te.symP) + } +} + +func (te *TransformEnv) flattenText(sb *strings.Builder, lst *sxpf.List) { + for elem := lst; elem != nil; elem = elem.Tail() { + switch obj := elem.Car().(type) { + case sxpf.String: + sb.WriteString(obj.String()) + case *sxpf.List: + te.flattenText(sb, obj) + } + } +} + +type transformFn func(*sxpf.List) sxpf.Object + +func (te *TransformEnv) bind(name string, minArity int, fn transformFn) { + te.astEnv.Bind(te.astSF.MustMake(name), eval.MakeBuiltin(name, func(_ sxpf.Environment, args *sxpf.List) (sxpf.Object, error) { + if nArgs := args.Length(); nArgs < minArity { + return sxpf.Nil(), fmt.Errorf("not enough arguments (%d) for form %v (%d)", nArgs, name, minArity) + } + res := fn(args) + return res, te.err + })) +} + +func (te *TransformEnv) Rebind(name string, fn func(sxpf.Environment, *sxpf.List, sxpf.Callable) sxpf.Object) { + sym := te.astSF.MustMake(name) + obj, found := te.astEnv.Lookup(sym) + if !found { + panic(sym.String()) + } + preFn, ok := obj.(sxpf.Callable) + if !ok { + panic(sym.String()) + } + te.astEnv.Bind(sym, eval.MakeBuiltin(name, func(env sxpf.Environment, args *sxpf.List) (sxpf.Object, error) { + res := fn(env, args, preFn) + return res, te.err + })) +} + +func (te *TransformEnv) Make(name string) *sxpf.Symbol { return te.tr.Make(name) } +func (te *TransformEnv) getString(lst *sxpf.List) sxpf.String { + if te.err != nil { + return "" + } + val := lst.Car() + if s, ok := val.(sxpf.String); ok { + return s + } + te.err = fmt.Errorf("%v/%T is not a string", val, val) + return "" +} +func (te *TransformEnv) getInt64(lst *sxpf.List) int64 { + if te.err != nil { + return -1017 + } + val := lst.Car() + if num, ok := val.(*sxpf.Number); ok { + return num.GetInt64() + } + te.err = fmt.Errorf("%v/%T is not a number", val, val) + return -1017 +} +func (te *TransformEnv) getList(lst *sxpf.List) *sxpf.List { + if te.err == nil { + val := lst.Car() + if res, ok := val.(*sxpf.List); ok { + return res + } + te.err = fmt.Errorf("%v/%T is not a list", val, val) + } + return sxpf.Nil() +} +func (te *TransformEnv) getAttributes(args *sxpf.List) attrs.Attributes { + return sexpr.GetAttributes(te.getList(args)) +} + +func (te *TransformEnv) transformLink(a attrs.Attributes, refValue sxpf.String, inline *sxpf.List) sxpf.Object { + result := inline + if inline.IsNil() { + result = sxpf.Nil().Cons(refValue) + } + if te.tr.noLinks { + return result.Cons(te.symSpan) + } + return result.Cons(te.transformAttribute(a)).Cons(te.symA) +} + +func (te *TransformEnv) transformAttribute(a attrs.Attributes) *sxpf.List { + return te.tr.TransformAttrbute(a) +} + +func (te *TransformEnv) transformMeta(a attrs.Attributes) *sxpf.List { + return te.tr.TransformMeta(a) +} + +var unsafeSnippets = []string{ + "Change Log + + +

Changes for Version 0.12.0 (pending)

+ + +

Changes for Version 0.11.0 (2023-03-27)

+ * Remove all zjson related declarations. + * Generate HTML via SxHTML, not manually and direct. + + +

Changes for Version 0.10.0 (2023-01-24)

+ * Add query key parseonly and two encoding names (plain, json) to + allow to merge various API endpoints into /z + * Client method do not use endpoints /j, /m, /q, /p, /v any more. + They are merged into endpoint /z. Use this client only with + Zettelstore v0.10.0 or better. + (breaking)

Changes for Version 0.9.0 (2022-12-12)

* Rename api.QueryKeyDepth to api.QueryKeyCost * Update encode / syntax names Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -1,12 +1,12 @@ Home This repository contains Go client software to access [https://zettelstore.de|Zettelstore] via its API. -

Latest Release: 0.9.0 (2022-12-12)

- * [./changes.wiki#0_9|Change summary] - * [/timeline?p=v0.9.0&bt=v0.8.0&y=ci|Check-ins for version 0.9.0], - [/vdiff?to=v0.9.0&from=v0.8.0|content diff] - * [/timeline?df=v0.9.0&y=ci|Check-ins derived from the 0.9.0 release], - [/vdiff?from=v0.9.0&to=trunk|content diff] +

Latest Release: 0.11.0 (2023-03-27)

+ * [./changes.wiki#0_11|Change summary] + * [/timeline?p=v0.11.0&bt=v0.10.0&y=ci|Check-ins for version 0.11.0], + [/vdiff?to=v0.11.0&from=v0.10.0|content diff] + * [/timeline?df=v0.11.0&y=ci|Check-ins derived from the 0.11.0 release], + [/vdiff?from=v0.11.0&to=trunk|content diff] * [/timeline?t=release|Timeline of all past releases] DELETED zjson/const.go Index: zjson/const.go ================================================================== --- zjson/const.go +++ zjson/const.go @@ -1,99 +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 zjson - -// Values for Zettelmarkup element object names -const ( - NameType = "" - NameAttribute = "a" - NameBLOB = "j" - NameBinary = "o" - NameBlock = "b" - NameDescrList = "d" - NameDescription = "e" - NameInline = "i" - NameList = "c" - NameNumeric = "n" - NameSet = "y" - NameString = "s" - NameString2 = "q" - NameString3 = "v" - NameTable = "p" -) - -// Values to specify the Zettelmarkup element type -const ( - TypeBLOB = "BLOB" - TypeBlock = "Block" - TypeBreakHard = "Hard" - TypeBreakSoft = "Soft" - TypeBreakThematic = "Thematic" - TypeCitation = "Cite" - TypeDescrList = "Description" - TypeEmbed = "Embed" - TypeEmbedBLOB = "EmbedBLOB" - TypeExcerpt = "Excerpt" - TypeFootnote = "Footnote" - TypeFormatDelete = "Delete" - TypeFormatEmph = "Emph" - TypeFormatInsert = "Insert" - TypeFormatQuote = "Quote" - TypeFormatSpan = "Span" - TypeFormatStrong = "Strong" - TypeFormatSub = "Sub" - TypeFormatSuper = "Super" - TypeHeading = "Heading" - TypeLink = "Link" - TypeListBullet = "Bullet" - TypeListOrdered = "Ordered" - TypeListQuotation = "Quotation" - TypeLiteralCode = "Code" - TypeLiteralComment = "Comment" - TypeLiteralHTML = "HTML" - TypeLiteralInput = "Input" - TypeLiteralOutput = "Output" - TypeLiteralMath = "Math" - TypeLiteralZettel = "Zettel" - TypeMark = "Mark" - TypeParagraph = "Para" - TypePoem = "Poem" - TypeSpace = "Space" - TypeTable = "Table" - TypeText = "Text" - TypeTransclude = "Transclude" - TypeVerbatimCode = "CodeBlock" - TypeVerbatimComment = "CommentBlock" - TypeVerbatimEval = "EvalBlock" - TypeVerbatimHTML = "HTMLBlock" - TypeVerbatimMath = "MathBlock" - TypeVerbatimZettel = "ZettelBlock" -) - -// Values to specify the state of a reference -const ( - RefStateBased = "based" - RefStateBroken = "broken" - RefStateExternal = "external" - RefStateFound = "found" - RefStateHosted = "local" - RefStateInvalid = "invalid" - RefStateQuery = "query" - RefStateSelf = "self" - RefStateZettel = "zettel" -) - -// Values for table cell alignment -const ( - AlignDefault = "" - AlignLeft = "<" - AlignCenter = ":" - AlignRight = ">" -) DELETED zjson/meta.go Index: zjson/meta.go ================================================================== --- zjson/meta.go +++ zjson/meta.go @@ -1,65 +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 zjson - -type Meta map[string]MetaValue -type MetaValue struct { - Type string - Key string - Value Value -} - -func MakeMeta(val Value) Meta { - obj := MakeObject(val) - if len(obj) == 0 { - return nil - } - result := make(Meta, len(obj)) - for k, v := range obj { - mvObj := MakeObject(v) - if len(mvObj) == 0 { - continue - } - mv := makeMetaValue(mvObj) - if mv.Type != "" { - result[k] = mv - } - } - return result -} -func makeMetaValue(mvObj Object) MetaValue { - mv := MetaValue{} - for n, val := range mvObj { - if n == NameType { - if t, ok := val.(string); ok { - mv.Type = t - } - } else { - mv.Key = n - mv.Value = val - } - } - return mv -} - -func (m Meta) GetArray(key string) Array { - if v, found := m[key]; found { - return MakeArray(v.Value) - } - return nil -} - -func (m Meta) GetString(key string) string { - if v, found := m[key]; found { - return MakeString(v.Value) - } - return "" -} DELETED zjson/zjson.go Index: zjson/zjson.go ================================================================== --- zjson/zjson.go +++ zjson/zjson.go @@ -1,326 +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 zjson provides types, constants and function to work with the ZJSON -// encoding of zettel. -package zjson - -import ( - "encoding/json" - "fmt" - "io" - - "zettelstore.de/c/attrs" -) - -// Value is the gerneric JSON value. -type Value = interface{} - -// Array represents a JSON array. -type Array = []Value - -// Object represents a JSON object. -type Object = map[string]Value - -// Decode some JSON data and return the tree. -func Decode(data io.Reader) (Value, error) { - var result interface{} - dec := json.NewDecoder(data) - dec.UseNumber() - err := dec.Decode(&result) - if err != nil { - return nil, err - } - return result, nil -} - -// GetMetaContent returns the metadata and the content of a zettel ZJSON. -func GetMetaContent(zjZettel Value) (Meta, Array) { - if zettel, ok := zjZettel.(Object); ok { - meta := MakeMeta(zettel["meta"]) - content := MakeArray(zettel["content"]) - return meta, content - } - return nil, nil -} - -// Visitor provides functionality when a Value is traversed. -type Visitor interface { - BlockArray(a Array, pos int) CloseFunc - InlineArray(a Array, pos int) CloseFunc - ItemArray(a Array, pos int) CloseFunc - - BlockObject(t string, obj Object, pos int) (bool, CloseFunc) - InlineObject(t string, obj Object, pos int) (bool, CloseFunc) - - Unexpected(val Value, pos int, exp string) -} - -// CloseFunc is a function that executes after a ZJSON element is visited. -type CloseFunc func() - -// WalkBlock traverses a block array. -func WalkBlock(v Visitor, a Array, pos int) { - ef := v.BlockArray(a, pos) - for i, elem := range a { - WalkBlockObject(v, elem, i) - } - if ef != nil { - ef() - } -} - -// WalkInline traverses an inline array. -func WalkInline(v Visitor, a Array, pos int) { - ef := v.InlineArray(a, pos) - for i, elem := range a { - WalkInlineObject(v, elem, i) - } - if ef != nil { - ef() - } -} - -// WalkBlockObject traverses a value as a JSON object in a block array. -func WalkBlockObject(v Visitor, val Value, pos int) { walkObject(v, val, pos, v.BlockObject) } - -// WalkInlineObject traverses a value as a JSON object in an inline array. -func WalkInlineObject(v Visitor, val Value, pos int) { walkObject(v, val, pos, v.InlineObject) } - -func walkObject(v Visitor, val Value, pos int, objFunc func(string, Object, int) (bool, CloseFunc)) { - obj, ok := val.(Object) - if !ok { - v.Unexpected(val, pos, "Object") - return - } - - tVal, ok := obj[NameType] - if !ok { - v.Unexpected(obj, pos, "Object type") - return - } - t, ok := tVal.(string) - if !ok { - v.Unexpected(obj, pos, "Object type value") - return - } - - doChilds, ef := objFunc(t, obj, pos) - if doChilds { - WalkBlockChild(v, obj, pos) - WalkItemChild(v, obj, pos) - WalkInlineChild(v, obj, pos) - walkDescriptionList(v, obj) - walkTable(v, obj, pos) - } - if ef != nil { - ef() - } -} - -// WalkInlineChild traverses the array found at the name NameInline ('i'). -func WalkInlineChild(v Visitor, obj Object, pos int) { - if iVal, ok := obj[NameInline]; ok { - if il, ok := iVal.(Array); ok { - WalkInline(v, il, 0) - } else { - v.Unexpected(iVal, pos, "Inline array") - } - } -} - -// WalkBlockChild traverses the array found at the name NameBlock ('b'). -func WalkBlockChild(v Visitor, obj Object, pos int) { - if bVal, ok := obj[NameBlock]; ok { - if bl, ok := bVal.(Array); ok { - WalkBlock(v, bl, 0) - } else { - v.Unexpected(bVal, pos, "Block array") - } - } -} - -// WalkItemChild traverses the arrays found at the name NameList ('c'). -func WalkItemChild(v Visitor, obj Object, pos int) { - iVal, ok := obj[NameList] - if !ok { - return - } - it, ok := iVal.(Array) - if !ok { - v.Unexpected(iVal, pos, "Item array") - return - } - for i, l := range it { - ef := v.ItemArray(it, i) - if bl, ok := l.(Array); ok { - WalkBlock(v, bl, i) - } else { - v.Unexpected(l, i, "Item block array") - } - if ef != nil { - ef() - } - } -} - -func walkDescriptionList(v Visitor, obj Object) { - descrs := GetArray(obj, NameDescrList) - if len(descrs) == 0 { - return - } - for i, elem := range descrs { - dObj := MakeObject(elem) - if dObj == nil { - continue - } - WalkInlineChild(v, dObj, i) - descr := GetArray(dObj, NameDescription) - if len(descr) == 0 { - continue - } - for j, ddv := range descr { - dd := MakeArray(ddv) - if len(dd) == 0 { - continue - } - WalkBlock(v, dd, j) - } - } -} - -func walkTable(v Visitor, obj Object, pos int) { - tdata := GetArray(obj, NameTable) - if len(tdata) == 0 { - return - } - if len(tdata) != 2 { - v.Unexpected(obj, pos, "Table header/rows") - return - } - walkRow(v, MakeArray(tdata[0])) - if bArray := MakeArray(tdata[1]); len(bArray) > 0 { - for _, row := range bArray { - if rArray := MakeArray(row); rArray != nil { - walkRow(v, rArray) - } - } - } -} -func walkRow(v Visitor, row Array) { - if len(row) > 0 { - for _, cell := range row { - if cObj := MakeObject(cell); cObj != nil { - WalkInlineChild(v, cObj, 0) - } - } - } -} - -// GetArray returns the array-typed value under the given name. -func GetArray(obj Object, name string) Array { - if v, ok := obj[name]; ok && v != nil { - return MakeArray(v) - } - return nil -} - -// GetNumber returns the numeric value at NameNumberic ('n') as a string. -func GetNumber(obj Object) string { - if v, ok := obj[NameNumeric]; ok { - if n, ok := v.(json.Number); ok { - return string(n) - } - if f, ok := v.(float64); ok { - return fmt.Sprint(f) - } - } - return "" -} - -// GetString returns the string value at the given name. -func GetString(obj Object, name string) string { - if v, ok := obj[name]; ok { - return MakeString(v) - } - return "" -} - -// MakeArray returns the given value as a JSON array. -func MakeArray(val Value) Array { - if a, ok := val.(Array); ok { - return a - } - return nil -} - -// MakeString returns the given value as a string. -func MakeString(val Value) string { - if s, ok := val.(string); ok { - return s - } - return "" -} - -// GetAttribute returns a copy of the attributes of the given object. -func GetAttributes(obj Object) attrs.Attributes { - a := GetObject(obj, NameAttribute) - if len(a) == 0 { - return nil - } - result := make(attrs.Attributes, len(a)) - for n, v := range a { - if val, ok := v.(string); ok { - result[n] = val - } - } - return result -} - -// SetAttributes copies the attributes to the given object. -func SetAttributes(obj Object, a attrs.Attributes) { - if len(a) == 0 { - delete(obj, NameAttribute) - } - val := make(Object) - for k, v := range a { - val[k] = v - } - obj[NameAttribute] = val -} - -// GetObject returns the object found at the given object with the given name. -func GetObject(obj Object, name string) Object { - if v, ok := obj[name]; ok && v != nil { - return MakeObject(v) - } - return nil -} - -// MakeObject returns the given value as a JSON object. -func MakeObject(val Value) Object { - if o, ok := val.(Object); ok { - return o - } - return nil -} - -// GetParagraphInline return the inline list of the first paragraph (or nil if there is no such thing) -func GetParagraphInline(a Array) Array { - if len(a) != 1 { - return nil - } - if o := MakeObject(a[0]); o != nil { - if GetString(o, NameType) == TypeParagraph { - return GetArray(o, NameInline) - } - } - return nil -}