Index: README.md ================================================================== --- README.md +++ README.md @@ -21,6 +21,6 @@ The software, including the manual, is licensed under the [European Union Public License 1.2 (or later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk). -[Stay tuned](https://twitter.com/search?q=%40t73fde%20zettelstore&f=live) … +[Stay tuned](https://mastodon.social/tags/Zettelstore) … Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.12 +0.13.0 Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -8,11 +8,11 @@ // under this license. //----------------------------------------------------------------------------- package ast -import "zettelstore.de/c/attrs" +import "zettelstore.de/client.fossil/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -11,11 +11,11 @@ package ast import ( "unicode/utf8" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -11,11 +11,11 @@ package ast_test import ( "testing" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ ADDED auth/impl/digest.go Index: auth/impl/digest.go ================================================================== --- /dev/null +++ auth/impl/digest.go @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 impl + +import ( + "bytes" + "crypto" + "crypto/hmac" + "encoding/base64" + + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxreader" +) + +var encoding = base64.RawURLEncoding + +const digestAlg = crypto.SHA384 + +func sign(claim sx.Object, secret []byte) ([]byte, error) { + var buf bytes.Buffer + sx.Print(&buf, claim) + token := make([]byte, encoding.EncodedLen(buf.Len())) + encoding.Encode(token, buf.Bytes()) + + digest := hmac.New(digestAlg.New, secret) + _, err := digest.Write(buf.Bytes()) + if err != nil { + return nil, err + } + dig := digest.Sum(nil) + encDig := make([]byte, encoding.EncodedLen(len(dig))) + encoding.Encode(encDig, dig) + + token = append(token, '.') + token = append(token, encDig...) + return token, nil +} + +func check(token []byte, secret []byte) (sx.Object, error) { + i := bytes.IndexByte(token, '.') + if i <= 0 || 1024 < i { + return nil, ErrMalformedToken + } + buf := make([]byte, len(token)) + n, err := encoding.Decode(buf, token[:i]) + if err != nil { + return nil, err + } + rdr := sxreader.MakeReader(bytes.NewReader(buf[:n])) + obj, err := rdr.Read() + if err != nil { + return nil, err + } + + var objBuf bytes.Buffer + _, err = sx.Print(&objBuf, obj) + if err != nil { + return nil, err + } + + digest := hmac.New(digestAlg.New, secret) + _, err = digest.Write(objBuf.Bytes()) + if err != nil { + return nil, err + } + + n, err = encoding.Decode(buf, token[i+1:]) + if err != nil { + return nil, err + } + if !hmac.Equal(buf[:n], digest.Sum(nil)) { + return nil, ErrMalformedToken + } + return obj, nil +} Index: auth/impl/impl.go ================================================================== --- auth/impl/impl.go +++ auth/impl/impl.go @@ -15,13 +15,13 @@ "errors" "hash/fnv" "io" "time" - "github.com/pascaldekloe/jwt" - - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/sexp" + "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" @@ -65,11 +65,12 @@ } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } -const reqHash = jwt.HS512 +// ErrMalformedToken signals a broken token. +var ErrMalformedToken = errors.New("auth: malformed token") // ErrNoIdent signals that the 'ident' key is missing. var ErrNoIdent = errors.New("auth: missing ident") // ErrOtherKind signals that the token was defined for another token kind. @@ -84,68 +85,66 @@ if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) - claims := jwt.Claims{ - Registered: jwt.Registered{ - Subject: subject, - Expires: jwt.NewNumericTime(now.Add(d)), - Issued: jwt.NewNumericTime(now), - }, - Set: map[string]interface{}{ - "zid": ident.Zid.String(), - "_tk": int(kind), - }, - } - token, err := claims.HMACSign(reqHash, a.secret) - if err != nil { - return nil, err - } - return token, nil + sClaim := sx.MakeList( + sx.Int64(kind), + sx.MakeString(subject), + sx.Int64(now.Unix()), + sx.Int64(now.Add(d).Unix()), + sx.Int64(ident.Zid), + ) + return sign(sClaim, a.secret) } // ErrTokenExpired signals an exired token var ErrTokenExpired = errors.New("auth: token expired") // CheckToken checks the validity of the token and returns relevant data. -func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) { - h, err := jwt.NewHMAC(reqHash, a.secret) - if err != nil { - return auth.TokenData{}, err - } - claims, err := h.Check(token) - if err != nil { - return auth.TokenData{}, err - } - now := time.Now().Round(time.Second) - expires := claims.Expires.Time() - if expires.Before(now) { - return auth.TokenData{}, ErrTokenExpired - } - ident := claims.Subject +func (a *myAuth) CheckToken(tok []byte, k auth.TokenKind) (auth.TokenData, error) { + var tokenData auth.TokenData + + obj, err := check(tok, a.secret) + if err != nil { + return tokenData, err + } + + tokenData.Token = tok + err = setupTokenData(obj, k, &tokenData) + return tokenData, err +} + +func setupTokenData(obj sx.Object, k auth.TokenKind, tokenData *auth.TokenData) error { + vals, err := sexp.ParseList(obj, "isiii") + if err != nil { + return ErrMalformedToken + } + if auth.TokenKind(vals[0].(sx.Int64)) != k { + return ErrOtherKind + } + ident := vals[1].(sx.String) if ident == "" { - return auth.TokenData{}, ErrNoIdent - } - if zidS, ok := claims.Set["zid"].(string); ok { - if zid, err2 := id.Parse(zidS); err2 == nil { - if kind, ok2 := claims.Set["_tk"].(float64); ok2 { - if auth.TokenKind(kind) == k { - return auth.TokenData{ - Token: token, - Now: now, - Issued: claims.Issued.Time(), - Expires: expires, - Ident: ident, - Zid: zid, - }, nil - } - } - return auth.TokenData{}, ErrOtherKind - } - } - return auth.TokenData{}, ErrNoZid + return ErrNoIdent + } + issued := time.Unix(int64(vals[2].(sx.Int64)), 0) + expires := time.Unix(int64(vals[3].(sx.Int64)), 0) + now := time.Now().Round(time.Second) + if expires.Before(now) { + return ErrTokenExpired + } + zid := id.Zid(vals[4].(sx.Int64)) + if !zid.IsValid() { + return ErrNoZid + } + + tokenData.Ident = ident.String() + tokenData.Issued = issued + tokenData.Now = now + tokenData.Expires = expires + tokenData.Zid = zid + return nil } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -76,10 +76,14 @@ } func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { return pp.box.GetAllZettel(ctx, zid) } + +func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { + return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) +} func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { return nil, err @@ -89,23 +93,15 @@ return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } -func (pp *polBox) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - return pp.box.GetAllMeta(ctx, zid) -} - -func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) { - return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid) -} - -func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { +func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { user := server.GetUser(ctx) canRead := pp.policy.CanRead q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) }) - return pp.box.SelectMeta(ctx, q) + return pp.box.SelectMeta(ctx, metaSeq, q) } func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } @@ -115,15 +111,15 @@ user := server.GetUser(ctx) if !zid.IsValid() { return &box.ErrInvalidID{Zid: zid} } // Write existing zettel - oldMeta, err := pp.box.GetMeta(ctx, zid) + oldZettel, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } - if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { + if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } @@ -130,16 +126,16 @@ func (pp *polBox) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { return pp.box.AllowRenameZettel(ctx, zid) } func (pp *polBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { - meta, err := pp.box.GetMeta(ctx, curZid) + z, err := pp.box.GetZettel(ctx, curZid) if err != nil { return err } user := server.GetUser(ctx) - if pp.policy.CanRename(user, meta) { + if pp.policy.CanRename(user, z.Meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -146,16 +142,16 @@ func (pp *polBox) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { return pp.box.CanDeleteZettel(ctx, zid) } func (pp *polBox) DeleteZettel(ctx context.Context, zid id.Zid) error { - meta, err := pp.box.GetMeta(ctx, zid) + z, err := pp.box.GetZettel(ctx, zid) if err != nil { return err } user := server.GetUser(ctx) - if pp.policy.CanDelete(user, meta) { + if pp.policy.CanDelete(user, z.Meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -9,11 +9,11 @@ //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/zettel/meta" ) type defaultPolicy struct { Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -9,11 +9,11 @@ //----------------------------------------------------------------------------- package policy import ( - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/zettel/meta" ) Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -12,11 +12,11 @@ import ( "fmt" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -16,11 +16,11 @@ "errors" "fmt" "io" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -39,13 +39,10 @@ CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - // CanUpdateZettel returns true, if box could possibly update the given zettel. CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool // UpdateZettel updates an existing zettel. UpdateZettel(ctx context.Context, zettel zettel.Zettel) error @@ -70,10 +67,13 @@ type MetaFunc func(*meta.Meta) // ManagedBox is the interface of managed boxes. type ManagedBox interface { BaseBox + + // HasZettel returns true, if box conains zettel with given identifier. + HasZettel(context.Context, id.Zid) bool // Apply identifier of every zettel to the given function, if predicate returns true. ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error // Apply metadata of every zettel to the given function, if predicate returns true. @@ -130,20 +130,21 @@ type Box interface { BaseBox // FetchZids returns the set of all zettel identifer managed by the box. FetchZids(ctx context.Context) (id.Set, error) + + // GetMeta returns the metadata of the zettel with the given identifier. + GetMeta(context.Context, id.Zid) (*meta.Meta, error) // SelectMeta returns a list of metadata that comply to the given selection criteria. - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) + // If `metaSeq` is `nil`, the box assumes metadata of all available zettel. + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) // GetAllZettel retrieves a specific zettel from all managed boxes. GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) - // GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. - GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) - // Refresh the data from the box and from its managed sub-boxes. Refresh(context.Context) error } // Stats record stattistics about a box. Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -13,11 +13,11 @@ import ( "context" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" @@ -80,40 +80,31 @@ func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { if gen, ok := myZettel[zid]; ok && gen.meta != nil { if m := gen.meta(zid); m != nil { updateMeta(m) if genContent := gen.content; genContent != nil { - cb.log.Trace().Msg("GetMeta/Content") + cb.log.Trace().Msg("GetZettel/Content") return zettel.Zettel{ Meta: m, Content: zettel.NewContent(genContent(m)), }, nil } - cb.log.Trace().Msg("GetMeta/NoContent") + cb.log.Trace().Msg("GetZettel/NoContent") return zettel.Zettel{Meta: m}, nil } } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") return zettel.Zettel{}, box.ErrNotFound } -func (cb *compBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - if gen, ok := myZettel[zid]; ok { - if genMeta := gen.meta; genMeta != nil { - if m := genMeta(zid); m != nil { - updateMeta(m) - cb.log.Trace().Msg("GetMeta") - return m, nil - } - } - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") - return nil, box.ErrNotFound +func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { + _, found := myZettel[zid] + return found } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") + cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -11,11 +11,11 @@ package compbox import ( "bytes" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -12,11 +12,11 @@ import ( "bytes" "fmt" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -32,9 +32,9 @@ keys := meta.GetSortedKeyDescriptions() var buf bytes.Buffer buf.WriteString("|=Name<|=Type<|=Computed?:|=Property?:\n") for _, kd := range keys { fmt.Fprintf(&buf, - "|%v|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) + "|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) } return buf.Bytes() } Index: box/compbox/log.go ================================================================== --- box/compbox/log.go +++ box/compbox/log.go @@ -11,11 +11,11 @@ package compbox import ( "bytes" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -12,11 +12,11 @@ import ( "bytes" "fmt" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -14,11 +14,11 @@ "bytes" "fmt" "sort" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -9,11 +9,11 @@ //----------------------------------------------------------------------------- package compbox import ( - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -34,11 +34,11 @@ )) ,@(if new-zettel-links `((div (@ (class "zs-dropdown")) (button "New") (nav (@ (class "zs-dropdown-content")) - ,@(map pair-to-href new-zettel-links) + ,@(map wui-link new-zettel-links) ))) ) (form (@ (action ,search-url)) (input (@ (type "text") (placeholder "Search..") (name ,query-key-query)))) ) Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -14,11 +14,11 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" @@ -71,17 +71,13 @@ } cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") return zettel.Zettel{}, box.ErrNotFound } -func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - if z, ok := cb.zettel[zid]; ok { - cb.log.Trace().Msg("GetMeta") - return meta.NewWithData(zid, z.header), nil - } - cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta") - return nil, box.ErrNotFound +func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { + _, found := cb.zettel[zid] + return found } func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid") for zid := range cb.zettel { @@ -221,50 +217,50 @@ constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230529125700", + api.KeyModified: "20230621131500", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentInfoSxn)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230528210200", + api.KeyModified: "20230621132600", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentFormSxn)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230602155600", + api.KeyModified: "20230707190246", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentRenameSxn)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, api.KeyCreated: "20200804111624", - api.KeyModified: "20230602155500", + api.KeyModified: "20230621133100", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentDeleteSxn)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230526221600", + api.KeyCreated: "20230704122100", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentListZettelSxn)}, id.ErrorTemplateZid: { constHeader{ @@ -274,10 +270,19 @@ api.KeyCreated: "20210305133215", api.KeyModified: "20230527224800", api.KeyVisibility: api.ValueVisibilityExpert, }, zettel.NewContent(contentErrorSxn)}, + id.TemplateSxnZid: { + constHeader{ + api.KeyTitle: "Zettelstore Sxn Code for Templates", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: meta.SyntaxSxn, + api.KeyCreated: "20230619132800", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + zettel.NewContent(contentTemplateCodeSxn)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, api.KeySyntax: meta.SyntaxCSS, @@ -389,10 +394,13 @@ var contentListZettelSxn []byte //go:embed error.sxn var contentErrorSxn []byte +//go:embed wuicode.sxn +var contentTemplateCodeSxn []byte + //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -9,18 +9,18 @@ ) ,@(if incoming `((div (@ (class "zs-warning")) (h2 "Warning!") (p "If you delete this zettel, incoming references from the following zettel will become invalid.") - (ul ,@(map pair-to-href-li incoming)) + (ul ,@(map wui-item-link incoming)) )) ) ,@(if (and (bound? 'useless) useless) `((div (@ (class "zs-warning")) (h2 "Warning!") (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") - (ul ,@(map (lambda (s) `(li ,s)) useless)) + (ul ,@(map wui-item useless)) )) ) - ,(pairs-to-dl metapairs) + ,(wui-meta-desc metapairs) (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete")))) ) Index: box/constbox/dependencies.zettel ================================================================== --- box/constbox/dependencies.zettel +++ box/constbox/dependencies.zettel @@ -99,23 +99,10 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` -=== pascaldekloe/jwt -; URL & Source -: [[https://github.com/pascaldekloe/jwt]] -; License -: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]] -``` -To the extent possible under law, Pascal S. de Kloe has waived all -copyright and related or neighboring rights to JWT. This work is -published from The Netherlands. - -https://creativecommons.org/publicdomain/zero/1.0/legalcode -``` - === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License @@ -141,17 +128,15 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` -=== t73fde/sxpf, t73fde/sxhtml, zettelstore-client +=== sx, zettelstore-client These are companion projects, written by the current main developer of Zettelstore. They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. -; URL & Source t73fde/sxpf -: [[https://codeberg.org/t73fde/sxpf]] -; URL & Source t73fde/sxhtml -: [[https://codeberg.org/t73fde/sxhtml]] +; URL & Source sx +: [[https://zettelstore.de/sx]] ; URL & Source zettelstore-client : [[https://zettelstore.de/client/]] ; License: : European Union Public License, version 1.2 (EUPL v1.2), or later. Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -7,15 +7,11 @@ (div (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) ,@(if role-data '((list "zs-role-data"))) )) - ,@(if role-data - `((datalist (@ (id "zs-role-data")) - ,@(map (lambda (v) `(option (@ (value ,v)))) role-data) - )) - ) + ,@(wui-datalist "zs-role-data" role-data) ) (div (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags)))) (div @@ -24,15 +20,11 @@ (div (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "ⓘ"))) (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) ,@(if syntax-data '((list "zs-syntax-data"))) )) - ,@(if syntax-data - `((datalist (@ (id "zs-syntax-data")) - ,@(map (lambda (v) `(option (@ (value ,v)))) syntax-data) - )) - ) + ,@(wui-datalist "zs-syntax-data" syntax-data) ) ,@(if (bound? 'content) `((div (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "ⓘ"))) (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..")) ,content) Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -4,44 +4,33 @@ (a (@ (href ,web-url)) "Web") (@H " · ") (a (@ (href ,context-url)) "Context") ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) ,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy"))) ,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version"))) + ,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child"))) ,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge"))) ,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename"))) ,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete"))) ) ) (h2 "Interpreted Metadata") - (table ,@(map (lambda (p) `(tr (td ,(car p)) (td ,(cdr p)))) metadata)) + (table ,@(map wui-table-row metadata)) (h2 "References") - ,@(if local-links - `((h3 "Local") - (ul ,@(map (lambda (l) (if (car l) `(li (a (@ (href ,(cdr l))) ,(cdr l))) `(li ,(cdr l)))) local-links)) - ) - ) - ,@(if query-links - `((h3 "Queries") - (ul ,@(map (lambda (q) `(li (a (@ (href ,(cdr q))) ,(car q)))) query-links)) - ) - ) - ,@(if ext-links - `((h3 "External") - (ul ,@(map (lambda (e) `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) ext-links)) - ) - ) + ,@(if local-links `((h3 "Local") (ul ,@(map wui-valid-link local-links)))) + ,@(if query-links `((h3 "Queries") (ul ,@(map wui-item-link query-links)))) + ,@(if ext-links `((h3 "External") (ul ,@(map wui-item-popup-link ext-links)))) (h3 "Unlinked") ,@unlinked-content (form (label (@ (for "phrase")) "Search Phrase") (input (@ (class "zs-input") (type "text") (id "phrase") (name ,query-key-phrase) (placeholder "Phrase..") (value ,phrase))) ) (h2 "Parts and encodings") - ,(make-enc-matrix enc-eval) + ,(wui-enc-matrix enc-eval) (h3 "Parsed (not evaluated)") - ,(make-enc-matrix enc-parsed) + ,(wui-enc-matrix enc-parsed) ,@(if shadow-links `((h2 "Shadowed Boxes") - (ul ,@(map (lambda (s) `(li ,s)) shadow-links)) + (ul ,@(map wui-item shadow-links)) ) ) ) Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -2,13 +2,18 @@ (header (h1 ,heading)) (form (@ (action ,search-url)) (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value)))) ,@content ,@endnotes - ,@(if (bound? 'create-url) - `((form (@ (action ,create-url)) - (input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) - (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) - (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) - )) + (form (@ (action ,(if (bound? 'create-url) create-url))) + "Other encodings: " + (a (@ (href ,data-url)) "data") + ", " + (a (@ (href ,plain-url)) "plain") + ,@(if (bound? 'create-url) + `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value))) + (input (@ (type "hidden") (name ,query-key-seed) (value ,seed))) + (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel"))) + ) + ) ) ) Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -1,7 +1,7 @@ `(article - (header (h1 ,heading)) + (header (h1 "Login")) ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again."))) (form (@ (method "POST") (action "")) (div (label (@ (for "username")) "User name:") (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus)))) Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ box/constbox/rename.sxn @@ -3,24 +3,24 @@ (p "Do you really want to rename this zettel?") ,@(if incoming `((div (@ (class "zs-warning")) (h2 "Warning!") (p "If you rename this zettel, incoming references from the following zettel will become invalid.") - (ul ,@(map pair-to-href-li incoming)) + (ul ,@(map wui-item-link incoming)) )) ) ,@(if (and (bound? 'useless) useless) `((div (@ (class "zs-warning")) (h2 "Warning!") (p "Renaming this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.") - (ul ,@(map (lambda (s) `(li ,s)) useless)) + (ul ,@(map wui-item useless)) )) ) (form (@ (method "POST")) - (input (@ (type "hidden" (id "curzid") (name "curzid") (value ,zid)))) + (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) (div - (label (@ (for "newid") "New zettel id")) - (input (@ (class "zs-input") (type "text") (id "newid") (name "newid") (placeholder "ZID..") (value ,zid) (autofocus)))) + (label (@ (for "newzid")) "New zettel id") + (input (@ (class "zs-input") (type "text") (id "newzid") (name "newzid") (placeholder "ZID..") (value ,zid) (autofocus)))) (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) ) - ,(pairs-to-dl metapairs) + ,(wui-meta-desc metapairs) ) ADDED box/constbox/wuicode.sxn Index: box/constbox/wuicode.sxn ================================================================== --- /dev/null +++ box/constbox/wuicode.sxn @@ -0,0 +1,66 @@ +;;;---------------------------------------------------------------------------- +;;; Copyright (c) 2023-present Detlef Stern +;;; +;;; This file is part of Zettelstore. +;;; +;;; Zettelstore 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. +;;;---------------------------------------------------------------------------- + +;; wui-list-item returns the argument as a HTML list item. +(define (wui-item s) `(li ,s)) + +;; wui-table-row takes a pair and translates it into a HTML table row with +;; two columns. +(define (wui-table-row p) + `(tr (td ,(car p)) (td ,(cdr p)))) + +;; wui-valid-link translates a local link into a HTML link. A link is a pair +;; (valid . url). If valid is not truish, only the invalid url is returned. +(define (wui-valid-link l) + (if (car l) + `(li (a (@ (href ,(cdr l))) ,(cdr l))) + `(li ,(cdr l)))) + +;; wui-link taks an url and returns a HTML link inside. +(define (wui-link q) + `(a (@ (href ,(cdr q))) ,(car q))) + +;; wui-item-link taks a pair (text . url) and returns a HTML link inside +;; a list item. +(define (wui-item-link q) `(li ,(wui-link q))) + +;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside +;; a table data item. +(define (wui-tdata-link q) `(td ,(wui-link q))) + +;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open +;; a new tab / window. +(define (wui-item-popup-link e) + `(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e))) + +;; wui-option-value returns a value for an HTML option element. +(define (wui-option-value v) `(option (@ (value ,v)))) + +;; wui-datalist returns a HTML datalist with the given HTML identifier and a +;; list of values. +(define (wui-datalist id lst) + (if lst + `((datalist (@ (id ,id)) ,@(map wui-option-value lst))))) + +;; wui-pair-desc-item takes a pair '(term . text) and returns a list with +;; a HTML description term and a HTML description data. +(define (wui-pair-desc-item p) `((dt ,(car p)) (dd ,(cdr p)))) + +;; wui-meta-desc returns a HTML description list made from the list of pairs +;; given. +(define (wui-meta-desc l) + `(dl ,@(apply append (map wui-pair-desc-item l)))) + +;; wui-enc-matrix returns the HTML table of all encodings and parts. +(define (wui-enc-matrix matrix) + `(table + ,@(map + (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) + matrix))) Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -3,28 +3,32 @@ (h1 ,heading) (div (@ (class "zs-meta")) ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) ,zid (@H " · ") (a (@ (href ,info-url)) "Info") (@H " · ") - "(" (a (@ (href ,role-url)) ,meta-role) ")" + "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role))) + ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role)) + `((@H " → ") (a (@ (href ,folge-role-url)) ,meta-folge-role))) + ")" ,@(if tag-refs `((@H " · ") ,@tag-refs)) ,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy"))) ,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version"))) + ,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child"))) ,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge"))) ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) ,@(if superior-refs `((br) "Superior: " ,superior-refs)) - ,@(if ext-url `((br) "URL: " ,ext-url)) + ,@(if (bound? 'meta-url) `((br) "URL: " ,(url-to-html meta-url))) ,@(let (author (and (bound? 'meta-author) meta-author)) (if author `((br) "By " ,author))) ) ) ,@content ,endnotes ,@(if (or folge-links subordinate-links back-links successor-links) `((nav - ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map pair-to-href-li folge-links))))) - ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map pair-to-href-li subordinate-links))))) - ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map pair-to-href-li back-links))))) - ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map pair-to-href-li successor-links))))) + ,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) + ,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) + ,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) + ,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) )) ) ) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -258,25 +258,12 @@ zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } -func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - m, err := dp.doGetMeta(ctx, zid) - dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta") - return m, err -} -func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - entry := dp.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return nil, box.ErrNotFound - } - m, err := dp.srvGetMeta(ctx, entry, zid) - if err != nil { - return nil, err - } - return m, nil +func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { + return dp.dirSrv.GetDirEntry(zid).IsValid() } func (dp *dirBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := dp.dirSrv.GetDirEntries(constraint) dp.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") @@ -351,11 +338,11 @@ if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. - if _, err := dp.doGetMeta(ctx, newZid); err == nil { + if dp.HasZettel(ctx, newZid) { return &box.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) if err != nil { Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -15,11 +15,11 @@ "errors" "net/url" "path/filepath" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -138,23 +138,12 @@ CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles) zb.log.Trace().Zid(zid).Msg("GetZettel") return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil } -func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { - entry := zb.dirSrv.GetDirEntry(zid) - if !entry.IsValid() { - return nil, box.ErrNotFound - } - reader, err := zip.OpenReader(zb.name) - if err != nil { - return nil, err - } - defer reader.Close() - m, err := zb.readZipMeta(reader, zid, entry) - zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta") - return m, err +func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { + return zb.dirSrv.GetDirEntry(zid).IsValid() } func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { entries := zb.dirSrv.GetDirEntries(constraint) zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid") Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -92,51 +92,10 @@ } } return result, nil } -// GetMeta retrieves just the meta data of a specific zettel. -func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - return mgr.doGetMeta(ctx, zid) -} - -func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound { - if err == nil { - mgr.Enrich(ctx, m, i+1) - } - return m, err - } - } - return nil, box.ErrNotFound -} - -// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes. -func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - mgr.mgrLog.Debug().Zid(zid).Msg("GetAllMeta") - if mgr.State() != box.StartStateStarted { - return nil, box.ErrStopped - } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - var result []*meta.Meta - for i, p := range mgr.boxes { - if m, err := p.GetMeta(ctx, zid); err == nil { - mgr.Enrich(ctx, m, i+1) - result = append(result, m) - } - } - return result, nil -} - // FetchZids returns the set of all zettel identifer managed by the box. func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) { mgr.mgrLog.Debug().Msg("FetchZids") if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped @@ -151,35 +110,57 @@ } } return result, nil } -type metaMap map[id.Zid]*meta.Meta +func (mgr *Manager) HasZettel(ctx context.Context, zid id.Zid) bool { + mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel") + if mgr.State() != box.StartStateStarted { + return false + } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + for _, bx := range mgr.boxes { + if bx.HasZettel(ctx, zid) { + return true + } + } + return false +} + +func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { + mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta") + if mgr.State() != box.StartStateStarted { + return nil, box.ErrStopped + } + + m, err := mgr.idxStore.GetMeta(ctx, zid) + if err != nil { + return nil, err + } + mgr.Enrich(ctx, m, 0) + return m, nil +} // SelectMeta returns all zettel meta data that match the selection // criteria. The result is ordered by descending zettel id. -func (mgr *Manager) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { +func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { if msg := mgr.mgrLog.Debug(); msg.Enabled() { msg.Str("query", q.String()).Msg("SelectMeta") } if mgr.State() != box.StartStateStarted { return nil, box.ErrStopped } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - return mgr.doSelectMeta(ctx, q) -} -func (mgr *Manager) doSelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { - compSearch, err := q.RetrieveAndCompile(ctx, mgr, mgr.doGetMeta, mgr.doSelectMeta) - if err != nil { - return nil, err - } + + compSearch := q.RetrieveAndCompile(ctx, mgr, metaSeq) if result := compSearch.Result(); result != nil { mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found without ApplyMeta") return result, nil } - selected := metaMap{} + selected := map[id.Zid]*meta.Meta{} for _, term := range compSearch.Terms { rejected := id.Set{} handleMeta := func(m *meta.Meta) { zid := m.Zid if rejected.Contains(zid) { Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -12,11 +12,11 @@ import ( "context" "strconv" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -32,11 +32,13 @@ // Enrich is called indirectly via indexer or enrichment is not requested // because of other reasons -> ignore this call, do not update metadata return } computePublished(m) - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + if boxNumber > 0 { + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) + } mgr.idxStore.Enrich(ctx, m) } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -161,11 +161,11 @@ var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta - zi := store.NewZettelIndex(m.Zid) + zi := store.NewZettelIndex(m) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } @@ -198,11 +198,11 @@ } } func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { - if _, err := mgr.GetMeta(ctx, ref); err == nil { + if mgr.HasZettel(ctx, ref) { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } @@ -213,19 +213,19 @@ func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if _, err = mgr.GetMeta(ctx, zid); err != nil { + if !mgr.HasZettel(ctx, zid) { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } - zi.AddMetaRef(inverseKey, zid) + zi.AddInverseRef(inverseKey, zid) } func (mgr *Manager) idxDeleteZettel(zid id.Zid) { toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -16,11 +16,11 @@ "io" "net/url" "sync" "time" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -17,66 +17,73 @@ "io" "sort" "strings" "sync" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -type metaRefs struct { +type bidiRefs struct { forward id.Slice backward id.Slice } -type zettelIndex struct { - dead id.Slice - forward id.Slice - backward id.Slice - meta map[string]metaRefs - words []string - urls []string -} - -func (zi *zettelIndex) isEmpty() bool { - if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 { - return false - } - return len(zi.meta) == 0 +type zettelData struct { + meta *meta.Meta + dead id.Slice + forward id.Slice + backward id.Slice + otherRefs map[string]bidiRefs + words []string + urls []string } type stringRefs map[string]id.Slice type memStore struct { - mx sync.RWMutex - idx map[id.Zid]*zettelIndex - dead map[id.Zid]id.Slice // map dead refs where they occur - words stringRefs - urls stringRefs + mx sync.RWMutex + intern map[string]string // map to intern strings + idx map[id.Zid]*zettelData + dead map[id.Zid]id.Slice // map dead refs where they occur + words stringRefs + urls stringRefs // Stats + mxStats sync.Mutex updates uint64 } // New returns a new memory-based index store. func New() store.Store { return &memStore{ - idx: make(map[id.Zid]*zettelIndex), - dead: make(map[id.Zid]id.Slice), - words: make(stringRefs), - urls: make(stringRefs), + intern: make(map[string]string, 1024), + idx: make(map[id.Zid]*zettelData), + dead: make(map[id.Zid]id.Slice), + words: make(stringRefs), + urls: make(stringRefs), + } +} + +func (ms *memStore) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { + ms.mx.RLock() + defer ms.mx.RUnlock() + if zi, found := ms.idx[zid]; found { + return zi.meta.Clone(), nil } + return nil, box.ErrNotFound } func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { if ms.doEnrich(m) { - ms.mx.Lock() + ms.mxStats.Lock() ms.updates++ - ms.mx.Unlock() + ms.mxStats.Unlock() } } func (ms *memStore) doEnrich(m *meta.Meta) bool { ms.mx.RLock() @@ -98,11 +105,11 @@ if len(zi.forward) > 0 { m.Set(api.KeyForward, zi.forward.String()) back = remRefs(back, zi.forward) updated = true } - for k, refs := range zi.meta { + for k, refs := range zi.otherRefs { if len(refs.backward) > 0 { m.Set(k, refs.backward.String()) back = remRefs(back, refs.backward) updated = true } @@ -232,15 +239,15 @@ result.AddSlice(refs) } return result } -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { // Must only be called if ms.mx is read-locked! result.Zid(zid) result.AddSlice(zi.backward) - for _, mref := range zi.meta { + for _, mref := range zi.otherRefs { result.AddSlice(mref.backward) } } func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { @@ -260,15 +267,16 @@ } return back } func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { + m := ms.makeMeta(zidx) ms.mx.Lock() defer ms.mx.Unlock() zi, ziExist := ms.idx[zidx.Zid] if !ziExist || zi == nil { - zi = &zettelIndex{} + zi = &zettelData{} ziExist = false } // Is this zettel an old dead reference mentioned in other zettel? var toCheck id.Set @@ -276,25 +284,63 @@ // These must be checked later again toCheck = id.NewSet(refs...) delete(ms.dead, zidx.Zid) } + zi.meta = m ms.updateDeadReferences(zidx, zi) ms.updateForwardBackwardReferences(zidx, zi) ms.updateMetadataReferences(zidx, zi) zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords()) zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) // Check if zi must be inserted into ms.idx - if !ziExist && !zi.isEmpty() { + if !ziExist { ms.idx[zidx.Zid] = zi } return toCheck } -func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +var internableKeys = map[string]bool{ + api.KeyRole: true, + api.KeySyntax: true, + api.KeyFolgeRole: true, + api.KeyLang: true, + api.KeyReadOnly: true, +} + +func isInternableValue(key string) bool { + if internableKeys[key] { + return true + } + return strings.HasSuffix(key, "-role") +} + +func (ms *memStore) internString(s string) string { + if is, found := ms.intern[s]; found { + return is + } + ms.intern[s] = s + return s +} + +func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta { + origM := zidx.GetMeta() + copyM := meta.New(origM.Zid) + for _, p := range origM.Pairs() { + key := ms.internString(p.Key) + if isInternableValue(key) { + copyM.Set(key, ms.internString(p.Value)) + } else if key == api.KeyBoxNumber || !meta.IsComputed(key) { + copyM.Set(key, p.Value) + } + } + return copyM +} + +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! drefs := zidx.GetDeadRefs() newRefs, remRefs := refsDiff(drefs, zi.dead) zi.dead = drefs for _, ref := range remRefs { @@ -303,11 +349,11 @@ for _, ref := range newRefs { ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) } } -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! brefs := zidx.GetBackRefs() newRefs, remRefs := refsDiff(brefs, zi.forward) zi.forward = brefs for _, ref := range remRefs { @@ -318,43 +364,42 @@ bzi := ms.getEntry(ref) bzi.backward = addRef(bzi.backward, zidx.Zid) } } -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) { // Must only be called if ms.mx is write-locked! - metarefs := zidx.GetMetaRefs() - for key, mr := range zi.meta { - if _, ok := metarefs[key]; ok { + inverseRefs := zidx.GetInverseRefs() + for key, mr := range zi.otherRefs { + if _, ok := inverseRefs[key]; ok { continue } ms.removeInverseMeta(zidx.Zid, key, mr.forward) } - if zi.meta == nil { - zi.meta = make(map[string]metaRefs) + if zi.otherRefs == nil { + zi.otherRefs = make(map[string]bidiRefs) } - for key, mrefs := range metarefs { - mr := zi.meta[key] + for key, mrefs := range inverseRefs { + mr := zi.otherRefs[key] newRefs, remRefs := refsDiff(mrefs, mr.forward) mr.forward = mrefs - zi.meta[key] = mr + zi.otherRefs[key] = mr for _, ref := range newRefs { bzi := ms.getEntry(ref) - if bzi.meta == nil { - bzi.meta = make(map[string]metaRefs) + if bzi.otherRefs == nil { + bzi.otherRefs = make(map[string]bidiRefs) } - bmr := bzi.meta[key] + bmr := bzi.otherRefs[key] bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.meta[key] = bmr + bzi.otherRefs[key] = bmr } ms.removeInverseMeta(zidx.Zid, key, remRefs) } } func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { - // Must only be called if ms.mx is write-locked! newWords, removeWords := next.Diff(prev) for _, word := range newWords { if refs, ok := srefs[word]; ok { srefs[word] = addRef(refs, zid) continue @@ -374,16 +419,16 @@ srefs[word] = refs2 } return next.Words() } -func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { +func (ms *memStore) getEntry(zid id.Zid) *zettelData { // Must only be called if ms.mx is write-locked! if zi, ok := ms.idx[zid]; ok { return zi } - zi := &zettelIndex{} + zi := &zettelData{} ms.idx[zid] = zi return zi } func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { @@ -395,21 +440,21 @@ return nil } ms.deleteDeadSources(zid, zi) toCheck := ms.deleteForwardBackward(zid, zi) - if len(zi.meta) > 0 { - for key, mrefs := range zi.meta { + if len(zi.otherRefs) > 0 { + for key, mrefs := range zi.otherRefs { ms.removeInverseMeta(zid, key, mrefs.forward) } } ms.deleteWords(zid, zi.words) delete(ms.idx, zid) return toCheck } -func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) { +func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelData) { // Must only be called if ms.mx is write-locked! for _, ref := range zi.dead { if drefs, ok := ms.dead[ref]; ok { drefs = remRef(drefs, zid) if len(drefs) > 0 { @@ -419,11 +464,11 @@ } } } } -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set { +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { // Must only be called if ms.mx is write-locked! var toCheck id.Set for _, ref := range zi.forward { if fzi, ok := ms.idx[ref]; ok { fzi.backward = remRef(fzi.backward, zid) @@ -443,24 +488,24 @@ func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) { // Must only be called if ms.mx is write-locked! for _, ref := range forward { bzi, ok := ms.idx[ref] - if !ok || bzi.meta == nil { + if !ok || bzi.otherRefs == nil { continue } - bmr, ok := bzi.meta[key] + bmr, ok := bzi.otherRefs[key] if !ok { continue } bmr.backward = remRef(bmr.backward, zid) if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.meta[key] = bmr + bzi.otherRefs[key] = bmr } else { - delete(bzi.meta, key) - if len(bzi.meta) == 0 { - bzi.meta = nil + delete(bzi.otherRefs, key) + if len(bzi.otherRefs) == 0 { + bzi.otherRefs = nil } } } } @@ -481,14 +526,16 @@ } func (ms *memStore) ReadStats(st *store.Stats) { ms.mx.RLock() st.Zettel = len(ms.idx) - st.Updates = ms.updates st.Words = uint64(len(ms.words)) st.Urls = uint64(len(ms.urls)) ms.mx.RUnlock() + ms.mxStats.Lock() + st.Updates = ms.updates + ms.mxStats.Unlock() } func (ms *memStore) Dump(w io.Writer) { ms.mx.RLock() defer ms.mx.RUnlock() @@ -516,11 +563,11 @@ if len(zi.dead) > 0 { fmt.Fprintln(w, "* Dead:", zi.dead) } dumpZids(w, "* Forward:", zi.forward) dumpZids(w, "* Backward:", zi.backward) - for k, fb := range zi.meta { + for k, fb := range zi.otherRefs { fmt.Fprintln(w, "* Meta", k) dumpZids(w, "** Forward:", fb.forward) dumpZids(w, "** Backward:", fb.backward) } dumpStrings(w, "* Words", "", "", zi.words) Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -37,10 +37,13 @@ // Store all relevant zettel data. There may be multiple implementations, i.e. // memory-based, file-based, based on SQLite, ... type Store interface { query.Searcher + + // GetMeta returns the metadata of the zettel with the given identifier. + GetMeta(context.Context, id.Zid) (*meta.Meta, error) // Entrich metadata with data from store. Enrich(ctx context.Context, m *meta.Meta) // UpdateReferences for a specific zettel. Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -8,46 +8,51 @@ // under this license. //----------------------------------------------------------------------------- package store -import "zettelstore.de/z/zettel/id" +import ( + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { - Zid id.Zid // zid of the indexed zettel - backrefs id.Set // set of back references - metarefs map[string]id.Set // references to inverse keys - deadrefs id.Set // set of dead references - words WordSet - urls WordSet + Zid id.Zid // zid of the indexed zettel + meta *meta.Meta // full metadata + backrefs id.Set // set of back references + inverseRefs map[string]id.Set // references of inverse keys + deadrefs id.Set // set of dead references + words WordSet + urls WordSet } // NewZettelIndex creates a new zettel index. -func NewZettelIndex(zid id.Zid) *ZettelIndex { +func NewZettelIndex(m *meta.Meta) *ZettelIndex { return &ZettelIndex{ - Zid: zid, - backrefs: id.NewSet(), - metarefs: make(map[string]id.Set), - deadrefs: id.NewSet(), + Zid: m.Zid, + meta: m, + backrefs: id.NewSet(), + inverseRefs: make(map[string]id.Set), + deadrefs: id.NewSet(), } } // AddBackRef adds a reference to a zettel where the current zettel links to // without any more information. func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs.Zid(zid) } -// AddMetaRef adds a named reference to a zettel. On that zettel, the given +// AddInverseRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. -func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { - if zids, ok := zi.metarefs[key]; ok { +func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { + if zids, ok := zi.inverseRefs[key]; ok { zids.Zid(zid) return } - zi.metarefs[key] = id.NewSet(zid) + zi.inverseRefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { zi.deadrefs.Zid(zid) @@ -58,26 +63,25 @@ // SetUrls sets the words to the given value. func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls } // GetDeadRefs returns all dead references as a sorted list. -func (zi *ZettelIndex) GetDeadRefs() id.Slice { - return zi.deadrefs.Sorted() -} +func (zi *ZettelIndex) GetDeadRefs() id.Slice { return zi.deadrefs.Sorted() } + +// GetMeta return just the raw metadata. +func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } // GetBackRefs returns all back references as a sorted list. -func (zi *ZettelIndex) GetBackRefs() id.Slice { - return zi.backrefs.Sorted() -} +func (zi *ZettelIndex) GetBackRefs() id.Slice { return zi.backrefs.Sorted() } -// GetMetaRefs returns all meta references as a map of strings to a sorted list of references -func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice { - if len(zi.metarefs) == 0 { +// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references +func (zi *ZettelIndex) GetInverseRefs() map[string]id.Slice { + if len(zi.inverseRefs) == 0 { return nil } - result := make(map[string]id.Slice, len(zi.metarefs)) - for key, refs := range zi.metarefs { + result := make(map[string]id.Slice, len(zi.inverseRefs)) + for key, refs := range zi.inverseRefs { result[key] = refs.Sorted() } return result } Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -21,11 +21,10 @@ "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func init() { manager.Register( "mem", @@ -128,19 +127,15 @@ z.Meta = z.Meta.Clone() mb.log.Trace().Msg("GetZettel") return z, nil } -func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { +func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { mb.mx.RLock() - zettel, ok := mb.zettel[zid] + _, found := mb.zettel[zid] mb.mx.RUnlock() - if !ok { - return nil, box.ErrNotFound - } - mb.log.Trace().Msg("GetMeta") - return zettel.Meta.Clone(), nil + return found } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -11,11 +11,11 @@ package notify import ( "path/filepath" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -15,11 +15,11 @@ "flag" "fmt" "io" "os" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -15,11 +15,11 @@ "fmt" "os" "golang.org/x/term" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth/cred" "zettelstore.de/z/zettel/id" ) // ---------- Subcommand: password ------------------------------------------- Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -54,25 +54,25 @@ webLog := kern.GetLogger(kernel.WebService) var getUser getUserImpl logAuth := kern.GetLogger(kernel.AuthService) logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) - ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) + ucGetUser := usecase.NewGetUser(authManager, boxManager) + ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser) ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) - ucGetMeta := usecase.NewGetMeta(protectedBoxManager) - ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) + ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager) ucGetZettel := usecase.NewGetZettel(protectedBoxManager) ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) - ucListMeta := usecase.NewListMeta(protectedBoxManager) - ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) + ucQuery := usecase.NewQuery(protectedBoxManager) + ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) + ucQuery.SetEvaluate(&ucEvaluate) ucListSyntax := usecase.NewListSyntax(protectedBoxManager) ucListRoles := usecase.NewListRoles(protectedBoxManager) ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) - ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig) ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) a := api.New( webLog.Clone().Str("adapter", "api").Child(), @@ -88,40 +88,37 @@ webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } // Web user interface if !authManager.IsReadonly() { - webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetMeta)) + webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) - webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(ucListMeta, &ucEvaluate, ucListRoles, ucListSyntax)) + webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(&ucQuery, &ucEvaluate, ucListRoles, ucListSyntax)) webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler( ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetMeta, ucGetAllMeta)) + webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel)) webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax)) webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate)) } webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh)) - webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta)) - webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetMeta)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(&ucQuery)) + webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( - ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs)) + ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery)) // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) - webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( - usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) - webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(ucGetMeta, ucUnlinkedRefs)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(ucListMeta)) - webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetMeta, ucGetZettel, ucParseZettel, ucEvaluate)) + webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery)) + webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) if !authManager.IsReadonly() { webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -11,11 +11,11 @@ package cmd import ( "flag" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -20,11 +20,11 @@ "runtime/debug" "strconv" "strings" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" @@ -86,19 +86,19 @@ Name: "password", Func: cmdPassword, }) } -func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) { +func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) { if configFlag := fs.Lookup("c"); configFlag != nil { if filename := configFlag.Value.String(); filename != "" { content, err := readConfiguration(filename) - return createConfiguration(content, err) + return filename, createConfiguration(content, err) } } - content, err := searchAndReadConfiguration() - return createConfiguration(content, err) + filename, content, err := searchAndReadConfiguration() + return filename, createConfiguration(content, err) } func createConfiguration(content []byte, err error) *meta.Meta { if err != nil { return meta.New(id.Invalid) @@ -106,21 +106,21 @@ return meta.NewFromInput(id.Invalid, input.NewInput(content)) } func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } -func searchAndReadConfiguration() ([]byte, error) { - for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} { +func searchAndReadConfiguration() (string, []byte, error) { + for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} { if content, err := readConfiguration(filename); err == nil { - return content, nil + return filename, content, nil } } - return readConfiguration(".zscfg") + return "", nil, os.ErrNotExist } -func getConfig(fs *flag.FlagSet) *meta.Meta { - cfg := fetchStartupConfiguration(fs) +func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { + filename, cfg := fetchStartupConfiguration(fs) fs.Visit(func(flg *flag.Flag) { switch flg.Name { case "p": cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String())) case "a": @@ -142,11 +142,11 @@ cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) - return cfg + return filename, cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for _, p := range cfg.PairsRest() { if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { @@ -249,11 +249,11 @@ fs := command.GetFlags() if err := fs.Parse(args); err != nil { fmt.Fprintf(os.Stderr, "%s: unable to parse flags: %v %v\n", name, args, err) return 1 } - cfg := getConfig(fs) + filename, cfg := getConfig(fs) if !setServiceConfig(cfg) { fs.Usage() return 2 } @@ -288,11 +288,11 @@ ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } - kern.Start(command.Header, command.LineServer) + kern.Start(command.Header, command.LineServer, filename) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) @@ -300,11 +300,11 @@ } // runSimple is called, when the user just starts the software via a double click // or via a simple call “./zettelstore“ on the command line. func runSimple() int { - if _, err := searchAndReadConfiguration(); err == nil { + if _, _, err := searchAndReadConfiguration(); err == nil { return executeCommand(strRunSimple) } dir := "./zettel" if err := os.MkdirAll(dir, 0750); err != nil { fmt.Fprintf(os.Stderr, "Unable to create zettel directory %q (%s)\n", dir, err) Index: docs/manual/00001002000000.zettel ================================================================== --- docs/manual/00001002000000.zettel +++ docs/manual/00001002000000.zettel @@ -2,17 +2,21 @@ title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20221018105415 +modified: 20230624171152 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. +: Normal zettel should be stored in a single file. + If this is not possible: at most in two files: one for the metadata, one for the content. + The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. +: There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. @@ -33,5 +37,7 @@ : External software can be written to deeply analyze your zettel and the structures they form. ; Security by default : Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks. : If you know what use are doing, Zettelstore allows you to relax some security-related preferences. However, even in this case, the more secure way is chosen. +: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed. +: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software. Index: docs/manual/00001005090000.zettel ================================================================== --- docs/manual/00001005090000.zettel +++ docs/manual/00001005090000.zettel @@ -2,11 +2,11 @@ title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20220909180240 +modified: 20230619133707 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore @@ -27,10 +27,12 @@ | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel +| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message +| [[00000000019100]] | Zettelstore Sxn Code for Templates | Some helper functions to build the templates | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu Index: docs/manual/00001006020000.zettel ================================================================== --- docs/manual/00001006020000.zettel +++ docs/manual/00001006020000.zettel @@ -2,11 +2,11 @@ title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 -modified: 20230421155051 +modified: 20230704161159 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. @@ -51,10 +51,12 @@ ''expire'' is just a documentation. You could define a query and execute it regularly, for example [[query:expire? ORDER expire]]. Alternatively, a Zettelstore client software could define some actions when it detects expired zettel. ; [!folge|''folge''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value. +; [!folge-role|''folge-role''] +: Specifies a suggested [[''role''|#role]] the zettel should use in the future, if zettel currently has a preliminary role. ; [!forward|''forward''] : Property that contains all references that identify another zettel within the content of the zettel. ; [!id|''id''] : Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore. It cannot be set manually, because it is a computed value. Index: docs/manual/00001006030000.zettel ================================================================== --- docs/manual/00001006030000.zettel +++ docs/manual/00001006030000.zettel @@ -2,11 +2,11 @@ title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 -modified: 20230402183536 +modified: 20230612183742 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. @@ -24,11 +24,11 @@ | any other suffix | [[EString|00001006031500]] The name of the metadata key is bound to the key type Every key type has an associated validation rule to check values of the given type. -There is also a rule how values are matched, e.g. against a search term when selecting some zettel. +There is also a rule how values are matched, e.g. against a [[search value|00001007706000]] when selecting some zettel. And there is a rule how values compare for sorting. * [[Credential|00001006031000]] * [[EString|00001006031500]] * [[Identifier|00001006032000]] Index: docs/manual/00001006032000.zettel ================================================================== --- docs/manual/00001006032000.zettel +++ docs/manual/00001006032000.zettel @@ -2,19 +2,29 @@ title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175535 +modified: 20230612183459 Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). === Query comparison -Comparison is done with the string representation of the identifiers. +[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters. + +When the [[search operators|00001007705000]] ""less"", ""not less"", ""greater"", and ""not greater"" are given, the length of the search value is checked. +If it contains less than 14 digits, zero digits (""0"") are appended, until it contains exactly 14 digits. + +All other comparisons assume that up to 14 characters are given. + +Comparison is done through the string representation. + +In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison. + For example, ""000010"" matches ""[[00001006032000]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006033000.zettel ================================================================== --- docs/manual/00001006033000.zettel +++ docs/manual/00001006033000.zettel @@ -2,17 +2,23 @@ title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175623 +modified: 20230612183900 Values of this type denote a numeric integer value. === Allowed values Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character. === Query comparison -All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number ""12"". +[[Search operators|00001007705000]] for equality (""equal"" or ""not equal"", ""has"" or ""not has""), for lesser values (""less"" or ""not less""), or for greater values (""greater"" or ""not greater"") are executed by converting both the [[search value|00001007706000]] and the metadata value into integer values and then comparing them numerically. +Integer values must be in the range -9223372036854775808 … 9223372036854775807. +Comparisons with metadata values outside this range always returns a negative match. +Comparisons with search values outside this range will be executed as a comparison of the string representation values. + +All other comparisons (""match"", ""not match"", ""prefix"", ""not prefix"", ""suffix"", and ""not suffix"") are done on the given string representation of the number. +In this case, the number ""+12"" will be treated as different to the number ""12"". === Sorting Sorting is done by comparing the numeric values. Index: docs/manual/00001006034500.zettel ================================================================== --- docs/manual/00001006034500.zettel +++ docs/manual/00001006034500.zettel @@ -2,11 +2,11 @@ title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175713 +modified: 20230612183509 Values of this type denote a point in time. === Allowed values Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". @@ -17,12 +17,19 @@ * hh is the hour, * mm is the minute, * ss is the second. === Query comparison -All comparisons assume that up to 14 digits are given. +[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters. + +When the [[search operators|00001007705000]] ""less"", ""not less"", ""greater"", and ""not greater"" are given, the length of the search value is checked. +If it contains less than 14 digits, zero digits (""0"") are appended, until it contains exactly 14 digits. + +All other comparisons assume that up to 14 characters are given. + Comparison is done through the string representation. +In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison. === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are timestamp values, this works well because both have the same length. Index: docs/manual/00001007700000.zettel ================================================================== --- docs/manual/00001007700000.zettel +++ docs/manual/00001007700000.zettel @@ -1,72 +1,30 @@ id: 00001007700000 -title: Query expression +title: Query Expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 -modified: 20230420190257 +modified: 20230731161954 A query expression allows you to search for specific zettel and to perform some actions on them. -You may select zettel based on a full-text search, based on specific metadata values, or both. +You may select zettel based on a list of [[zettel identifier|00001006050000]], based on a query directive, based on a full-text search, based on specific metadata values, or some or all of them. -A query expression consists of an optional __context expression__, a __search expression__ and an optional __action list__. +A query expression consists of an optional __[[zettel identifier list|00001007710000]]__, zero or more __[[query directives|00001007720000]]__, an optional __[[search expression|00001007701000]]__, and an optional __[[action list|00001007770000]]__. The latter two are separated by a vertical bar character (""''|''"", U+007C). A query expression follows a [[formal syntax|00001007780000]]. -=== Context expression - -An context expression starts with the keyword ''CONTEXT'', one or more space characters, and a [[zettel identifier|00001006050000]]. - -Optionally you may specify some context details, separated by space character. These are: -* ''BACKWARD'': search for context only though backward links, -* ''FORWARD'': search for context only through forward links, -* ''COST'', one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), -* ''MAX'', one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). - -If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. - -The cost of a context zettel is calculated iteratively: -* The specified zettel hast a cost of one. -* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus one. -* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus two. -* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus three. -* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the three choices above and multiplied with roughly a logarithmic value based on the size of the set. -* A zettel with the same tag, has the cost of the originating zettel, plus the number of zettel with the same tag (if it is less than eight), or the cost of the originating zettel plus two, multiplied by number of zettel with the same tag divided by four. - -Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel. -It also penalties a zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list. - -=== Search expression - -In its simplest form, a search expression just contains a string to be search for with the help of a full-text search. -For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". - -If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. -""title"" names the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]]. -The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. -""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". - -A search expression may contain more than one search term, such as ''title:syntax''. -Search terms must be separated by one or more space characters, for example ''title:syntax title:search''. -All terms of a select expression must be true so that a zettel is selected. - -* [[Search terms|00001007702000]] -* [[Search operator|00001007705000]] -* [[Search value|00001007706000]] - -Here are [[some examples|00001007790000]] of search / context expressions, which can be used to manage a Zettelstore: -{{{00001007790000}}} - -=== Action List - -With a search expression, a list of zettel is selected. -Actions allow to modify this list to a certain degree. - -Which actions are allowed depends on the context. -However, actions are further separated into __parameter action__ and __aggregate actions__. -A parameter action just sets a parameter for an aggregate action. -An aggregate action transforms the list of selected zettel into a different, aggregate form. -Only the first aggregate form is executed, following aggregate actions are ignored. - -In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. +* [[List of zettel identifier|00001007710000]] +* [[Query directives|00001007720000]] +** [[Context directive|00001007720300]] +** [[Ident directive|00001007720600]] +** [[Items directive|00001007720900]] +** [[Unlinked directive|00001007721200]] +* [[Search expression|00001007701000]] +** [[Search term|00001007702000]] +** [[Search operator|00001007705000]] +** [[Search value|00001007706000]] +* [[Action list|00001007770000]] + +Here are [[some examples|00001007790000]], which can be used to manage a Zettelstore: +{{{00001007790000}}} ADDED docs/manual/00001007701000.zettel Index: docs/manual/00001007701000.zettel ================================================================== --- /dev/null +++ docs/manual/00001007701000.zettel @@ -0,0 +1,26 @@ +id: 00001007701000 +title: Query: Search Expression +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230707205043 +modified: 20230707210039 + +In its simplest form, a search expression just contains a string to be search for with the help of a full-text search. +For example, the string ''syntax'' will search for all zettel containing the word ""syntax"". + +If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''. +""title"" denotes the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]]. +The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match. +""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"". + +A search expression may contain more than one search term, such as ''title:syntax''. +Search terms must be separated by one or more space characters, for example ''title:syntax title:search''. +All terms of a select expression must be true so that a zettel is selected. + +Above sequence of search expressions may be combined by specifying the keyword ''OR''. +At most one of those sequences must be true so that a zettel is selected. + +* [[Search term|00001007702000]] +* [[Search operator|00001007705000]] +* [[Search value|00001007706000]] Index: docs/manual/00001007702000.zettel ================================================================== --- docs/manual/00001007702000.zettel +++ docs/manual/00001007702000.zettel @@ -2,11 +2,11 @@ title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 -modified: 20230420153154 +modified: 20230612180954 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. A search term can be one of the following (the first three term are collectively called __search literals__): @@ -16,10 +16,14 @@ If no search value is given, then all zettel containing the given metadata key are selected (or ignored, for a negated search operator). * An optional [[search operator|00001007705000]], followed by a [[search value|00001007706000]]. This specifies a full-text search for the given search value. + + However, the operators ""less"" and ""greater"" are not supported, they are internally translated into the ""match"" operators. + Similar, ""not less"" and ""not greater"" are translated into ""not match"". + It simply does not make sense to search the content of all zettel for words less than a specific word, for example. **Note:** the search value will be normalized according to Unicode NKFD, ignoring everything except letters and numbers. Therefore, the following search expression are essentially the same: ''"search syntax"'' and ''search syntax''. The first is a search expression with one search value, which is normalized to two strings to be searched for. The second is a search expression containing two search values, giving two string to be searched for. Index: docs/manual/00001007705000.zettel ================================================================== --- docs/manual/00001007705000.zettel +++ docs/manual/00001007705000.zettel @@ -2,36 +2,43 @@ title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 -modified: 20230419163812 +modified: 20230612180539 A search operator specifies how the comparison of a search value and a zettel should be executed. Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters. The following are allowed search operator characters: -* The exclamation mark character (""''!''"", U+0021) negates the meaning -* The equal sign character (""'' ''"", U+003D) compares on equal content (""equals operator"") -* The tilde character (""''~''"", U+007E) compares on matching (""match operator"") -* The greater-than sign character (""''>''"", U+003E) matches if there is some prefix (""prefix operator"") -* The less-than sign character (""''<''"", U+003C) compares a suffix relationship (""suffix operator"") +* The exclamation mark character (""''!''"", U+0021) negates the meaning. +* The equal sign character (""''=''"", U+003D) compares on equal content (""equals operator""). +* The tilde character (""''~''"", U+007E) compares on matching (""match operator""). +* The left square bracket character (""''[''"", U+005B) matches if there is some prefix (""prefix operator""). +* The right square bracket character (""'']''"", U+005D) compares a suffix relationship (""suffix operator""). * The colon character (""'':''"", U+003A) compares depending on the on the actual [[key type|00001006030000]] (""has operator""). In most cases, it acts as a equals operator, but for some type it acts as the match operator. -* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"") +* The less-than sign character (""''<''"", U+003C) matches if the search value is somehow less then the metadata value (""less operator""). +* The greater-than sign character (""''>''"", U+003E) matches if the search value is somehow greater then the metadata value (""greater operator""). +* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator""). + In this case no [[search value|00001007706000]] must be given. -Since the exclamation mark character can be combined with the other, there are 14 possible combinations: +Since the exclamation mark character can be combined with the other, there are 18 possible combinations: # ""''!''"": is an abbreviation of the ""''!~''"" operator. # ""''~''"": is successful if the search value matched the value to be compared. # ""''!~''"": is successful if the search value does not match the value to be compared. # ""''=''"": is successful if the search value is equal to one word of the value to be compared. # ""''!=''"": is successful if the search value is not equal to any word of the value to be compared. -# ""''>''"": is successful if the search value is a prefix of the value to be compared. -# ""''!>''"": is successful if the search value is not a prefix of the value to be compared. -# ""''<''"": is successful if the search value is a suffix of the value to be compared. -# ""''!<''"": is successful if the search value is not a suffix of the value to be compared. +# ""''[''"": is successful if the search value is a prefix of the value to be compared. +# ""''![''"": is successful if the search value is not a prefix of the value to be compared. +# ""'']''"": is successful if the search value is a suffix of the value to be compared. +# ""''!]''"": is successful if the search value is not a suffix of the value to be compared. # ""'':''"": is successful if the search value is has/match one word of the value to be compared. # ""''!:''"": is successful if the search value is not match/has to any word of the value to be compared. +# ""''<''"": is successful if the search value is less than the value to be compared. +# ""''!<''"": is successful if the search value is not less than, e.g. greater or equal than the value to be compared. +# ""''>''"": is successful if the search value is greater than the value to be compared. +# ""''!>''"": is successful if the search value is not greater than, e.g. less or equal than the value to be compared. # ""''?''"": is successful if the metadata contains the given key. # ""''!?''"": is successful if the metadata does not contain the given key. # ""''''"": a missing search operator can only occur for a full-text search. It is equal to the ""''~''"" operator. ADDED docs/manual/00001007710000.zettel Index: docs/manual/00001007710000.zettel ================================================================== --- /dev/null +++ docs/manual/00001007710000.zettel @@ -0,0 +1,17 @@ +id: 20230707202600 +title: Query: List of Zettel Identifier +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230707202652 + +A query may start with a list of [[zettel identifier|00001006050000]], where the identifier are separated by one ore more space characters. + +If you specify at least one query directive, this list acts as a input for the first query directive. +Otherwise, only the zettel of the given list are used to evaluated the search expression or the action list (if no search expression was given). + +Some examples: +* [[query:00001007700000 CONTEXT]] returns the context of this zettel. +* [[query:00001007700000 00001007031140 man]] searches the given two zettel for a string ""man"". +* [[query:00001007700000 00001007031140 | tags]] return a tag cloud with tags from those two zettel. +* [[query:00001007700000 00001007031140]] returns a list with the two zettel. ADDED docs/manual/00001007720000.zettel Index: docs/manual/00001007720000.zettel ================================================================== --- /dev/null +++ docs/manual/00001007720000.zettel @@ -0,0 +1,19 @@ +id: 00001007720000 +title: Query Directives +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230707203135 +modified: 20230731162002 + +A query directive transforms a list of zettel identifier into a list of zettel identifiert. +It is only valid if a list of zettel identifier is specified at the beginning of the query expression. +Otherwise the text of the directive is interpreted as a search expression. +For example, ''CONTEXT'' is interpreted as a full-text search for the word ""context"". + +Every query directive therefore consumes a list of zettel, and it produces a list of zettel according to the specific directive. + +* [[Context directive|00001007720300]] +* [[Ident directive|00001007720600]] +* [[Items directive|00001007720900]] +* [[Unlinked directive|00001007721200]] ADDED docs/manual/00001007720300.zettel Index: docs/manual/00001007720300.zettel ================================================================== --- /dev/null +++ docs/manual/00001007720300.zettel @@ -0,0 +1,37 @@ +id: 00001007720300 +title: Query: Context Directive +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230707204706 +modified: 20230724153832 + +A context directive calculates the __context__ of a list of zettel identifier. +It starts with the keyword ''CONTEXT''. + +Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. +These are: +* ''BACKWARD'': search for context only though backward links, +* ''FORWARD'': search for context only through forward links, +* ''COST'', one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), +* ''MAX'', one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). + +If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. + +The cost of a context zettel is calculated iteratively: +* Each of the specified zettel hast a cost of one. +* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus one. +* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus two. +* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus three. +* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the three choices above and multiplied with roughly a logarithmic value based on the size of the set. +* A zettel with the same tag, has the cost of the originating zettel, plus the number of zettel with the same tag (if it is less than eight), or the cost of the originating zettel plus two, multiplied by number of zettel with the same tag divided by four. + +The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel. +This ensures that initial zettel that have only a highly used tag, will also produce some context zettel. + +Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel. +It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list. + +This directive may be specified only once as a query directive. +A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]]. +In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving power. ADDED docs/manual/00001007720600.zettel Index: docs/manual/00001007720600.zettel ================================================================== --- /dev/null +++ docs/manual/00001007720600.zettel @@ -0,0 +1,12 @@ +id: 00001007720600 +title: Query: Ident Directive +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230724153018 +modified: 20230724154015 + +An ident directive is needed if you want to specify just a list of zettel identifier. +It starts with / consists of the keyword ''IDENT''. + +When not using the ident directive, zettel identifier are interpreted as a [[search expression|00001007701000]]. ADDED docs/manual/00001007720900.zettel Index: docs/manual/00001007720900.zettel ================================================================== --- /dev/null +++ docs/manual/00001007720900.zettel @@ -0,0 +1,39 @@ +id: 00001007720900 +title: Query: Items Directive +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 00010101000000 +modified: 20230729120755 + +The items directive works on zettel that act as a ""table of contents"" for other zettel. +The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. +Every zettel with a certain internal structure can act as the ""table of contents"" for others. + +What is a ""table of contents""? +Basically, it is just a list of references to other zettel. + +To retrieve the items of a zettel, the software looks at first level [[list items|00001007030200]]. +If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the items list, in the ""table of contents"". + +This applies only to first level list items (ordered or unordered list), but not to deeper levels. +Only the first reference to a valid zettel is collected for the table of contents. +Following references to zettel within such an list item are ignored. + + +```` +# curl 'http://127.0.0.1:23123/z?q=00001000000000+ITEMS' +00001001000000 Introduction to the Zettelstore +00001002000000 Design goals for the Zettelstore +00001003000000 Installation of the Zettelstore software +00001004000000 Configuration of Zettelstore +00001005000000 Structure of Zettelstore +00001006000000 Layout of a Zettel +00001007000000 Zettelmarkup +00001008000000 Other Markup Languages +00001010000000 Security +00001012000000 API +00001014000000 Web user interface +00001017000000 Tips and Tricks +00001018000000 Troubleshooting +```` ADDED docs/manual/00001007721200.zettel Index: docs/manual/00001007721200.zettel ================================================================== --- /dev/null +++ docs/manual/00001007721200.zettel @@ -0,0 +1,44 @@ +id: 00001007721200 +title: Query: Unlinked Directive +role: manual +tags: #api #manual #zettelstore +syntax: zmk +created: 20211119133357 +modified: 20230731163343 + +The value of a personal Zettelstore is determined in part by explicit connections between related zettel. +If the number of zettel grow, some of these connections are missing. +There are various reasons for this. +Maybe, you forgot that a zettel exists. +Or you add a zettel later, but forgot that previous zettel already mention its title. + +__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel. + +To retrieve unlinked references to an existing zettel, use the query ''{ID} UNLINKED''. + +```` +# curl 'http://127.0.0.1:23123/z?q=00001012000000+UNLINKED' +00001012921200 API: Encoding of Zettel Access Rights +```` + +This returns all zettel (in this case: only one) that references the title of the given Zettel, but does not references it directly. + +In addition you may add __phrases__ if you do not want to scan for the title of the given zettel. + +``` +# curl 'http://localhost:23123/z?q=00001012054400+UNLINKED+PHRASE+API' +00001012050600 API: Provide an access token +00001012921200 API: Encoding of Zettel Access Rights +00001012080200 API: Check for authentication +00001012080500 API: Refresh internal data +00001012050200 API: Authenticate a client +00001010040700 Access token +``` + +This finds all zettel that does contain the phrase ""API"" but does not directly reference the given zettel. + +The directive searches within all zettel whether the title of the specified zettel occurs there. +The other zettel must not link to the specified zettel. +The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting. +The match must be exact, but is case-insensitive. +For example ""API"" does not match ""API:"". ADDED docs/manual/00001007770000.zettel Index: docs/manual/00001007770000.zettel ================================================================== --- /dev/null +++ docs/manual/00001007770000.zettel @@ -0,0 +1,18 @@ +id: 00001007770000 +title: Query: Action List +role: manual +tags: #manual #search #zettelstore +syntax: zmk +created: 20230707205246 +modified: 20230707205532 + +With a [[list of zettel identifier|00001007710000]], a [[query directives|00001007720000]], or a [[search expression|00001007701000]], a list of zettel is selected. +__Actions__ allow to modify this list to a certain degree. + +Which actions are allowed depends on the context. +However, actions are further separated into __parameter action__ and __aggregate actions__. +A parameter action just sets a parameter for an aggregate action. +An aggregate action transforms the list of selected zettel into a different, aggregate form. +Only the first aggregate form is executed, following aggregate actions are ignored. + +In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. Index: docs/manual/00001007780000.zettel ================================================================== --- docs/manual/00001007780000.zettel +++ docs/manual/00001007780000.zettel @@ -2,19 +2,29 @@ title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20230411162000 +modified: 20230731160413 ``` -QueryExpression := ContextExpression? SearchExpression ActionExpression? -ContextExpression := "CONTEXT" SPACE+ ZID (SPACE+ ContextDetail)*. +QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression? +ZettelList := (ZID (SPACE+ ZID)*). +ZID := '0'+ ('1' .. '9'') DIGIT* + | ('1' .. '9') DIGIT*. +QueryDirective := ContextDirective + | IdentDirective + | ItemsDirective + | UnlinkedDirective. +ContextDirective := "CONTEXT" (SPACE+ ContextDetail)*. ContextDetail := "BACKWARD" | "FORWARD" | "COST" SPACE+ PosInt | "MAX" SPACE+ PosInt. +IdentDirective := IDENT. +ItemsDirective := ITEMS. +UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*. SearchExpression := SearchTerm (SPACE+ SearchTerm)*. SearchTerm := SearchOperator? SearchValue | SearchKey SearchOperator SearchValue? | SearchKey ExistOperator | "OR" @@ -24,13 +34,13 @@ | "OFFSET" SPACE+ PosInt | "LIMIT" SPACE+ PosInt. SearchValue := Word. SearchKey := MetadataKey. SearchOperator := '!' - | ('!')? ('~' | ':' | '<' | '>'). + | ('!')? ('~' | ':' | '[' | '}'). ExistOperator := '?' | '!' '?'. PosInt := '0' | ('1' .. '9') DIGIT*. ActionExpression := '|' (Word (SPACE+ Word)*)? Word := NO-SPACE NO-SPACE* ``` Index: docs/manual/00001007790000.zettel ================================================================== --- docs/manual/00001007790000.zettel +++ docs/manual/00001007790000.zettel @@ -2,11 +2,11 @@ title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20230420153334 +modified: 20230706155134 |= Query Expression |= Meaning | [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore | [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel | [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel @@ -13,6 +13,6 @@ | [[query:PICK 40]] | 40 random zettel, ordered by zettel identifier | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags | [[query:expire? ORDER expire]] | Zettel with an expire date, ordered from the nearest to the latest -| [[query:CONTEXT 00001007700000]] | Zettel within the context of the [[given zettel|00001007700000]] +| [[query:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]] Index: docs/manual/00001008000000.zettel ================================================================== --- docs/manual/00001008000000.zettel +++ docs/manual/00001008000000.zettel @@ -47,12 +47,12 @@ The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. ; [!svg|''svg''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!sxn|''sxn''] -: S-Expressions, as implemented by [[sxpf|https://codeberg.org/t73fde/sxpf]]. - Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of [[sxhtml|https://codeberg.org/t73fde/sxhtml]]). +: S-Expressions, as implemented by [[sx|https://zettelstore.de/sx]]. + Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]). ; [!text|''text''], [!plain|''plain''], [!txt|''txt''] : Plain text that must not be interpreted further. ; [!zmk|''zmk''] : [[Zettelmarkup|00001007000000]]. Index: docs/manual/00001012000000.zettel ================================================================== --- docs/manual/00001012000000.zettel +++ docs/manual/00001012000000.zettel @@ -2,11 +2,11 @@ title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230411164103 +modified: 20230731162018 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. @@ -29,16 +29,14 @@ * [[Create a new zettel|00001012053200]] * [[Retrieve metadata and content of an existing zettel|00001012053300]] * [[Retrieve metadata of an existing zettel|00001012053400]] * [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]] * [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]] -* [[Retrieve unlinked references to an existing zettel|00001012053900]] -* [[Retrieve zettel order within an existing zettel|00001012054000]] * [[Update metadata and content of a zettel|00001012054200]] * [[Rename a zettel|00001012054400]] * [[Delete a zettel|00001012054600]] === Various helper methods * [[Retrieve administrative data|00001012070500]] * [[Execute some commands|00001012080100]] ** [[Check for authentication|00001012080200]] ** [[Refresh internal data|00001012080500]] Index: docs/manual/00001012051200.zettel ================================================================== --- docs/manual/00001012051200.zettel +++ docs/manual/00001012051200.zettel @@ -2,11 +2,11 @@ title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230420160640 +modified: 20230703180113 To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. Always use the endpoint ''/z'' to work with a list of zettel. Without further specifications, a plain text document is returned, with one line per zettel. @@ -25,10 +25,50 @@ The list is **not** sorted, even in the these examples where it appears to be sorted. If you want to have it ordered, you must specify it with the help of a [[query expression|00001007700000]] / [[search term|00001007702000]]. See [[Query the list of all zettel|00001012051400]] how to do it. +=== Data output + +Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'': + +```sh +# curl 'http://127.0.0.1:23123/z?enc=data' +(meta-list (query "") (human "") (list (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ... +``` + +Pretty-printed, this results in: +``` +(meta-list (query "") + (human "") + (list (zettel (id "00001012921200") + (meta (title "API: Encoding of Zettel Access Rights") + (role "manual") + (tags "#api #manual #reference #zettelstore") + (syntax "zmk") + (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") + (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") + (box-number "1") + (created "00010101000000") + (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") + (modified "20220201171959") + (published "20220201171959")) + (rights 62)) + (zettel (id "00001007030100") +``` + +* The result is a list, starting with the symbol ''meta-list''. +* Then, some key/value pairs are following, also nested. +* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]]. +* ''list'' starts a list of zettel. +* ''zettel'' itself start, well, a zettel. +* ''id'' denotes the zettel identifier, encoded as a string. +* Nested in ''meta'' are the metadata, each as a key/value pair. +* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel. + +=== JSON output (deprecated) + Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'': ```sh # curl 'http://127.0.0.1:23123/z?enc=json' {"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]} Index: docs/manual/00001012051400.zettel ================================================================== --- docs/manual/00001012051400.zettel +++ docs/manual/00001012051400.zettel @@ -2,11 +2,12 @@ title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 -modified: 20230420160857 +modified: 20230731162234 +precursor: 00001012051200 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). An empty search expression will select all zettel. @@ -27,10 +28,31 @@ 00001012920500 Formats available by the API 00001012920000 Endpoints used by the API ... ``` +If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]: + +```sh +# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data' +(meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (list (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ... +``` + +The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more). +It contains the keys ''"query"'' and ''"human"'' with a string value. +Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]]. +Without a selection, the values are the empty string. +''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. +where its value is a list of zettel JSON objects. + +The symbol ''list'' starts the list of zettel data. +Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value. +Leading zeroes are removed. +Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value. +Metadata keys are encoded as a symbol, metadata values as a string. +''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel. + If you want to retrieve a JSON document: ```sh # curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=json' {"query":"title:API ORDER REVERSE id OFFSET 1","human":"title HAS API ORDER REVERSE id OFFSET 1","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...] @@ -39,14 +61,12 @@ The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects. These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel). The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings. Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value. -Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]]. -Without a selection, the values are the empty string. -''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans. +=== Aggregates An implicit precondition is that the zettel must contain the given metadata key. For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true. But the situation is different for a key like [[''url''|00001006020000#url]]. Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list. @@ -67,14 +87,38 @@ Zettel identifier are separated by a space character (U+0020). Please note that the list is **not** sorted by the role name, so the same request might result in a different order. If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore. +Of course, this list can also be returned as a data object: + +```sh +# curl 'http://127.0.0.1:23123/z?q=|role&enc=data' +(aggregate "role" (query "| role") (human "| role") (list ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519))) +``` + +The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above. +Then a string follows, which specifies the key on which the aggregate was performed. +''query'' and ''human'' have the same meaning as above. +The ''symbol'' list starts the result list of aggregates. +Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value. + +Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''. +If successful, the output is a data object: + +```sh +# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data' +(aggregate "tags" (query "| tags") (human "| tags") (list ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ... +``` + +If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''. +You see from this that actions are separated by space characters. + Of course, this list can also be returned as a JSON object: ```sh -# curl 'http://127.0.0.1:23123/z?q=|role?enc=json' +# curl 'http://127.0.0.1:23123/z?q=|role&enc=json' {"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}} ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value. @@ -88,12 +132,11 @@ ``` The JSON object only contains the key ''"map"'' with the value of another object. This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value. -If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''. -You see from this that actions are separated by space characters. +=== Actions There are two types of actions: parameters and aggregates. The following actions are supported: ; ''MINn'' (parameter) : Emit only those values with at least __n__ aggregated values. Index: docs/manual/00001012053200.zettel ================================================================== --- docs/manual/00001012053200.zettel +++ docs/manual/00001012053200.zettel @@ -2,11 +2,11 @@ title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 -modified: 20221219162225 +modified: 20230807124310 A zettel is created by adding it to the [[list of zettel|00001012000000]]. Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request. @@ -20,11 +20,18 @@ The zettel identifier of the created zettel is returned. In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel. A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL. +=== Data input +Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''. +The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]]. + +The encoding for [[access rights|00001012921200]] must be given, but is ignored. +You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored. +=== JSON input (deprecated) Alternatively, the body of the POST request may contain a JSON object that specifies metadata and content of the zettel to be created. To do this, you must add the query parameter ''enc=json''. The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. Index: docs/manual/00001012053300.zettel ================================================================== --- docs/manual/00001012053300.zettel +++ docs/manual/00001012053300.zettel @@ -2,11 +2,11 @@ title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 -modified: 20221219160613 +modified: 20230807123533 The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. @@ -36,13 +36,49 @@ For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ... ```` -=== JSON output +=== Data output + +Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'': + +```sh +# curl 'http://127.0.0.1:23123/z/00001012053300?enc=data&part=zettel' +(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400") (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000") (box-number "1") (created "20211004093206") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500") (modified "20230703174152") (published "20230703174152") (role "manual") (syntax "zmk") (tags "#api #manual #zettelstore") (title "API: Retrieve metadata and content of an existing zettel")) (rights 62) (encoding "") (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ... +``` + +If you print the result a little bit nicer, you will see its structure: +``` +(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400") + (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000") + (box-number "1") + (created "20211004093206") + (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500") + (modified "20230703174152") + (published "20230703174152") + (role "manual") + (syntax "zmk") + (tags "#api #manual #zettelstore") + (title "API: Retrieve metadata and content of an existing zettel")) + (rights 62) + (encoding "") + (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ... +``` + +* The result is a list, starting with the symbol ''zettel''. +* Then, some key/value pairs are following, also nested. +* Nested in ''meta'' are the metadata, each as a key/value pair. +* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel. +* ''"encoding"'' states how the content is encoded. + Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. +* The zettel contents is stored as a value of the key ''content''. + Typically, text content is not encoded, and binary content is encoded via Base64. + +=== JSON output (deprecated) -Alternatively, you may retrieve the zettel as a JSON object by providinv the query parameter ''enc=json'': +You may also retrieve the zettel as a JSON object by providing the query parameter ''enc=json'': ```sh # curl 'http://127.0.0.1:23123/z/00001012053300?enc=json&part=zettel' {"id":"00001012053300","meta":{"back":"00001012000000 00001012054400","backward":"00001012000000 00001012054400 00001012920000","box-number":"1","created":"20211004093206","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200","modified":"20221219160211","published":"20221219160211","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata and content of an existing zettel"},"encoding":"","content":"The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ... ``` @@ -77,11 +113,10 @@ The name/value pairs of this objects are interpreted as the metadata of the new zettel. Please consider the [[list of supported metadata keys|00001006020000]] (and their value types). ; ''"encoding"'' : States how the content is encoded. Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]]. - Other values will result in a HTTP response status code ''400''. ; ''"content"'' : Is a string value that contains the content of the zettel to be created. Typically, text content is not encoded, and binary content is encoded via Base64. ; ''"rights"'' : An integer number that describes the [[access rights|00001012921200]] for the zettel. Index: docs/manual/00001012053400.zettel ================================================================== --- docs/manual/00001012053400.zettel +++ docs/manual/00001012053400.zettel @@ -2,11 +2,11 @@ title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 -modified: 20221219161030 +modified: 20230703175153 The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. To retrieve the plain metadata of a zettel, use the query parameter ''part=meta'' @@ -16,15 +16,46 @@ role: manual tags: #api #manual #zettelstore syntax: zmk ```` +=== Data output + +Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'': + +```sh +# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=data' +(list (meta (title "API: Retrieve metadata of an existing zettel") (role "manual") (tags "#api #manual #zettelstore") (syntax "zmk") (back "00001012000000 00001012053300") (backward "00001012000000 00001012053300") (box-number "1") (created "20210726174524") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200") (modified "20230703174515") (published "20230703174515")) (rights 62)) +``` + +Pretty-printed, this results in: +``` +(list (meta (title "API: Retrieve metadata of an existing zettel") + (role "manual") + (tags "#api #manual #zettelstore") + (syntax "zmk") + (back "00001012000000 00001012053300") + (backward "00001012000000 00001012053300") + (box-number "1") + (created "20210726174524") + (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200") + (modified "20230703174515") + (published "20230703174515")) + (rights 62)) +``` + +* The result is a list, starting with the symbol ''list''. +* Then, some key/value pairs are following, also nested. +* Nested in ''meta'' are the metadata, each as a key/value pair. +* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel. + +=== JSON output (deprecated) To return a JSON object, use also the query parameter ''enc=json''. ```sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=json' -{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20220917175233","published":"20220917175233","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62} +{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20230703174515","published":"20230703174515","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62} ``` Pretty-printed, this results in: ``` { DELETED docs/manual/00001012053900.zettel Index: docs/manual/00001012053900.zettel ================================================================== --- docs/manual/00001012053900.zettel +++ /dev/null @@ -1,80 +0,0 @@ -id: 00001012053900 -title: API: Retrieve unlinked references to an existing zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk -created: 20211119133357 -modified: 20220913152019 - -The value of a personal Zettelstore is determined in part by explicit connections between related zettel. -If the number of zettel grow, some of these connections are missing. -There are various reasons for this. -Maybe, you forgot that a zettel exists. -Or you add a zettel later, but forgot that previous zettel already mention its title. - -__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel. - -To retrieve unlinked references to an existing zettel, use the [[endpoint|00001012920000]] ''/u/{ID}''. - -```` -# curl 'http://127.0.0.1:23123/u/00001007000000' -{"id": "00001007000000","meta": {...},"rights":62,"list": [{"id": "00001012070500","meta": {...},"rights":62},...{"id": "00001006020000","meta": {...},"rights":62}]} -```` -Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] -````json -{ - "id": "00001007000000", - "meta": {...}, - "rights": 62, - "list": [ - { - "id": "00001012070500", - "meta": {...}, - "rights": 62 - }, - ... - { - "id": "00001006020000", - "meta": {...}, - "rights": 62 - } - ] -} -```` - -This call searches within all zettel whether the title of the specified zettel occurs there. -The other zettel must not link to the specified zettel. -The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting. -The match must be exact, but is case-insensitive. - -If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references, -you can specify the title phase to be searched for as a query parameter ''phrase'': - -```` -# curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown' -{"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]} -```` - -%%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content. - -=== Keys -The following top-level JSON keys are returned: -; ''id'' -: The [[zettel identifier|00001006050000]] for which the unlinked references were requested. -; ''meta'': -: The metadata of the zettel, encoded as a JSON object. -; ''rights'' -: An integer number that describes the [[access rights|00001012921200]] for the given zettel. -; ''list'' -: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe zettel with unlinked references. - -=== HTTP Status codes -; ''200'' -: Retrieval was successful, the body contains an appropriate JSON object. -; ''400'' -: Request was not valid. -; ''403'' -: You are not allowed to retrieve data of the given zettel. -; ''404'' -: Zettel not found. - You probably used a zettel identifier that is not used in the Zettelstore. DELETED docs/manual/00001012054000.zettel Index: docs/manual/00001012054000.zettel ================================================================== --- docs/manual/00001012054000.zettel +++ /dev/null @@ -1,84 +0,0 @@ -id: 00001012054000 -title: API: Retrieve zettel order within an existing zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk -modified: 20220202112451 - -Some zettel act as a ""table of contents"" for other zettel. -The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. -Every zettel with a certain internal structure can act as the ""table of contents"" for others. - -What is a ""table of contents""? -Basically, it is just a list of references to other zettel. - -To retrieve the ""table of contents"", the software looks at first level [[list items|00001007030200]]. -If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the table of contents. - -This applies only to first level list items (ordered or unordered list), but not to deeper levels. -Only the first reference to a valid zettel is collected for the table of contents. -Following references to zettel within such an list item are ignored. - -To retrieve the zettel order of an existing zettel, use the [[endpoint|00001012920000]] ''/o/{ID}''. - -```` -# curl http://127.0.0.1:23123/o/00001000000000 -{"id":"00001000000000","meta":{...},"rights":62,"list":[{"id":"00001001000000","meta":{...},"rights":62},{"id":"00001002000000","meta":{...},"rights":62},{"id":"00001003000000","meta":{...},"rights":62},{"id":"00001004000000","meta":{...},"rights":62},...,{"id":"00001014000000","meta":{...},"rights":62}]} -```` -Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] -````json -{ - "id": "00001000000000", - "meta": {...}, - "rights": 62, - "list": [ - { - "id": "00001001000000", - "meta": {...}, - "rights": 62 - }, - { - "id": "00001002000000", - "meta": {...}, - "rights": 62 - }, - { - "id": "00001003000000", - "meta": {...}, - "rights": 62 - }, - { - "id": "00001004000000", - "meta": {...}, - "rights": 62 - }, - ... - { - "id": "00001014000000", - "meta": {...}, - "rights": 62 - } - ] -} -```` - -The following top-level JSON keys are returned: -; ''id'' -: The [[zettel identifier|00001006050000]] for which the references were requested. -; ''meta'': -: The metadata of the zettel, encoded as a JSON object. -; ''rights'' -: An integer number that describes the [[access rights|00001012921200]] for the given zettel. -; ''list'' -: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe other zettel in the defined order. - -=== HTTP Status codes -; ''200'' -: Retrieval was successful, the body contains an appropriate JSON object. -; ''400'' -: Request was not valid. -; ''403'' -: You are not allowed to retrieve data of the given zettel. -; ''404'' -: Zettel not found. - You probably used a zettel identifier that is not used in the Zettelstore. Index: docs/manual/00001012054200.zettel ================================================================== --- docs/manual/00001012054200.zettel +++ docs/manual/00001012054200.zettel @@ -2,11 +2,11 @@ title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 -modified: 20221219155029 +modified: 20230807124354 Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]]. In both cases you must provide the data for the new or updated zettel in the body of the HTTP request. One difference is the endpoint. @@ -18,10 +18,18 @@ ``` # curl -X POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 ``` +=== Data input +Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''. +The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]]. + +The encoding for [[access rights|00001012921200]] must be given, but is ignored. +You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored. + +=== JSON input (deprecated) Alternatively, you can use the JSON encoding by using the query parameter ''enc=json''. ``` # curl -X PUT --data '{}' 'http://127.0.0.1:23123/z/00001012054200?enc=json' ``` Index: docs/manual/00001012070500.zettel ================================================================== --- docs/manual/00001012070500.zettel +++ docs/manual/00001012070500.zettel @@ -1,27 +1,29 @@ id: 00001012070500 title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk -modified: 20220805174216 +created: 00010101000000 +modified: 20230701160903 The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data. Currently, you can only request Zettelstore version data. ```` # curl 'http://127.0.0.1:23123/x' -{"major":0,"minor":4,"patch":0,"info":"dev","hash":"cb121cc980-dirty"} +(0 13 0 "dev" "f781dc384b-dirty") ```` -Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. +* Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. -The names ""major"", ""minor"", and ""patch"" are described in this standard. + The first three digits contain the major, minor, and patch version as described in this standard. +* The first string contains additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version. +* The second string contains data to identify the version from a developers perspective. -The name ""info"" contains sometimes some additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version. - -The name ""hash"" contains some data to identify the version from a developers perspective. +If any of the three digits has the value -1, its semantic value is unknown. +Similar, the two string might be empty. === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate JSON object. +: Retrieval was successful, the body contains an appropriate object. Index: docs/manual/00001012920000.zettel ================================================================== --- docs/manual/00001012920000.zettel +++ docs/manual/00001012920000.zettel @@ -2,11 +2,11 @@ title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 -modified: 20230407125812 +modified: 20230731162343 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' @@ -17,18 +17,16 @@ The following letters are currently in use: |= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic | ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate | | PUT: [[renew access token|00001012050400]] | -| ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder -| ''u'' | | GET [[unlinked references|00001012053900]] | **U**nlinked | ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute | | POST: [[execute command|00001012080100]] -| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel -| | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] +| ''z'' | GET: [[list zettel|00001012051200]]/[[query zettel|00001012051400]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel +| | POST: [[create new zettel|00001012053200]] | PUT: [[update zettel|00001012054200]] | | | DELETE: [[delete zettel|00001012054600]] | | | MOVE: [[rename zettel|00001012054400]] The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number. The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"". Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"". Index: docs/manual/00001012930500.zettel ================================================================== --- docs/manual/00001012930500.zettel +++ docs/manual/00001012930500.zettel @@ -2,11 +2,11 @@ title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 -modified: 20230403153609 +modified: 20230703174218 === Syntax of lists A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029). A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms. @@ -25,13 +25,13 @@ +---+---+ +---+---+ +---+---+ | V | N +-->| V | N +--> -->| V | | +-+-+---+ +-+-+---+ +-+-+---+ | | | v v v -+-------+ +-------+ +-------+ -| Elem1 | | Elem2 | | ElemN | -+-------+ +-------+ +-------+ ++-------+ +-------+ +-------+ +| Elem1 | | Elem2 | | ElemN | ++-------+ +-------+ +-------+ ~~~ ''V'' is a placeholder for a value, ''N'' is the reference to the next cell (also known as the rest / tail of the list). Above list will be represented as an symbolic expression as ''(Elem1 Elem2 ... ElemN)'' @@ -69,9 +69,9 @@ Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character. Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character. In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A). === See also -* Currently, Zettelstore uses [[Sxpf|https://codeberg.org/t73fde/sxpf]] (""Symbolic eXRression Framework"") to implement symbolic expression. +* Currently, Zettelstore uses [[sx|https://zettelstore.de/sx]] (""Symbolic eXPression Framework"") to implement symbolic expression. The project page might contain additional information about the full syntax. Zettelstore only uses lists, numbers, string, and symbols to represent zettel. Index: docs/readmezip.txt ================================================================== --- docs/readmezip.txt +++ docs/readmezip.txt @@ -15,7 +15,6 @@ possible to make it directly available for your local Zettelstore. The software, including the manual, is licensed under the European Union Public License 1.2 (or later). See the separate file LICENSE.txt. -To get in contact with the developer, send an email to ds@zettelstore.de or -follow Zettelstore on Twitter: https://twitter.com/zettelstore. +To get in contact with the developer, send an email to ds@zettelstore.de. Index: encoder/encoder.go ================================================================== --- encoder/encoder.go +++ encoder/encoder.go @@ -15,11 +15,11 @@ import ( "errors" "fmt" "io" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/zettel/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. Index: encoder/encoder_blob_test.go ================================================================== --- encoder/encoder_blob_test.go +++ encoder/encoder_blob_test.go @@ -11,11 +11,11 @@ package encoder_test import ( "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" Index: encoder/encoder_test.go ================================================================== --- encoder/encoder_test.go +++ encoder/encoder_test.go @@ -13,12 +13,12 @@ import ( "fmt" "strings" "testing" - "codeberg.org/t73fde/sxpf/reader" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" @@ -109,11 +109,11 @@ exp, err := pe.encode(encdr) if err != nil { t.Error(err) return } - val, err := reader.MakeReader(strings.NewReader(exp)).Read() + val, err := sxreader.MakeReader(strings.NewReader(exp)).Read() if err != nil { t.Error(err) return } got := val.Repr() Index: encoder/htmlenc/htmlenc.go ================================================================== --- encoder/htmlenc/htmlenc.go +++ encoder/htmlenc/htmlenc.go @@ -13,14 +13,14 @@ import ( "io" "strings" - "codeberg.org/t73fde/sxhtml" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" - "zettelstore.de/c/shtml" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" @@ -54,11 +54,11 @@ if err != nil { return 0, err } var isTitle ast.InlineSlice - var htitle *sxpf.List + var htitle *sx.Pair plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle) if hasTitle { isTitle = parser.ParseSpacedText(plainTitle) xtitle := he.tx.GetSz(&isTitle) htitle, err = he.th.Transform(xtitle) @@ -75,40 +75,40 @@ hen := he.th.Endnotes() sf := he.th.SymbolFactory() symAttr := sf.MustMake(sxhtml.NameSymAttr) - head := sxpf.MakeList(sf.MustMake("head")) + head := sx.MakeList(sf.MustMake("head")) curr := head - curr = curr.AppendBang(sxpf.Nil().Cons(sxpf.Nil().Cons(sxpf.Cons(sf.MustMake("charset"), sxpf.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta"))) + curr = curr.AppendBang(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sf.MustMake("charset"), sx.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta"))) for elem := hm; elem != nil; elem = elem.Tail() { curr = curr.AppendBang(elem.Car()) } var sb strings.Builder if hasTitle { he.textEnc.WriteInlines(&sb, &isTitle) } else { sb.Write(zn.Meta.Zid.Bytes()) } - _ = curr.AppendBang(sxpf.Nil().Cons(sxpf.MakeString(sb.String())).Cons(sf.MustMake("title"))) + _ = curr.AppendBang(sx.Nil().Cons(sx.MakeString(sb.String())).Cons(sf.MustMake("title"))) - body := sxpf.MakeList(sf.MustMake("body")) + body := sx.MakeList(sf.MustMake("body")) curr = body if hasTitle { curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1"))) } for elem := hast; elem != nil; elem = elem.Tail() { curr = curr.AppendBang(elem.Car()) } if hen != nil { - curr = curr.AppendBang(sxpf.Nil().Cons(sf.MustMake("hr"))) + curr = curr.AppendBang(sx.Nil().Cons(sf.MustMake("hr"))) _ = curr.AppendBang(hen) } - doc := sxpf.MakeList( + doc := sx.MakeList( sf.MustMake(sxhtml.NameSymDoctype), - sxpf.MakeList(sf.MustMake("html"), head, body), + sx.MakeList(sf.MustMake("html"), head, body), ) gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline) return gen.WriteHTML(w, doc) } @@ -146,14 +146,14 @@ // WriteInlines writes an inline slice to the writer func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) { hobj, err := he.th.Transform(he.tx.GetSz(is)) if err == nil { - gen := sxhtml.NewGenerator(sxpf.FindSymbolFactory(hobj)) + gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj)) length, err2 := gen.WriteListHTML(w, hobj) if err2 != nil { return length, err2 } return length, nil } return 0, err } Index: encoder/mdenc/mdenc.go ================================================================== --- encoder/mdenc/mdenc.go +++ encoder/mdenc/mdenc.go @@ -12,11 +12,11 @@ package mdenc import ( "io" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) Index: encoder/shtmlenc/shtmlenc.go ================================================================== --- encoder/shtmlenc/shtmlenc.go +++ encoder/shtmlenc/shtmlenc.go @@ -12,13 +12,13 @@ package shtmlenc import ( "io" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" - "zettelstore.de/c/shtml" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/shtml" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/zettel/meta" ) @@ -50,11 +50,11 @@ } contentSHTML, err := enc.th.Transform(enc.tx.GetSz(&zn.Ast)) if err != nil { return 0, err } - result := sxpf.Cons(metaSHTML, contentSHTML) + result := sx.Cons(metaSHTML, contentSHTML) return result.Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { Index: encoder/szenc/szenc.go ================================================================== --- encoder/szenc/szenc.go +++ encoder/szenc/szenc.go @@ -12,12 +12,12 @@ package szenc import ( "io" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) @@ -38,11 +38,11 @@ // WriteZettel writes the encoded zettel to the writer. func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) { content := enc.trans.GetSz(&zn.Ast) meta := enc.trans.GetMeta(zn.InhMeta, evalMeta) - return sxpf.MakeList(meta, content).Print(w) + return sx.MakeList(meta, content).Print(w) } // WriteMeta encodes meta data as s-expression. func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) { return enc.trans.GetMeta(m, evalMeta).Print(w) Index: encoder/szenc/transform.go ================================================================== --- encoder/szenc/transform.go +++ encoder/szenc/transform.go @@ -13,50 +13,50 @@ import ( "encoding/base64" "fmt" "strings" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/attrs" - "zettelstore.de/c/sz" + "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/sz" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) // NewTransformer returns a new transformer to create s-expressions from AST nodes. func NewTransformer() *Transformer { - sf := sxpf.MakeMappedFactory() + sf := sx.MakeMappedFactory() t := Transformer{sf: sf} t.zetSyms.InitializeZettelSymbols(sf) - t.mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{ + t.mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{ ast.VerbatimZettel: t.zetSyms.SymVerbatimZettel, ast.VerbatimProg: t.zetSyms.SymVerbatimProg, ast.VerbatimEval: t.zetSyms.SymVerbatimEval, ast.VerbatimMath: t.zetSyms.SymVerbatimMath, ast.VerbatimComment: t.zetSyms.SymVerbatimComment, ast.VerbatimHTML: t.zetSyms.SymVerbatimHTML, } - t.mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{ + t.mapRegionKindS = map[ast.RegionKind]*sx.Symbol{ ast.RegionSpan: t.zetSyms.SymRegionBlock, ast.RegionQuote: t.zetSyms.SymRegionQuote, ast.RegionVerse: t.zetSyms.SymRegionVerse, } - t.mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{ + t.mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{ ast.NestedListOrdered: t.zetSyms.SymListOrdered, ast.NestedListUnordered: t.zetSyms.SymListUnordered, ast.NestedListQuote: t.zetSyms.SymListQuote, } - t.alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{ + t.alignmentSymbolS = map[ast.Alignment]*sx.Symbol{ ast.AlignDefault: t.zetSyms.SymCell, ast.AlignLeft: t.zetSyms.SymCellLeft, ast.AlignCenter: t.zetSyms.SymCellCenter, ast.AlignRight: t.zetSyms.SymCellRight, } - t.mapRefStateLink = map[ast.RefState]*sxpf.Symbol{ + t.mapRefStateLink = map[ast.RefState]*sx.Symbol{ ast.RefStateInvalid: t.zetSyms.SymLinkInvalid, ast.RefStateZettel: t.zetSyms.SymLinkZettel, ast.RefStateSelf: t.zetSyms.SymLinkSelf, ast.RefStateFound: t.zetSyms.SymLinkFound, ast.RefStateBroken: t.zetSyms.SymLinkBroken, @@ -63,30 +63,30 @@ ast.RefStateHosted: t.zetSyms.SymLinkHosted, ast.RefStateBased: t.zetSyms.SymLinkBased, ast.RefStateQuery: t.zetSyms.SymLinkQuery, ast.RefStateExternal: t.zetSyms.SymLinkExternal, } - t.mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{ + t.mapFormatKindS = map[ast.FormatKind]*sx.Symbol{ ast.FormatEmph: t.zetSyms.SymFormatEmph, ast.FormatStrong: t.zetSyms.SymFormatStrong, ast.FormatDelete: t.zetSyms.SymFormatDelete, ast.FormatInsert: t.zetSyms.SymFormatInsert, ast.FormatSuper: t.zetSyms.SymFormatSuper, ast.FormatSub: t.zetSyms.SymFormatSub, ast.FormatQuote: t.zetSyms.SymFormatQuote, ast.FormatSpan: t.zetSyms.SymFormatSpan, } - t.mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{ + t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{ ast.LiteralZettel: t.zetSyms.SymLiteralZettel, ast.LiteralProg: t.zetSyms.SymLiteralProg, ast.LiteralInput: t.zetSyms.SymLiteralInput, ast.LiteralOutput: t.zetSyms.SymLiteralOutput, ast.LiteralComment: t.zetSyms.SymLiteralComment, ast.LiteralHTML: t.zetSyms.SymLiteralHTML, ast.LiteralMath: t.zetSyms.SymLiteralMath, } - t.mapRefStateS = map[ast.RefState]*sxpf.Symbol{ + t.mapRefStateS = map[ast.RefState]*sx.Symbol{ ast.RefStateInvalid: t.zetSyms.SymRefStateInvalid, ast.RefStateZettel: t.zetSyms.SymRefStateZettel, ast.RefStateSelf: t.zetSyms.SymRefStateSelf, ast.RefStateFound: t.zetSyms.SymRefStateFound, ast.RefStateBroken: t.zetSyms.SymRefStateBroken, @@ -93,11 +93,11 @@ ast.RefStateHosted: t.zetSyms.SymRefStateHosted, ast.RefStateBased: t.zetSyms.SymRefStateBased, ast.RefStateQuery: t.zetSyms.SymRefStateQuery, ast.RefStateExternal: t.zetSyms.SymRefStateExternal, } - t.mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{ + t.mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{ meta.TypeCredential: t.zetSyms.SymTypeCredential, meta.TypeEmpty: t.zetSyms.SymTypeEmpty, meta.TypeID: t.zetSyms.SymTypeID, meta.TypeIDSet: t.zetSyms.SymTypeIDSet, meta.TypeNumber: t.zetSyms.SymTypeNumber, @@ -111,147 +111,147 @@ } return &t } type Transformer struct { - sf sxpf.SymbolFactory + sf sx.SymbolFactory zetSyms sz.ZettelSymbols - mapVerbatimKindS map[ast.VerbatimKind]*sxpf.Symbol - mapRegionKindS map[ast.RegionKind]*sxpf.Symbol - mapNestedListKindS map[ast.NestedListKind]*sxpf.Symbol - alignmentSymbolS map[ast.Alignment]*sxpf.Symbol - mapRefStateLink map[ast.RefState]*sxpf.Symbol - mapFormatKindS map[ast.FormatKind]*sxpf.Symbol - mapLiteralKindS map[ast.LiteralKind]*sxpf.Symbol - mapRefStateS map[ast.RefState]*sxpf.Symbol - mapMetaTypeS map[*meta.DescriptionType]*sxpf.Symbol + mapVerbatimKindS map[ast.VerbatimKind]*sx.Symbol + mapRegionKindS map[ast.RegionKind]*sx.Symbol + mapNestedListKindS map[ast.NestedListKind]*sx.Symbol + alignmentSymbolS map[ast.Alignment]*sx.Symbol + mapRefStateLink map[ast.RefState]*sx.Symbol + mapFormatKindS map[ast.FormatKind]*sx.Symbol + mapLiteralKindS map[ast.LiteralKind]*sx.Symbol + mapRefStateS map[ast.RefState]*sx.Symbol + mapMetaTypeS map[*meta.DescriptionType]*sx.Symbol inVerse bool } -func (t *Transformer) GetSz(node ast.Node) *sxpf.List { +func (t *Transformer) GetSz(node ast.Node) *sx.Pair { switch n := node.(type) { case *ast.BlockSlice: return t.getBlockSlice(n) case *ast.InlineSlice: return t.getInlineSlice(*n) case *ast.ParaNode: return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara) case *ast.VerbatimNode: - return sxpf.MakeList( + return sx.MakeList( mapGetS(t, t.mapVerbatimKindS, n.Kind), t.getAttributes(n.Attrs), - sxpf.MakeString(string(n.Content)), + sx.MakeString(string(n.Content)), ) case *ast.RegionNode: return t.getRegion(n) case *ast.HeadingNode: - return sxpf.MakeList( + return sx.MakeList( t.zetSyms.SymHeading, - sxpf.Int64(int64(n.Level)), + sx.Int64(int64(n.Level)), t.getAttributes(n.Attrs), - sxpf.MakeString(n.Slug), - sxpf.MakeString(n.Fragment), + sx.MakeString(n.Slug), + sx.MakeString(n.Fragment), t.getInlineSlice(n.Inlines), ) case *ast.HRuleNode: - return sxpf.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs)) + return sx.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs)) case *ast.NestedListNode: return t.getNestedList(n) case *ast.DescriptionListNode: return t.getDescriptionList(n) case *ast.TableNode: return t.getTable(n) case *ast.TranscludeNode: - return sxpf.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref)) + return sx.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref)) case *ast.BLOBNode: return t.getBLOB(n) case *ast.TextNode: - return sxpf.MakeList(t.zetSyms.SymText, sxpf.MakeString(n.Text)) + return sx.MakeList(t.zetSyms.SymText, sx.MakeString(n.Text)) case *ast.SpaceNode: if t.inVerse { - return sxpf.MakeList(t.zetSyms.SymSpace, sxpf.MakeString(n.Lexeme)) + return sx.MakeList(t.zetSyms.SymSpace, sx.MakeString(n.Lexeme)) } - return sxpf.MakeList(t.zetSyms.SymSpace) + return sx.MakeList(t.zetSyms.SymSpace) case *ast.BreakNode: if n.Hard { - return sxpf.MakeList(t.zetSyms.SymHard) + return sx.MakeList(t.zetSyms.SymHard) } - return sxpf.MakeList(t.zetSyms.SymSoft) + return sx.MakeList(t.zetSyms.SymSoft) case *ast.LinkNode: return t.getLink(n) case *ast.EmbedRefNode: return t.getInlineSlice(n.Inlines).Tail(). - Cons(sxpf.MakeString(n.Syntax)). + Cons(sx.MakeString(n.Syntax)). Cons(t.getReference(n.Ref)). Cons(t.getAttributes(n.Attrs)). Cons(t.zetSyms.SymEmbed) case *ast.EmbedBLOBNode: return t.getEmbedBLOB(n) case *ast.CiteNode: return t.getInlineSlice(n.Inlines).Tail(). - Cons(sxpf.MakeString(n.Key)). + Cons(sx.MakeString(n.Key)). Cons(t.getAttributes(n.Attrs)). Cons(t.zetSyms.SymCite) case *ast.FootnoteNode: - text := sxpf.Nil().Cons(sxpf.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote)) + text := sx.Nil().Cons(sx.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote)) return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote) case *ast.MarkNode: return t.getInlineSlice(n.Inlines).Tail(). - Cons(sxpf.MakeString(n.Fragment)). - Cons(sxpf.MakeString(n.Slug)). - Cons(sxpf.MakeString(n.Mark)). + Cons(sx.MakeString(n.Fragment)). + Cons(sx.MakeString(n.Slug)). + Cons(sx.MakeString(n.Mark)). Cons(t.zetSyms.SymMark) case *ast.FormatNode: return t.getInlineSlice(n.Inlines).Tail(). Cons(t.getAttributes(n.Attrs)). Cons(mapGetS(t, t.mapFormatKindS, n.Kind)) case *ast.LiteralNode: - return sxpf.MakeList( + return sx.MakeList( mapGetS(t, t.mapLiteralKindS, n.Kind), t.getAttributes(n.Attrs), - sxpf.MakeString(string(n.Content)), + sx.MakeString(string(n.Content)), ) } - return sxpf.MakeList(t.zetSyms.SymUnknown, sxpf.MakeString(fmt.Sprintf("%T %v", node, node))) + return sx.MakeList(t.zetSyms.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node))) } -func (t *Transformer) getRegion(rn *ast.RegionNode) *sxpf.List { +func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair { saveInVerse := t.inVerse if rn.Kind == ast.RegionVerse { t.inVerse = true } symBlocks := t.GetSz(&rn.Blocks) t.inVerse = saveInVerse - return sxpf.MakeList( + return sx.MakeList( mapGetS(t, t.mapRegionKindS, rn.Kind), t.getAttributes(rn.Attrs), symBlocks, t.GetSz(&rn.Inlines), ) } -func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sxpf.List { - nlistObjs := make([]sxpf.Object, len(ln.Items)+1) +func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair { + nlistObjs := make([]sx.Object, len(ln.Items)+1) nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind) isCompact := isCompactList(ln.Items) for i, item := range ln.Items { if isCompact && len(item) > 0 { paragraph := t.GetSz(item[0]) nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline) continue } - itemObjs := make([]sxpf.Object, len(item)) + itemObjs := make([]sx.Object, len(item)) for j, in := range item { itemObjs[j] = t.GetSz(in) } if isCompact { - nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymInline) + nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymInline) } else { - nlistObjs[i+1] = sxpf.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock) + nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock) } } - return sxpf.MakeList(nlistObjs...) + return sx.MakeList(nlistObjs...) } func isCompactList(itemSlice []ast.ItemSlice) bool { for _, items := range itemSlice { if len(items) > 1 { return false @@ -263,165 +263,165 @@ } } return true } -func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.List { - dlObjs := make([]sxpf.Object, 2*len(dn.Descriptions)+1) +func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair { + dlObjs := make([]sx.Object, 2*len(dn.Descriptions)+1) dlObjs[0] = t.zetSyms.SymDescription for i, def := range dn.Descriptions { dlObjs[2*i+1] = t.getInlineSlice(def.Term) - descObjs := make([]sxpf.Object, len(def.Descriptions)) + descObjs := make([]sx.Object, len(def.Descriptions)) for j, b := range def.Descriptions { - dVal := make([]sxpf.Object, len(b)) + dVal := make([]sx.Object, len(b)) for k, dn := range b { dVal[k] = t.GetSz(dn) } - descObjs[j] = sxpf.MakeList(dVal...).Cons(t.zetSyms.SymBlock) - } - dlObjs[2*i+2] = sxpf.MakeList(descObjs...).Cons(t.zetSyms.SymBlock) - } - return sxpf.MakeList(dlObjs...) -} - -func (t *Transformer) getTable(tn *ast.TableNode) *sxpf.List { - tObjs := make([]sxpf.Object, len(tn.Rows)+2) + descObjs[j] = sx.MakeList(dVal...).Cons(t.zetSyms.SymBlock) + } + dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(t.zetSyms.SymBlock) + } + return sx.MakeList(dlObjs...) +} + +func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair { + tObjs := make([]sx.Object, len(tn.Rows)+2) tObjs[0] = t.zetSyms.SymTable tObjs[1] = t.getHeader(tn.Header) for i, row := range tn.Rows { tObjs[i+2] = t.getRow(row) } - return sxpf.MakeList(tObjs...) + return sx.MakeList(tObjs...) } -func (t *Transformer) getHeader(header ast.TableRow) *sxpf.List { +func (t *Transformer) getHeader(header ast.TableRow) *sx.Pair { if len(header) == 0 { - return sxpf.Nil() + return nil } return t.getRow(header) } -func (t *Transformer) getRow(row ast.TableRow) *sxpf.List { - rObjs := make([]sxpf.Object, len(row)) +func (t *Transformer) getRow(row ast.TableRow) *sx.Pair { + rObjs := make([]sx.Object, len(row)) for i, cell := range row { rObjs[i] = t.getCell(cell) } - return sxpf.MakeList(rObjs...).Cons(t.zetSyms.SymList) + return sx.MakeList(rObjs...).Cons(t.zetSyms.SymList) } -func (t *Transformer) getCell(cell *ast.TableCell) *sxpf.List { +func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair { return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align)) } -func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sxpf.List { - var lastObj sxpf.Object +func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair { + var lastObj sx.Object if bn.Syntax == meta.SyntaxSVG { - lastObj = sxpf.MakeString(string(bn.Blob)) + lastObj = sx.MakeString(string(bn.Blob)) } else { lastObj = getBase64String(bn.Blob) } - return sxpf.MakeList( + return sx.MakeList( t.zetSyms.SymBLOB, t.getInlineSlice(bn.Description), - sxpf.MakeString(bn.Syntax), + sx.MakeString(bn.Syntax), lastObj, ) } -func (t *Transformer) getLink(ln *ast.LinkNode) *sxpf.List { +func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair { return t.getInlineSlice(ln.Inlines).Tail(). - Cons(sxpf.MakeString(ln.Ref.Value)). + Cons(sx.MakeString(ln.Ref.Value)). Cons(t.getAttributes(ln.Attrs)). Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State)) } -func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.List { +func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair { tail := t.getInlineSlice(en.Inlines).Tail() if en.Syntax == meta.SyntaxSVG { - tail = tail.Cons(sxpf.MakeString(string(en.Blob))) + tail = tail.Cons(sx.MakeString(string(en.Blob))) } else { tail = tail.Cons(getBase64String(en.Blob)) } - return tail.Cons(sxpf.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB) + return tail.Cons(sx.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB) } -func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.List { - objs := make([]sxpf.Object, len(*bs)) +func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sx.Pair { + objs := make([]sx.Object, len(*bs)) for i, n := range *bs { objs[i] = t.GetSz(n) } - return sxpf.MakeList(objs...).Cons(t.zetSyms.SymBlock) + return sx.MakeList(objs...).Cons(t.zetSyms.SymBlock) } -func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sxpf.List { - objs := make([]sxpf.Object, len(is)) +func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sx.Pair { + objs := make([]sx.Object, len(is)) for i, n := range is { objs[i] = t.GetSz(n) } - return sxpf.MakeList(objs...).Cons(t.zetSyms.SymInline) + return sx.MakeList(objs...).Cons(t.zetSyms.SymInline) } -func (t *Transformer) getAttributes(a attrs.Attributes) sxpf.Object { +func (t *Transformer) getAttributes(a attrs.Attributes) sx.Object { if a.IsEmpty() { - return sxpf.Nil() + return sx.Nil() } keys := a.Keys() - objs := make([]sxpf.Object, 0, len(keys)) + objs := make([]sx.Object, 0, len(keys)) for _, k := range keys { - objs = append(objs, sxpf.Cons(sxpf.MakeString(k), sxpf.MakeString(a[k]))) + objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k]))) } - return sxpf.Nil().Cons(sxpf.MakeList(objs...)).Cons(t.zetSyms.SymQuote) + return sx.Nil().Cons(sx.MakeList(objs...)).Cons(t.zetSyms.SymQuote) } -func (t *Transformer) getReference(ref *ast.Reference) *sxpf.List { - return sxpf.MakeList( +func (t *Transformer) getReference(ref *ast.Reference) *sx.Pair { + return sx.MakeList( t.zetSyms.SymQuote, - sxpf.MakeList( + sx.MakeList( mapGetS(t, t.mapRefStateS, ref.State), - sxpf.MakeString(ref.Value), + sx.MakeString(ref.Value), ), ) } -func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.List { +func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { pairs := m.ComputedPairs() - objs := make([]sxpf.Object, 0, len(pairs)) + objs := make([]sx.Object, 0, len(pairs)) for _, p := range pairs { key := p.Key ty := m.Type(key) symType := mapGetS(t, t.mapMetaTypeS, ty) - var obj sxpf.Object + var obj sx.Object if ty.IsSet { setList := meta.ListFromValue(p.Value) - setObjs := make([]sxpf.Object, len(setList)) + setObjs := make([]sx.Object, len(setList)) for i, val := range setList { - setObjs[i] = sxpf.MakeString(val) + setObjs[i] = sx.MakeString(val) } - obj = sxpf.MakeList(setObjs...).Cons(t.zetSyms.SymList) + obj = sx.MakeList(setObjs...).Cons(t.zetSyms.SymList) } else if ty == meta.TypeZettelmarkup { is := evalMeta(p.Value) obj = t.GetSz(&is) } else { - obj = sxpf.MakeString(p.Value) + obj = sx.MakeString(p.Value) } - symKey := sxpf.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key)) - objs = append(objs, sxpf.Nil().Cons(obj).Cons(symKey).Cons(symType)) + symKey := sx.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key)) + objs = append(objs, sx.Nil().Cons(obj).Cons(symKey).Cons(symType)) } - return sxpf.MakeList(objs...).Cons(t.zetSyms.SymMeta) + return sx.MakeList(objs...).Cons(t.zetSyms.SymMeta) } -func mapGetS[T comparable](t *Transformer, m map[T]*sxpf.Symbol, k T) *sxpf.Symbol { +func mapGetS[T comparable](t *Transformer, m map[T]*sx.Symbol, k T) *sx.Symbol { if result, found := m[k]; found { return result } return t.sf.MustMake(fmt.Sprintf("**%v:NOT-FOUND**", k)) } -func getBase64String(data []byte) sxpf.String { +func getBase64String(data []byte) sx.String { var sb strings.Builder encoder := base64.NewEncoder(base64.StdEncoding, &sb) _, err := encoder.Write(data) if err == nil { err = encoder.Close() } if err == nil { - return sxpf.MakeString(sb.String()) + return sx.MakeString(sb.String()) } - return sxpf.MakeString("") + return sx.MakeString("") } Index: encoder/textenc/textenc.go ================================================================== --- encoder/textenc/textenc.go +++ encoder/textenc/textenc.go @@ -12,11 +12,11 @@ package textenc import ( "io" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/zettel/meta" ) Index: encoder/zmkenc/zmkenc.go ================================================================== --- encoder/zmkenc/zmkenc.go +++ encoder/zmkenc/zmkenc.go @@ -14,12 +14,12 @@ import ( "fmt" "io" "strings" - "zettelstore.de/c/api" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" Index: encoding/atom/atom.go ================================================================== --- encoding/atom/atom.go +++ encoding/atom/atom.go @@ -13,11 +13,11 @@ import ( "bytes" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" Index: encoding/encoding.go ================================================================== --- encoding/encoding.go +++ encoding/encoding.go @@ -12,11 +12,11 @@ package encoding import ( "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) Index: encoding/rss/rss.go ================================================================== --- encoding/rss/rss.go +++ encoding/rss/rss.go @@ -14,11 +14,11 @@ import ( "bytes" "context" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" Index: evaluator/evaluator.go ================================================================== --- evaluator/evaluator.go +++ evaluator/evaluator.go @@ -17,12 +17,12 @@ "fmt" "path" "strconv" "strings" - "zettelstore.de/c/api" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" @@ -34,13 +34,12 @@ "zettelstore.de/z/zettel/meta" ) // Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel. type Port interface { - GetMeta(context.Context, id.Zid) (*meta.Meta, error) GetZettel(context.Context, id.Zid) (zettel.Zettel, error) - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) + QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) } // EvaluateZettel evaluates the given zettel in the given context, with the // given ports, and the given environment. func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) { @@ -253,11 +252,11 @@ return &zn.Ast } func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode { q := query.Parse(expr) - ml, err := e.port.SelectMeta(e.ctx, q) + ml, err := e.port.QueryMeta(e.ctx, q) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return nil } return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel")) @@ -343,11 +342,11 @@ if ref == nil || ref.State != ast.RefStateZettel { return ln } zid := mustParseZid(ref) - _, err := e.port.GetMeta(box.NoEnrichContext(e.ctx), zid) + _, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid) if errors.Is(err, &box.ErrNotAllowed{}) { return &ast.FormatNode{ Kind: ast.FormatSpan, Attrs: ln.Attrs, Inlines: getLinkInline(ln), Index: evaluator/list.go ================================================================== --- evaluator/list.go +++ evaluator/list.go @@ -16,12 +16,12 @@ "math" "sort" "strconv" "strings" - "zettelstore.de/c/api" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/parser" Index: go.mod ================================================================== --- go.mod +++ go.mod @@ -1,17 +1,15 @@ module zettelstore.de/z -go 1.19 +go 1.20 require ( - codeberg.org/t73fde/sxhtml v0.1.1 - codeberg.org/t73fde/sxpf v0.2.0 github.com/fsnotify/fsnotify v1.6.0 - github.com/pascaldekloe/jwt v1.12.0 - github.com/yuin/goldmark v1.5.4 - golang.org/x/crypto v0.9.0 - golang.org/x/term v0.8.0 - golang.org/x/text v0.9.0 - zettelstore.de/c v0.12.0 + github.com/yuin/goldmark v1.5.5 + golang.org/x/crypto v0.12.0 + golang.org/x/term v0.11.0 + golang.org/x/text v0.12.0 + zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841 + zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 ) -require golang.org/x/sys v0.8.0 // indirect +require golang.org/x/sys v0.11.0 // indirect Index: go.sum ================================================================== --- go.sum +++ go.sum @@ -1,21 +1,17 @@ -codeberg.org/t73fde/sxhtml v0.1.1 h1:H++LyFAStN1snsCKuZ7W4tvITRX+D7lDqGSfAKcXaVU= -codeberg.org/t73fde/sxhtml v0.1.1/go.mod h1:jpEOVVCylcnOVBO5f5yPnsVl4NWfKeZH7gZZBDVyuxs= -codeberg.org/t73fde/sxpf v0.2.0 h1:ic/60KUXxx51E/YbdDF6sVvgPDp1y7kPzFO9iREUjiQ= -codeberg.org/t73fde/sxpf v0.2.0/go.mod h1:X+XmeukFGzykXmTnR1tyFmyB5kV7UdO8V1sX5gMwdRQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ= -github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U= -github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= -github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= +github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -zettelstore.de/c v0.12.0 h1:fIMZCnrOaJbdA77n1u6QXkIUWOH3Yir8fjKeA8oZEOo= -zettelstore.de/c v0.12.0/go.mod h1:DJDZXPCulexzXKbt68GPrq9mIn55k3qmUUpgpiNlvTE= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841 h1:QlOIlT2cURqM2nYEvL7zOrjiE5/ZA3iAAfslcd6u2PY= +zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841/go.mod h1:MaVH7f0eHaWB5bK0GHNUiPJxKIYGyBk9amBUSbDZM0g= +zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 h1:26xwZWEjdyL3wObczc/PKugv0EY6mgSH5NUe5kYFCt4= +zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284/go.mod h1:nsWXVrQG8RNKtoXzEMrWXNMdnpfIDU6Hb0pk56KpVKE= Index: kernel/impl/cfg.go ================================================================== --- kernel/impl/cfg.go +++ kernel/impl/cfg.go @@ -16,11 +16,11 @@ "fmt" "strconv" "strings" "sync" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" @@ -145,15 +145,16 @@ mgr.RegisterObserver(cs.observe) cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid}) } func (cs *configService) doUpdate(p box.BaseBox) error { - m, err := p.GetMeta(context.Background(), cs.orig.Zid) + z, err := p.GetZettel(context.Background(), cs.orig.Zid) cs.logger.Trace().Err(err).Msg("got config meta") if err != nil { return err } + m := z.Meta cs.mxService.Lock() for _, pair := range cs.orig.Pairs() { key := pair.Key if val, ok := m.Get(key); ok { cs.SetConfig(key, val) Index: kernel/impl/cmd.go ================================================================== --- kernel/impl/cmd.go +++ kernel/impl/cmd.go @@ -17,11 +17,11 @@ "runtime/metrics" "sort" "strconv" "strings" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" ) Index: kernel/impl/config.go ================================================================== --- kernel/impl/config.go +++ kernel/impl/config.go @@ -16,11 +16,11 @@ "sort" "strconv" "strings" "sync" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/zettel/id" ) Index: kernel/impl/core.go ================================================================== --- kernel/impl/core.go +++ kernel/impl/core.go @@ -16,11 +16,11 @@ "os" "runtime" "sync" "time" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) Index: kernel/impl/impl.go ================================================================== --- kernel/impl/impl.go +++ kernel/impl/impl.go @@ -127,11 +127,11 @@ kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname) kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version) kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout)) } -func (kern *myKernel) Start(headline, lineServer bool) { +func (kern *myKernel) Start(headline, lineServer bool, configFilename string) { for _, srvD := range kern.srvs { srvD.srv.Freeze() } if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) { kern.SetLogLevel(defaultSimpleLogLevel.String()) @@ -158,10 +158,15 @@ kern.core.GetCurConfig(kernel.CoreGoVersion), kern.core.GetCurConfig(kernel.CoreGoOS), kern.core.GetCurConfig(kernel.CoreGoArch), )) logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)") + if configFilename != "" { + logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found") + } else { + logger.Mandatory().Msg("No configuration file found / used") + } if kern.core.GetCurConfig(kernel.CoreDebug).(bool) { logger.Warn().Msg("----------------------------------------") logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION") logger.Warn().Msg("----------------------------------------") } Index: kernel/kernel.go ================================================================== --- kernel/kernel.go +++ kernel/kernel.go @@ -29,11 +29,11 @@ // Setup sets the most basic data of a software: its name, its version, // and when the version was created. Setup(progname, version string, versionTime time.Time) // Start the service. - Start(headline bool, lineServer bool) + Start(headline bool, lineServer bool, configFile string) // WaitForShutdown blocks the call until Shutdown is called. WaitForShutdown() // Shutdown the service. Waits for all concurrent activities to stop. Index: logger/message.go ================================================================== --- logger/message.go +++ logger/message.go @@ -14,11 +14,11 @@ "context" "net/http" "strconv" "sync" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" ) // Message presents a message to log. type Message struct { Index: parser/draw/draw.go ================================================================== --- parser/draw/draw.go +++ parser/draw/draw.go @@ -15,11 +15,11 @@ package draw import ( "strconv" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) Index: parser/markdown/markdown.go ================================================================== --- parser/markdown/markdown.go +++ parser/markdown/markdown.go @@ -19,11 +19,11 @@ gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" Index: parser/parser.go ================================================================== --- parser/parser.go +++ parser/parser.go @@ -14,11 +14,11 @@ import ( "context" "fmt" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/zettel" Index: parser/plain/plain.go ================================================================== --- parser/plain/plain.go +++ parser/plain/plain.go @@ -13,14 +13,14 @@ import ( "bytes" "strings" - "codeberg.org/t73fde/sxpf/builtins/pprint" - "codeberg.org/t73fde/sxpf/builtins/quote" - "codeberg.org/t73fde/sxpf/reader" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" + "zettelstore.de/sx.fossil/sxbuiltins/pprint" + "zettelstore.de/sx.fossil/sxbuiltins/quote" + "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) @@ -134,11 +134,11 @@ // TODO: check proper end return svgSrc } func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice { - rd := reader.MakeReader(bytes.NewReader(inp.Src)) + rd := sxreader.MakeReader(bytes.NewReader(inp.Src)) sf := rd.SymbolFactory() quote.InstallQuoteReader(rd, sf.MustMake("quote"), '\'') quote.InstallQuasiQuoteReader(rd, sf.MustMake("quasiquote"), '`', sf.MustMake("unquote"), ',', Index: parser/zettelmark/inline.go ================================================================== --- parser/zettelmark/inline.go +++ parser/zettelmark/inline.go @@ -13,11 +13,11 @@ import ( "bytes" "fmt" "strings" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/input" "zettelstore.de/z/zettel/meta" ) Index: parser/zettelmark/zettelmark.go ================================================================== --- parser/zettelmark/zettelmark.go +++ parser/zettelmark/zettelmark.go @@ -13,11 +13,11 @@ import ( "strings" "unicode" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) Index: parser/zettelmark/zettelmark_test.go ================================================================== --- parser/zettelmark/zettelmark_test.go +++ parser/zettelmark/zettelmark_test.go @@ -14,11 +14,11 @@ import ( "fmt" "strings" "testing" - "zettelstore.de/c/attrs" + "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" Index: query/compiled.go ================================================================== --- query/compiled.go +++ query/compiled.go @@ -25,13 +25,13 @@ pick int order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit - contextMeta []*meta.Meta - PreMatch MetaMatchFunc // Precondition for Match and Retrieve - Terms []CompiledTerm + startMeta []*meta.Meta + PreMatch MetaMatchFunc // Precondition for Match and Retrieve + Terms []CompiledTerm } // MetaMatchFunc is a function determine whethe some metadata should be selected or not. type MetaMatchFunc func(*meta.Meta) bool @@ -49,19 +49,19 @@ func (c *Compiled) isDeterministic() bool { return c.seed > 0 } // Result returns a result of the compiled search, that is achievable without iterating through a box. func (c *Compiled) Result() []*meta.Meta { - if len(c.contextMeta) == 0 { - // nil -> no context + if len(c.startMeta) == 0 { + // nil -> no directive // empty slice -> nothing found - return c.contextMeta + return c.startMeta } - result := make([]*meta.Meta, 0, len(c.contextMeta)) - for _, m := range c.contextMeta { + result := make([]*meta.Meta, 0, len(c.startMeta)) + for _, m := range c.startMeta { for _, term := range c.Terms { - if term.Match(m) { + if term.Match(m) && term.Retrieve(m.Zid) { result = append(result, m) break } } } Index: query/context.go ================================================================== --- query/context.go +++ query/context.go @@ -11,47 +11,64 @@ package query import ( "context" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -type contextDirection uint8 - -const ( - _ contextDirection = iota - dirForward - dirBackward - dirBoth -) - -func (q *Query) getContext(ctx context.Context, preMatch MetaMatchFunc, getMeta GetMetaFunc, selectMeta SelectMetaFunc) ([]*meta.Meta, error) { - if !q.zid.IsValid() { - return nil, nil - } - - start, err := getMeta(ctx, q.zid) - if err != nil { - return nil, err - } - if !preMatch(start) { - return []*meta.Meta{}, nil - } - maxCost := q.maxCost +// ContextSpec contains all specification values for calculating a context. +type ContextSpec struct { + Direction ContextDirection + MaxCost int + MaxCount int +} + +// ContextDirection specifies the direction a context should be calculated. +type ContextDirection uint8 + +const ( + ContextDirBoth ContextDirection = iota + ContextDirForward + ContextDirBackward +) + +// ContextPort is the collection of box methods needed by this directive. +type ContextPort interface { + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error) +} + +func (spec *ContextSpec) Print(pe *PrintEnv) { + pe.printSpace() + pe.writeString(api.ContextDirective) + switch spec.Direction { + case ContextDirBackward: + pe.printSpace() + pe.writeString(api.BackwardDirective) + case ContextDirForward: + pe.printSpace() + pe.writeString(api.ForwardDirective) + } + pe.printPosInt(api.CostDirective, spec.MaxCost) + pe.printPosInt(api.MaxDirective, spec.MaxCount) +} + +func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta { + maxCost := spec.MaxCost if maxCost <= 0 { maxCost = 17 } - maxCount := q.maxCount + maxCount := spec.MaxCount if maxCount <= 0 { maxCount = 200 } - tasks := newQueue(start, maxCost, maxCount, preMatch, getMeta, selectMeta) - isBackward := q.dir == dirBoth || q.dir == dirBackward - isForward := q.dir == dirBoth || q.dir == dirForward + tasks := newQueue(startSeq, maxCost, maxCount, port) + isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward + isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward result := []*meta.Meta{} for { m, cost := tasks.next() if m == nil { break @@ -65,11 +82,11 @@ for _, tag := range tags { tasks.addSameTag(ctx, tag, cost) } } } - return result, nil + return result } type ztlCtxTask struct { next *ztlCtxTask prev *ztlCtxTask @@ -76,38 +93,40 @@ meta *meta.Meta cost int } type contextQueue struct { - preMatch MetaMatchFunc - getMeta GetMetaFunc - selectMeta SelectMetaFunc - seen id.Set - first *ztlCtxTask - last *ztlCtxTask - maxCost int - limit int - tagCost map[string][]*meta.Meta -} - -func newQueue(m *meta.Meta, maxCost, limit int, preMatch MetaMatchFunc, getMeta GetMetaFunc, selectMeta SelectMetaFunc) *contextQueue { - task := &ztlCtxTask{ - next: nil, - prev: nil, - meta: m, - cost: 1, - } + port ContextPort + seen id.Set + first *ztlCtxTask + last *ztlCtxTask + maxCost int + limit int + tagCost map[string][]*meta.Meta +} + +func newQueue(startSeq []*meta.Meta, maxCost, limit int, port ContextPort) *contextQueue { result := &contextQueue{ - preMatch: preMatch, - getMeta: getMeta, - selectMeta: selectMeta, - seen: id.NewSet(), - first: task, - last: task, - maxCost: maxCost, - limit: limit, - tagCost: make(map[string][]*meta.Meta, 1024), + port: port, + seen: id.NewSet(), + first: nil, + last: nil, + maxCost: maxCost, + limit: limit, + tagCost: make(map[string][]*meta.Meta, 1024), + } + + var prev *ztlCtxTask + for _, m := range startSeq { + task := &ztlCtxTask{next: nil, prev: prev, meta: m, cost: 1} + if prev == nil { + result.first = task + } else { + prev.next = task + } + result.last = task + prev = task } return result } func (zc *contextQueue) addPair(ctx context.Context, key, value string, curCost int, isBackward, isForward bool) { @@ -151,21 +170,14 @@ func (zc *contextQueue) addID(ctx context.Context, newCost int, value string) { if zc.costMaxed(newCost) { return } - - zid, err := id.Parse(value) - if err != nil { - return - } - m, err := zc.getMeta(ctx, zid) - if err != nil { - return - } - if zc.preMatch(m) { - zc.addMeta(m, newCost) + if zid, errParse := id.Parse(value); errParse == nil { + if m, errGetMeta := zc.port.GetMeta(ctx, zid); errGetMeta == nil { + zc.addMeta(m, newCost) + } } } func (zc *contextQueue) addMeta(m *meta.Meta, newCost int) { task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost} if zc.first == nil { @@ -196,11 +208,14 @@ zc.first.prev = task zc.first = task } func (zc *contextQueue) costMaxed(newCost int) bool { - return (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() + // If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all + // other zettel that are directly reachable, without taking the cost into account. + // Of course, the limit ist still relevant. + return (len(zc.seen) > 1 && zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() } func (zc *contextQueue) addIDSet(ctx context.Context, newCost int, value string) { elems := meta.ListFromValue(value) refCost := referenceCost(newCost, len(elems)) @@ -230,11 +245,11 @@ func (zc *contextQueue) addSameTag(ctx context.Context, tag string, baseCost int) { tagMetas, found := zc.tagCost[tag] if !found { q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID) - ml, err := zc.selectMeta(ctx, q) + ml, err := zc.port.SelectMeta(ctx, nil, q) if err != nil { return } tagMetas = ml zc.tagCost[tag] = ml @@ -242,13 +257,11 @@ cost := tagCost(baseCost, len(tagMetas)) if zc.costMaxed(cost) { return } for _, m := range tagMetas { - if zc.preMatch(m) { // selectMeta will not check preMatch - zc.addMeta(m, cost) - } + zc.addMeta(m, cost) } } func tagCost(baseCost, numTags int) int { if numTags < 8 { @@ -281,7 +294,7 @@ return nil, -1 } func (zc *contextQueue) hasLimit() bool { limit := zc.limit - return limit > 0 && len(zc.seen) > limit + return limit > 0 && len(zc.seen) >= limit } Index: query/parser.go ================================================================== --- query/parser.go +++ query/parser.go @@ -11,11 +11,11 @@ package query import ( "strconv" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/input" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -54,80 +54,135 @@ } return false } const ( - actionSeparatorChar = '|' - existOperatorChar = '?' - searchOperatorNotChar = '!' - searchOperatorEqualChar = '=' - searchOperatorHasChar = ':' - searchOperatorPrefixChar = '>' - searchOperatorSuffixChar = '<' - searchOperatorMatchChar = '~' - - kwBackward = "BACKWARD" - kwContext = api.ContextDirective - kwCost = "COST" - kwForward = "FORWARD" - kwMax = "MAX" - kwLimit = "LIMIT" - kwOffset = "OFFSET" - kwOr = "OR" - kwOrder = "ORDER" - kwPick = "PICK" - kwRandom = "RANDOM" - kwReverse = "REVERSE" + actionSeparatorChar = '|' + existOperatorChar = '?' + searchOperatorNotChar = '!' + searchOperatorEqualChar = '=' + searchOperatorHasChar = ':' + searchOperatorPrefixChar = '[' + searchOperatorSuffixChar = ']' + searchOperatorMatchChar = '~' + searchOperatorLessChar = '<' + searchOperatorGreaterChar = '>' ) func (ps *parserState) parse(q *Query) *Query { - q = ps.parseContext(q) + ps.skipSpace() + if ps.mustStop() { + return q + } inp := ps.inp + firstPos := inp.Pos + zidSet := id.NewSet() + for { + pos := inp.Pos + zid, found := ps.scanZid() + if !found { + inp.SetPos(pos) + break + } + if !zidSet.Contains(zid) { + zidSet.Zid(zid) + q = createIfNeeded(q) + q.zids = append(q.zids, zid) + } + ps.skipSpace() + if ps.mustStop() { + q.zids = nil + break + } + } + + hasContext := false + for { + ps.skipSpace() + if ps.mustStop() { + break + } + pos := inp.Pos + if ps.acceptSingleKw(api.ContextDirective) { + if hasContext { + inp.SetPos(pos) + break + } + q = ps.parseContext(q, pos) + hasContext = true + continue + } + inp.SetPos(pos) + if q == nil || len(q.zids) == 0 { + break + } + if ps.acceptSingleKw(api.IdentDirective) { + q.directives = append(q.directives, &IdentSpec{}) + continue + } + inp.SetPos(pos) + if ps.acceptSingleKw(api.ItemsDirective) { + q.directives = append(q.directives, &ItemsSpec{}) + continue + } + inp.SetPos(pos) + if ps.acceptSingleKw(api.UnlinkedDirective) { + q = ps.parseUnlinked(q) + continue + } + inp.SetPos(pos) + break + } + if q != nil && len(q.directives) == 0 { + inp.SetPos(firstPos) // No directive -> restart at beginning + q.zids = nil + } + for { ps.skipSpace() if ps.mustStop() { break } pos := inp.Pos - if ps.acceptSingleKw(kwOr) { + if ps.acceptSingleKw(api.OrDirective) { q = createIfNeeded(q) if !q.terms[len(q.terms)-1].isEmpty() { q.terms = append(q.terms, conjTerms{}) } continue } inp.SetPos(pos) - if ps.acceptSingleKw(kwRandom) { + if ps.acceptSingleKw(api.RandomDirective) { q = createIfNeeded(q) if len(q.order) == 0 { q.order = []sortOrder{{"", false}} } continue } inp.SetPos(pos) - if ps.acceptKwArgs(kwPick) { + if ps.acceptKwArgs(api.PickDirective) { if s, ok := ps.parsePick(q); ok { q = s continue } } inp.SetPos(pos) - if ps.acceptKwArgs(kwOrder) { + if ps.acceptKwArgs(api.OrderDirective) { if s, ok := ps.parseOrder(q); ok { q = s continue } } inp.SetPos(pos) - if ps.acceptKwArgs(kwOffset) { + if ps.acceptKwArgs(api.OffsetDirective) { if s, ok := ps.parseOffset(q); ok { q = s continue } } inp.SetPos(pos) - if ps.acceptKwArgs(kwLimit) { + if ps.acceptKwArgs(api.LimitDirective) { if s, ok := ps.parseLimit(q); ok { q = s continue } } @@ -139,87 +194,108 @@ q = ps.parseText(q) } return q } -func (ps *parserState) parseContext(q *Query) *Query { - inp := ps.inp - ps.skipSpace() - if ps.mustStop() { - return q - } - pos := inp.Pos - if !ps.acceptSingleKw(kwContext) { - inp.SetPos(pos) - return q - } - ps.skipSpace() - if ps.mustStop() { - inp.SetPos(pos) - return q - } - zid, ok := ps.scanZid() - if !ok { - inp.SetPos(pos) - return q - } - - q = createIfNeeded(q) - q.zid = zid - q.dir = dirBoth - - for { - ps.skipSpace() - if ps.mustStop() { - return q - } - pos = inp.Pos - if ps.acceptSingleKw(kwBackward) { - q.dir = dirBackward - continue - } - inp.SetPos(pos) - if ps.acceptSingleKw(kwForward) { - q.dir = dirForward - continue - } - inp.SetPos(pos) - if ps.acceptKwArgs(kwCost) { - if ps.parseCost(q) { - continue - } - } - inp.SetPos(pos) - if ps.acceptKwArgs(kwMax) { - if ps.parseCount(q) { - continue - } - } - inp.SetPos(pos) - return q - } -} -func (ps *parserState) parseCost(q *Query) bool { - num, ok := ps.scanPosInt() - if !ok { - return false - } - if q.maxCost == 0 || q.maxCost >= num { - q.maxCost = num - } - return true -} -func (ps *parserState) parseCount(q *Query) bool { - num, ok := ps.scanPosInt() - if !ok { - return false - } - if q.maxCount == 0 || q.maxCount >= num { - q.maxCount = num - } - return true -} +func (ps *parserState) parseContext(q *Query, pos int) *Query { + inp := ps.inp + + if q == nil || len(q.zids) == 0 { + ps.skipSpace() + if ps.mustStop() { + inp.SetPos(pos) + return q + } + zid, ok := ps.scanZid() + if !ok { + inp.SetPos(pos) + return q + } + q = createIfNeeded(q) + q.zids = append(q.zids, zid) + } + + spec := &ContextSpec{} + for { + ps.skipSpace() + if ps.mustStop() { + break + } + pos = inp.Pos + if ps.acceptSingleKw(api.BackwardDirective) { + spec.Direction = ContextDirBackward + continue + } + inp.SetPos(pos) + if ps.acceptSingleKw(api.ForwardDirective) { + spec.Direction = ContextDirForward + continue + } + inp.SetPos(pos) + if ps.acceptKwArgs(api.CostDirective) { + if ps.parseCost(spec) { + continue + } + } + inp.SetPos(pos) + if ps.acceptKwArgs(api.MaxDirective) { + if ps.parseCount(spec) { + continue + } + } + + inp.SetPos(pos) + break + } + q.directives = append(q.directives, spec) + return q +} +func (ps *parserState) parseCost(spec *ContextSpec) bool { + num, ok := ps.scanPosInt() + if !ok { + return false + } + if spec.MaxCost == 0 || spec.MaxCost >= num { + spec.MaxCost = num + } + return true +} +func (ps *parserState) parseCount(spec *ContextSpec) bool { + num, ok := ps.scanPosInt() + if !ok { + return false + } + if spec.MaxCount == 0 || spec.MaxCount >= num { + spec.MaxCount = num + } + return true +} + +func (ps *parserState) parseUnlinked(q *Query) *Query { + inp := ps.inp + + spec := &UnlinkedSpec{} + for { + ps.skipSpace() + if ps.mustStop() { + break + } + pos := inp.Pos + if ps.acceptKwArgs(api.PhraseDirective) { + if word := ps.scanWord(); len(word) > 0 { + spec.words = append(spec.words, string(word)) + continue + } + } + + inp.SetPos(pos) + break + } + q.directives = append(q.directives, spec) + return q +} + func (ps *parserState) parsePick(q *Query) (*Query, bool) { num, ok := ps.scanPosInt() if !ok { return q, false } @@ -230,11 +306,11 @@ return q, true } func (ps *parserState) parseOrder(q *Query) (*Query, bool) { reverse := false - if ps.acceptKwArgs(kwReverse) { + if ps.acceptKwArgs(api.ReverseDirective) { reverse = true } word := ps.scanWord() if len(word) == 0 { return q, false @@ -348,11 +424,12 @@ for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() { if allowKey { switch inp.Ch { case searchOperatorNotChar, existOperatorChar, searchOperatorEqualChar, searchOperatorHasChar, - searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar: + searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar, + searchOperatorLessChar, searchOperatorGreaterChar: allowKey = false if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) { return nil, key } } @@ -422,10 +499,16 @@ inp.Next() op = cmpPrefix case searchOperatorMatchChar: inp.Next() op = cmpMatch + case searchOperatorLessChar: + inp.Next() + op = cmpLess + case searchOperatorGreaterChar: + inp.Next() + op = cmpGreater default: if negate { return cmpNoMatch, true } return cmpUnknown, false Index: query/parser_test.go ================================================================== --- query/parser_test.go +++ query/parser_test.go @@ -20,41 +20,73 @@ t.Parallel() testcases := []struct { spec string exp string }{ - {"CONTEXT", "CONTEXT"}, {"CONTEXT 0", "CONTEXT 0"}, {"CONTEXT a", "CONTEXT a"}, - {"CONTEXT 1", "CONTEXT 00000000000001"}, - {"CONTEXT 00000000000001", "CONTEXT 00000000000001"}, + {"1", "1"}, // Just a number will transform to search for that numer in all zettel + + {"1 IDENT", "00000000000001 IDENT"}, + {"IDENT", "IDENT"}, + + {"1 ITEMS", "00000000000001 ITEMS"}, + {"ITEMS", "ITEMS"}, + + {"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"}, + {"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"}, + {"00000000000001 CONTEXT", "00000000000001 CONTEXT"}, + {"100000000000001 CONTEXT", "100000000000001 CONTEXT"}, + {"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"}, + {"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"}, + {"1 CONTEXT COST ", "00000000000001 CONTEXT COST"}, + {"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"}, + {"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"}, + {"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"}, + {"1 CONTEXT | N", "00000000000001 CONTEXT | N"}, + {"1 1 CONTEXT", "00000000000001 CONTEXT"}, + {"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"}, + {"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"}, + + {"CONTEXT 0", "CONTEXT 0"}, {"CONTEXT 1", "00000000000001 CONTEXT"}, + {"CONTEXT 00000000000001", "00000000000001 CONTEXT"}, {"CONTEXT 100000000000001", "CONTEXT 100000000000001"}, - {"CONTEXT 1 BACKWARD", "CONTEXT 00000000000001 BACKWARD"}, - {"CONTEXT 1 FORWARD", "CONTEXT 00000000000001 FORWARD"}, - {"CONTEXT 1 COST 3", "CONTEXT 00000000000001 COST 3"}, {"CONTEXT 1 COST x", "CONTEXT 00000000000001 COST x"}, - {"CONTEXT 1 MAX 5", "CONTEXT 00000000000001 MAX 5"}, {"CONTEXT 1 MAX y", "CONTEXT 00000000000001 MAX y"}, - {"CONTEXT 1 MAX 5 COST 7", "CONTEXT 00000000000001 COST 7 MAX 5"}, - {"CONTEXT 1 | N", "CONTEXT 00000000000001 | N"}, + {"CONTEXT 1 BACKWARD", "00000000000001 CONTEXT BACKWARD"}, + {"CONTEXT 1 FORWARD", "00000000000001 CONTEXT FORWARD"}, + {"CONTEXT 1 COST 3", "00000000000001 CONTEXT COST 3"}, {"CONTEXT 1 COST x", "00000000000001 CONTEXT COST x"}, + {"CONTEXT 1 MAX 5", "00000000000001 CONTEXT MAX 5"}, {"CONTEXT 1 MAX y", "00000000000001 CONTEXT MAX y"}, + {"CONTEXT 1 MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"}, + {"CONTEXT 1 | N", "00000000000001 CONTEXT | N"}, + + {"1 UNLINKED", "00000000000001 UNLINKED"}, + {"UNLINKED", "UNLINKED"}, + {"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"}, + {"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"}, + {"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"}, {"key?", "key?"}, {"key!?", "key!?"}, {"b key?", "key? b"}, {"b key!?", "key!? b"}, {"key?a", "key?a"}, {"key!?a", "key!?a"}, - {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {">", ""}, {"!>", ""}, {"<", ""}, {"!<", ""}, {"~", ""}, {"!~", ""}, + {"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {"[", ""}, {"![", ""}, {"]", ""}, {"!]", ""}, {"~", ""}, {"!~", ""}, {"<", ""}, {"!<", ""}, {">", ""}, {"!>", ""}, {`a`, `a`}, {`!a`, `!a`}, {`=a`, `=a`}, {`!=a`, `!=a`}, {`:a`, `:a`}, {`!:a`, `!:a`}, - {`>a`, `>a`}, {`!>a`, `!>a`}, - {``, `key>`}, {`key!>`, `key!>`}, - {`key<`, `key<`}, {`key!<`, `key!<`}, + {`key[`, `key[`}, {`key![`, `key![`}, + {`key]`, `key]`}, {`key!]`, `key!]`}, {`key~`, `key~`}, {`key!~`, `key!~`}, + {`key<`, `key<`}, {`key!<`, `key!<`}, + {`key>`, `key>`}, {`key!>`, `key!>`}, {`key=a`, `key=a`}, {`key!=a`, `key!=a`}, {`key:a`, `key:a`}, {`key!:a`, `key!:a`}, - {`key>a`, `key>a`}, {`key!>a`, `key!>a`}, - {`keya`, `key>a`}, {`key!>a`, `key!>a`}, {`key1:a key2:b`, `key1:a key2:b`}, {`key1: key2:b`, `key1: key2:b`}, {"word key:a", "key:a word"}, {`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`}, {"PICK a", "PICK a"}, Index: query/print.go ================================================================== --- query/print.go +++ query/print.go @@ -13,27 +13,32 @@ import ( "io" "strconv" "strings" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/z/zettel/id" ) var op2string = map[compareOp]string{ - cmpExist: api.ExistOperator, - cmpNotExist: api.ExistNotOperator, - cmpEqual: api.SearchOperatorEqual, - cmpNotEqual: api.SearchOperatorNotEqual, - cmpHas: api.SearchOperatorHas, - cmpHasNot: api.SearchOperatorHasNot, - cmpPrefix: api.SearchOperatorPrefix, - cmpNoPrefix: api.SearchOperatorNoPrefix, - cmpSuffix: api.SearchOperatorSuffix, - cmpNoSuffix: api.SearchOperatorNoSuffix, - cmpMatch: api.SearchOperatorMatch, - cmpNoMatch: api.SearchOperatorNoMatch, + cmpExist: api.ExistOperator, + cmpNotExist: api.ExistNotOperator, + cmpEqual: api.SearchOperatorEqual, + cmpNotEqual: api.SearchOperatorNotEqual, + cmpHas: api.SearchOperatorHas, + cmpHasNot: api.SearchOperatorHasNot, + cmpPrefix: api.SearchOperatorPrefix, + cmpNoPrefix: api.SearchOperatorNoPrefix, + cmpSuffix: api.SearchOperatorSuffix, + cmpNoSuffix: api.SearchOperatorNoSuffix, + cmpMatch: api.SearchOperatorMatch, + cmpNoMatch: api.SearchOperatorNoMatch, + cmpLess: api.SearchOperatorLess, + cmpNoLess: api.SearchOperatorNotLess, + cmpGreater: api.SearchOperatorGreater, + cmpNoGreater: api.SearchOperatorNotGreater, } func (q *Query) String() string { var sb strings.Builder q.Print(&sb) @@ -43,12 +48,15 @@ // Print the query in a parseable form. func (q *Query) Print(w io.Writer) { if q == nil { return } - env := printEnv{w: w} - env.printContext(q) + env := PrintEnv{w: w} + env.printZids(q.zids) + for _, d := range q.directives { + d.Print(&env) + } for i, term := range q.terms { if i > 0 { env.writeString(" OR") } for _, name := range maps.Keys(term.keys) { @@ -55,68 +63,60 @@ env.printSpace() env.writeString(name) if op := term.keys[name]; op == cmpExist || op == cmpNotExist { env.writeString(op2string[op]) } else { - env.writeString(api.ExistOperator) - env.printSpace() - env.writeString(name) - env.writeString(api.ExistNotOperator) + env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator) } } for _, name := range maps.Keys(term.mvals) { env.printExprValues(name, term.mvals[name]) } if len(term.search) > 0 { env.printExprValues("", term.search) } } - env.printPosInt(kwPick, q.pick) + env.printPosInt(api.PickDirective, q.pick) env.printOrder(q.order) - env.printPosInt(kwOffset, q.offset) - env.printPosInt(kwLimit, q.limit) + env.printPosInt(api.OffsetDirective, q.offset) + env.printPosInt(api.LimitDirective, q.limit) env.printActions(q.actions) } -type printEnv struct { +// PrintEnv is an environment where queries are printed. +type PrintEnv struct { w io.Writer space bool } var bsSpace = []byte{' '} -func (pe *printEnv) printSpace() { +func (pe *PrintEnv) printSpace() { if pe.space { pe.w.Write(bsSpace) return } pe.space = true } -func (pe *printEnv) write(ch byte) { pe.w.Write([]byte{ch}) } -func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) } - -func (pe *printEnv) printContext(q *Query) { - if zid := q.zid; zid.IsValid() { - pe.writeString(kwContext) - pe.space = true - pe.printSpace() - pe.writeString(zid.String()) - switch q.dir { - case dirBackward: - pe.printSpace() - pe.writeString(kwBackward) - case dirForward: - pe.printSpace() - pe.writeString(kwForward) - } - pe.printPosInt(kwCost, q.maxCost) - pe.printPosInt(kwMax, q.maxCount) - // pe.writeString("!") - } - -} -func (pe *printEnv) printExprValues(key string, values []expValue) { +func (pe *PrintEnv) write(ch byte) { pe.w.Write([]byte{ch}) } +func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) } +func (pe *PrintEnv) writeStrings(sSeq ...string) { + for _, s := range sSeq { + io.WriteString(pe.w, s) + } +} + +func (pe *PrintEnv) printZids(zids []id.Zid) { + for i, zid := range zids { + if i > 0 { + pe.printSpace() + } + pe.writeString(zid.String()) + pe.space = true + } +} +func (pe *PrintEnv) printExprValues(key string, values []expValue) { for _, val := range values { pe.printSpace() pe.writeString(key) switch op := val.op; op { case cmpMatch: @@ -155,12 +155,15 @@ // PrintHuman the query to a writer in a human readable form. func (q *Query) PrintHuman(w io.Writer) { if q == nil { return } - env := printEnv{w: w} - env.printContext(q) + env := PrintEnv{w: w} + env.printZids(q.zids) + for _, d := range q.directives { + d.Print(&env) + } for i, term := range q.terms { if i > 0 { env.writeString(" OR ") env.space = false } @@ -195,18 +198,18 @@ env.printHumanSelectExprValues(term.search) env.space = true } } - env.printPosInt(kwPick, q.pick) + env.printPosInt(api.PickDirective, q.pick) env.printOrder(q.order) - env.printPosInt(kwOffset, q.offset) - env.printPosInt(kwLimit, q.limit) + env.printPosInt(api.OffsetDirective, q.offset) + env.printPosInt(api.LimitDirective, q.limit) env.printActions(q.actions) } -func (pe *printEnv) printHumanSelectExprValues(values []expValue) { +func (pe *PrintEnv) printHumanSelectExprValues(values []expValue) { if len(values) == 0 { pe.writeString(" MATCH ANY") return } @@ -233,10 +236,18 @@ pe.writeString(" NOT SUFFIX ") case cmpMatch: pe.writeString(" MATCH ") case cmpNoMatch: pe.writeString(" NOT MATCH ") + case cmpLess: + pe.writeString(" LESS ") + case cmpNoLess: + pe.writeString(" NOT LESS ") + case cmpGreater: + pe.writeString(" GREATER ") + case cmpNoGreater: + pe.writeString(" NOT GREATER ") default: pe.writeString(" MaTcH ") } if val.value == "" { pe.writeString("NOTHING") @@ -244,42 +255,40 @@ pe.writeString(val.value) } } } -func (pe *printEnv) printOrder(order []sortOrder) { +func (pe *PrintEnv) printOrder(order []sortOrder) { for _, o := range order { if o.isRandom() { pe.printSpace() - pe.writeString(kwRandom) + pe.writeString(api.RandomDirective) continue } pe.printSpace() - pe.writeString(kwOrder) + pe.writeString(api.OrderDirective) if o.descending { pe.printSpace() - pe.writeString(kwReverse) + pe.writeString(api.ReverseDirective) } pe.printSpace() pe.writeString(o.key) } } -func (pe *printEnv) printPosInt(key string, val int) { +func (pe *PrintEnv) printPosInt(key string, val int) { if val > 0 { pe.printSpace() - pe.writeString(key) - pe.writeString(" ") - pe.writeString(strconv.Itoa(val)) + pe.writeStrings(key, " ", strconv.Itoa(val)) } } -func (pe *printEnv) printActions(words []string) { +func (pe *PrintEnv) printActions(words []string) { if len(words) > 0 { pe.printSpace() pe.write(actionSeparatorChar) for _, word := range words { pe.printSpace() pe.writeString(word) } } } Index: query/query.go ================================================================== --- query/query.go +++ query/query.go @@ -38,15 +38,15 @@ SearchContains(s string) id.Set } // Query specifies a mechanism for querying zettel. type Query struct { - // Fields for context. Only valid if zid.IsValid(). - zid id.Zid - dir contextDirection - maxCost int - maxCount int + // Präfixed zettel identifier. + zids []id.Zid + + // Querydirectives, like CONTEXT, ... + directives []Directive // Fields to be used for selecting preMatch MetaMatchFunc // Match that must be true terms []conjTerms @@ -61,10 +61,35 @@ limit int // <= 0: no limit // Execute specification actions []string } + +// GetZids returns a slide of all specified zettel identifier. +func (q *Query) GetZids() []id.Zid { + if q == nil || len(q.zids) == 0 { + return nil + } + result := make([]id.Zid, len(q.zids)) + copy(result, q.zids) + return result +} + +// Directive are executed to process the list of metadata. +type Directive interface { + Print(*PrintEnv) +} + +// GetDirectives returns the slice of query directives. +func (q *Query) GetDirectives() []Directive { + if q == nil || len(q.directives) == 0 { + return nil + } + result := make([]Directive, len(q.directives)) + copy(result, q.directives) + return result +} type keyExistMap map[string]compareOp type expMetaValues map[string][]expValue type conjTerms struct { @@ -111,15 +136,17 @@ func (q *Query) Clone() *Query { if q == nil { return nil } c := new(Query) - c.zid = q.zid - if q.zid.IsValid() { - c.dir = q.dir - c.maxCost = q.maxCost - c.maxCount = q.maxCount + if len(q.zids) > 0 { + c.zids = make([]id.Zid, len(q.zids)) + copy(c.zids, q.zids) + } + if len(q.directives) > 0 { + c.directives = make([]Directive, len(q.directives)) + copy(c.directives, q.directives) } c.preMatch = q.preMatch c.terms = make([]conjTerms, len(q.terms)) for i, term := range q.terms { @@ -164,36 +191,46 @@ cmpNoPrefix cmpSuffix cmpNoSuffix cmpMatch cmpNoMatch + cmpLess + cmpNoLess + cmpGreater + cmpNoGreater ) var negateMap = map[compareOp]compareOp{ - cmpUnknown: cmpUnknown, - cmpExist: cmpNotExist, - cmpEqual: cmpNotEqual, - cmpNotEqual: cmpEqual, - cmpHas: cmpHasNot, - cmpHasNot: cmpHas, - cmpPrefix: cmpNoPrefix, - cmpNoPrefix: cmpPrefix, - cmpSuffix: cmpNoSuffix, - cmpNoSuffix: cmpSuffix, - cmpMatch: cmpNoMatch, - cmpNoMatch: cmpMatch, + cmpUnknown: cmpUnknown, + cmpExist: cmpNotExist, + cmpEqual: cmpNotEqual, + cmpNotEqual: cmpEqual, + cmpHas: cmpHasNot, + cmpHasNot: cmpHas, + cmpPrefix: cmpNoPrefix, + cmpNoPrefix: cmpPrefix, + cmpSuffix: cmpNoSuffix, + cmpNoSuffix: cmpSuffix, + cmpMatch: cmpNoMatch, + cmpNoMatch: cmpMatch, + cmpLess: cmpNoLess, + cmpNoLess: cmpLess, + cmpGreater: cmpNoGreater, + cmpNoGreater: cmpGreater, } func (op compareOp) negate() compareOp { return negateMap[op] } var negativeMap = map[compareOp]bool{ - cmpNotExist: true, - cmpNotEqual: true, - cmpHasNot: true, - cmpNoPrefix: true, - cmpNoSuffix: true, - cmpNoMatch: true, + cmpNotExist: true, + cmpNotEqual: true, + cmpHasNot: true, + cmpNoPrefix: true, + cmpNoSuffix: true, + cmpNoMatch: true, + cmpNoLess: true, + cmpNoGreater: true, } func (op compareOp) isNegated() bool { return negativeMap[op] } type expValue struct { @@ -241,28 +278,10 @@ q.seed = int(rand.Intn(10000) + 1) } return q } -// SetLimit sets the given limit of the query object. -func (q *Query) SetLimit(limit int) *Query { - q = createIfNeeded(q) - if limit < 0 { - limit = 0 - } - q.limit = limit - return q -} - -// GetLimit returns the current offset value. -func (q *Query) GetLimit() int { - if q == nil { - return 0 - } - return q.limit -} - // Actions returns the slice of action specifications func (q *Query) Actions() []string { if q == nil { return nil } @@ -280,11 +299,11 @@ // is calculated via metadata enrichments. func (q *Query) EnrichNeeded() bool { if q == nil { return false } - if q.zid.IsValid() { + if len(q.zids) > 0 { return true } if len(q.actions) > 0 { // Unknown, what an action will use. Example: RSS needs api.KeyPublished. return true @@ -307,59 +326,43 @@ } } return false } -// GetMetaFunc is a function that allows to retieve the metadata for a specific zid. -type GetMetaFunc func(context.Context, id.Zid) (*meta.Meta, error) - -// SelectMetaFunc is a function the returns a list of metadata based on a query. -type SelectMetaFunc func(context.Context, *Query) ([]*meta.Meta, error) - // RetrieveAndCompile queries the search index and returns a predicate // for its results and returns a matching predicate. -func (q *Query) RetrieveAndCompile(ctx context.Context, searcher Searcher, getMeta GetMetaFunc, selectMeta SelectMetaFunc) (Compiled, error) { +func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled { if q == nil { return Compiled{ PreMatch: matchAlways, Terms: []CompiledTerm{{ Match: matchAlways, Retrieve: alwaysIncluded, - }}}, nil + }}} } q = q.Clone() preMatch := q.preMatch if preMatch == nil { preMatch = matchAlways } - contextMeta, err := q.getContext( - ctx, preMatch, - func(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - m, err := getMeta(ctx, zid) - return m, err - }, - selectMeta, - ) - if err != nil { - return Compiled{}, err - } - contextSet := metaList2idSet(contextMeta) + + startSet := metaList2idSet(metaSeq) result := Compiled{ - hasQuery: true, - seed: q.seed, - pick: q.pick, - order: q.order, - offset: q.offset, - limit: q.limit, - contextMeta: contextMeta, - PreMatch: preMatch, - Terms: []CompiledTerm{}, + hasQuery: true, + seed: q.seed, + pick: q.pick, + order: q.order, + offset: q.offset, + limit: q.limit, + startMeta: metaSeq, + PreMatch: preMatch, + Terms: []CompiledTerm{}, } for _, term := range q.terms { - cTerm := term.retrieveAndCompileTerm(searcher, contextSet) + cTerm := term.retrieveAndCompileTerm(searcher, startSet) if cTerm.Retrieve == nil { if cTerm.Match == nil { // no restriction on match/retrieve -> all will match result.Terms = []CompiledTerm{{ Match: matchAlways, @@ -372,11 +375,11 @@ if cTerm.Match == nil { cTerm.Match = matchAlways } result.Terms = append(result.Terms, cTerm) } - return result, nil + return result } func metaList2idSet(ml []*meta.Meta) id.Set { if ml == nil { return nil @@ -386,21 +389,21 @@ result = result.Zid(m.Zid) } return result } -func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, contextSet id.Set) CompiledTerm { +func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm { match := ct.compileMeta() // Match might add some searches var pred RetrievePredicate if searcher != nil { pred = ct.retrieveIndex(searcher) - if contextSet != nil { + if startSet != nil { if pred == nil { - pred = contextSet.Contains + pred = startSet.Contains } else { - predSet := id.NewSetCap(len(contextSet)) - for zid := range contextSet { + predSet := id.NewSetCap(len(startSet)) + for zid := range startSet { if pred(zid) { predSet = predSet.Zid(zid) } } pred = predSet.Contains Index: query/retrieve.go ================================================================== --- query/retrieve.go +++ query/retrieve.go @@ -26,15 +26,17 @@ } type searchFunc func(string) id.Set type searchCallMap map[searchOp]searchFunc var cmpPred = map[compareOp]func(string, string) bool{ - cmpEqual: stringEqual, - cmpPrefix: strings.HasPrefix, - cmpSuffix: strings.HasSuffix, - cmpMatch: strings.Contains, - cmpHas: strings.Contains, // the "has" operator have string semantics here in a index search + cmpEqual: stringEqual, + cmpPrefix: strings.HasPrefix, + cmpSuffix: strings.HasSuffix, + cmpMatch: strings.Contains, + cmpHas: strings.Contains, // the "has" operator have string semantics here in a index search + cmpLess: strings.Contains, // in index search there is no "less", only "has" + cmpGreater: strings.Contains, // in index search there is no "greater", only "has" } func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) { pred := cmpPred[op] for k := range scm { @@ -160,11 +162,11 @@ return searcher.SearchEqual case cmpPrefix: return searcher.SearchPrefix case cmpSuffix: return searcher.SearchSuffix - case cmpMatch, cmpHas: // for index search we assume string semantics + case cmpMatch, cmpHas, cmpLess, cmpGreater: // for index search we assume string semantics return searcher.SearchContains default: panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op)) } } Index: query/select.go ================================================================== --- query/select.go +++ query/select.go @@ -10,10 +10,11 @@ package query import ( "fmt" + "strconv" "strings" "unicode/utf8" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" @@ -30,13 +31,17 @@ match matchValueFunc } // compileMeta calculates a selection func based on the given select criteria. func (ct *conjTerms) compileMeta() MetaMatchFunc { - for key := range ct.mvals { - // All queried keys must exist - ct.addKey(key, cmpExist) + for key, vals := range ct.mvals { + // All queried keys must exist, if there is at least one non-negated compare operation + // + // This is only an optimization to make selection of metadata faster. + if countNegatedOps(vals) < len(vals) { + ct.addKey(key, cmpExist) + } } for _, op := range ct.keys { if op != cmpExist && op != cmpNotExist { return matchNever } @@ -45,10 +50,20 @@ if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 { return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys) } return nil } + +func countNegatedOps(vals []expValue) int { + count := 0 + for _, val := range vals { + if val.op.isNegated() { + count++ + } + } + return count +} func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) { posSpecs = make([]matchSpec, 0, len(ct.mvals)) negSpecs = make([]matchSpec, 0, len(ct.mvals)) for key, values := range ct.mvals { @@ -96,10 +111,12 @@ return matchValueNever case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout return createMatchIDFunc(values, addSearch) case meta.TypeIDSet: return createMatchIDSetFunc(values, addSearch) + case meta.TypeNumber: + return createMatchNumberFunc(values, addSearch) case meta.TypeTagSet: return createMatchTagSetFunc(values, addSearch) case meta.TypeWord: return createMatchWordFunc(values, addSearch) case meta.TypeWordSet: @@ -109,11 +126,11 @@ } return createMatchStringFunc(values, addSearch) } func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { - preds := valuesToWordPredicates(values, addSearch) + preds := valuesToIDPredicates(values, addSearch) return func(value string) bool { for _, pred := range preds { if !pred(value) { return false } @@ -130,10 +147,22 @@ for _, pred := range preds { if !pred(ids) { return false } } + } + return true + } +} + +func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc { + preds := valuesToNumberPredicates(values, addSearch) + return func(value string) bool { + for _, pred := range preds { + if !pred(value) { + return false + } } return true } } @@ -314,10 +343,106 @@ } return result } type stringPredicate func(string) bool + +func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { + result := make([]stringPredicate, len(values)) + for i, v := range values { + value := v.value + if len(value) > 14 { + value = value[:14] + } + switch op := disambiguatedIDOp(v.op); op { + case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: + if isDigits(value) { + // Never add the strValue to search. + // Append enough zeroes to make it comparable as string. + // (an ID and a timestamp always have 14 digits) + strValue := value + "00000000000000"[:14-len(value)] + result[i] = createIDCompareFunc(strValue, op) + continue + } + fallthrough + default: + // Otherwise compare as a word. + if !op.isNegated() { + addSearch(v) // addSearch only for positive selections + } + result[i] = createWordCompareFunc(value, op) + } + } + return result +} + +func isDigits(s string) bool { + for i := 0; i < len(s); i++ { + if ch := s[i]; ch < '0' || '9' < ch { + return false + } + } + return true +} + +func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } + +func createIDCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate { + return createWordCompareFunc(cmpVal, cmpOp) +} + +func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { + result := make([]stringPredicate, len(values)) + for i, v := range values { + switch op := disambiguatedNumberOp(v.op); op { + case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater: + iValue, err := strconv.ParseInt(v.value, 10, 64) + if err == nil { + // Never add the strValue to search. + result[i] = createNumberCompareFunc(iValue, op) + continue + } + fallthrough + default: + // In all other cases, a number is treated like a word. + if !op.isNegated() { + addSearch(v) // addSearch only for positive selections + } + result[i] = createWordCompareFunc(v.value, op) + } + } + return result +} + +func disambiguatedNumberOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) } + +func createNumberCompareFunc(cmpVal int64, cmpOp compareOp) stringPredicate { + var cmpFunc func(int64) bool + switch cmpOp { + case cmpEqual: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal == cmpVal } + case cmpNotEqual: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal != cmpVal } + case cmpLess: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal < cmpVal } + case cmpNoLess: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal >= cmpVal } + case cmpGreater: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal } + case cmpNoGreater: + cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal } + default: + panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) + } + return func(metaVal string) bool { + iMetaVal, err := strconv.ParseInt(metaVal, 10, 64) + if err != nil { + return false + } + return cmpFunc(iMetaVal) + } +} func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate { result := make([]stringPredicate, len(values)) for i, v := range values { op := disambiguatedStringOp(v.op) @@ -383,10 +508,18 @@ return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) } case cmpMatch: return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) } case cmpNoMatch: return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) } + case cmpLess: + return func(metaVal string) bool { return metaVal < cmpVal } + case cmpNoLess: + return func(metaVal string) bool { return metaVal >= cmpVal } + case cmpGreater: + return func(metaVal string) bool { return metaVal > cmpVal } + case cmpNoGreater: + return func(metaVal string) bool { return metaVal <= cmpVal } case cmpHas, cmpHasNot: panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal)) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal)) } @@ -401,28 +534,32 @@ for j, v := range val { opVal := v.value // loop variable is used in closure --> save needed value switch op := disambiguateWordOp(v.op); op { case cmpEqual: addSearch(v) // addSearch only for positive selections - elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true) + fallthrough case cmpNotEqual: - elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false) + elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, op == cmpEqual) case cmpPrefix: addSearch(v) - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true) + fallthrough case cmpNoPrefix: - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false) + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, op == cmpPrefix) case cmpSuffix: addSearch(v) - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true) + fallthrough case cmpNoSuffix: - elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false) + elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, op == cmpSuffix) case cmpMatch: addSearch(v) - elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true) + fallthrough case cmpNoMatch: - elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false) + elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, op == cmpMatch) + case cmpLess, cmpNoLess: + elemPreds[j] = makeStringSetPredicate(opVal, stringLess, op == cmpLess) + case cmpGreater, cmpNoGreater: + elemPreds[j] = makeStringSetPredicate(opVal, stringGreater, op == cmpGreater) case cmpHas, cmpHasNot: panic(fmt.Sprintf("operator %d not disambiguated with value %q", op, opVal)) default: panic(fmt.Sprintf("Unknown compare operation %d with value %q", op, opVal)) } @@ -430,11 +567,13 @@ result[i] = elemPreds } return result } -func stringEqual(val1, val2 string) bool { return val1 == val2 } +func stringEqual(val1, val2 string) bool { return val1 == val2 } +func stringLess(val1, val2 string) bool { return val1 < val2 } +func stringGreater(val1, val2 string) bool { return val1 > val2 } type compareStringFunc func(val1, val2 string) bool func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate { return func(metaVals []string) bool { Index: query/select_test.go ================================================================== --- query/select_test.go +++ query/select_test.go @@ -12,19 +12,19 @@ import ( "context" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/query" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) func TestMatchZidNegate(t *testing.T) { q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense)) - compiled, _ := q.RetrieveAndCompile(context.Background(), nil, nil, nil) + compiled := q.RetrieveAndCompile(context.Background(), nil, nil) testCases := []struct { zid api.ZettelID exp bool }{ Index: query/sorter.go ================================================================== --- query/sorter.go +++ query/sorter.go @@ -11,11 +11,11 @@ package query import ( "strconv" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/meta" ) type sortFunc func(i, j int) bool ADDED query/specs.go Index: query/specs.go ================================================================== --- /dev/null +++ query/specs.go @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 query + +import "zettelstore.de/client.fossil/api" + +// IdentSpec contains all specification values to calculate the ident directive. +type IdentSpec struct{} + +func (spec *IdentSpec) Print(pe *PrintEnv) { + pe.printSpace() + pe.writeString(api.IdentDirective) +} + +// ItemsSpec contains all specification values to calculate items. +type ItemsSpec struct{} + +func (spec *ItemsSpec) Print(pe *PrintEnv) { + pe.printSpace() + pe.writeString(api.ItemsDirective) +} ADDED query/unlinked.go Index: query/unlinked.go ================================================================== --- /dev/null +++ query/unlinked.go @@ -0,0 +1,47 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2023-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 query + +import ( + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel/meta" +) + +// UnlinkedSpec contains all specification values to calculate unlinked references. +type UnlinkedSpec struct { + words []string +} + +func (spec *UnlinkedSpec) Print(pe *PrintEnv) { + pe.printSpace() + pe.writeString(api.UnlinkedDirective) + for _, word := range spec.words { + pe.writeStrings(" ", api.PhraseDirective, " ", word) + } +} + +func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string { + if words := spec.words; len(words) > 0 { + result := make([]string, len(words)) + copy(result, words) + return result + } + result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title + for _, m := range metaSeq { + title, hasTitle := m.Get(api.KeyTitle) + if !hasTitle { + continue + } + result = append(result, strfun.MakeWords(title)...) + } + return result +} Index: strfun/strfun.go ================================================================== --- strfun/strfun.go +++ strfun/strfun.go @@ -11,10 +11,11 @@ // Package strfun provides some string functions. package strfun import ( "strings" + "unicode" "unicode/utf8" ) // Length returns the number of runes in the given string. func Length(s string) int { @@ -49,5 +50,13 @@ func SplitLines(s string) []string { return strings.FieldsFunc(s, func(r rune) bool { return r == '\n' || r == '\r' }) } + +// MakeWords produces a list of words, i.e. string that were separated by +// control character, space characters, or separator characters. +func MakeWords(text string) []string { + return strings.FieldsFunc(text, func(r rune) bool { + return unicode.In(r, unicode.C, unicode.P, unicode.Z) + }) +} Index: tests/client/client_test.go ================================================================== --- tests/client/client_test.go +++ tests/client/client_test.go @@ -18,12 +18,12 @@ "net/http" "net/url" "strconv" "testing" - "zettelstore.de/c/api" - "zettelstore.de/c/client" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/client" "zettelstore.de/z/kernel" ) func nextZid(zid api.ZettelID) api.ZettelID { numVal, err := strconv.ParseUint(string(zid), 10, 64) @@ -47,14 +47,14 @@ } } func TestListZettel(t *testing.T) { const ( - ownerZettel = 46 - configRoleZettel = 28 - writerZettel = ownerZettel - 22 - readerZettel = ownerZettel - 22 + ownerZettel = 47 + configRoleZettel = 29 + writerZettel = ownerZettel - 23 + readerZettel = ownerZettel - 23 creatorZettel = 7 publicZettel = 4 ) testdata := []struct { @@ -71,11 +71,11 @@ t.Parallel() c := getClient() for i, tc := range testdata { t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) { c.SetAuth(tc.user, tc.user) - q, h, l, err := c.ListZettelJSON(context.Background(), "") + q, h, l, err := c.QueryZettelData(context.Background(), "") if err != nil { tt.Error(err) return } if q != "" { @@ -89,11 +89,11 @@ tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l) } }) } search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id" - q, h, l, err := c.ListZettelJSON(context.Background(), search) + q, h, l, err := c.QueryZettelData(context.Background(), search) if err != nil { t.Error(err) return } expQ := "role:configuration ORDER id" @@ -107,36 +107,36 @@ got := len(l) if got != configRoleZettel { t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l) } - pl, err := c.ListZettel(context.Background(), search) + pl, err := c.QueryZettel(context.Background(), search) if err != nil { t.Error(err) return } compareZettelList(t, pl, l) } -func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) { +func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) { t.Helper() if len(pl) != len(l) { - t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l)) + t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l)) } else { for i, line := range pl { if got := api.ZettelID(line[:14]); got != l[i].ID { - t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got) + t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got) } } } } -func TestGetZettelJSON(t *testing.T) { +func TestGetZettelData(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) + z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if m := z.Meta; len(m) == 0 { @@ -144,21 +144,24 @@ } if z.Content == "" || z.Encoding != "" { t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding) } - m, err := c.GetMeta(context.Background(), api.ZidDefaultHome) + mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } - if len(m) != len(z.Meta) { - t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta) + if mr.Rights == api.ZettelCanNone { + t.Error("rights must be greater zero") + } + if len(mr.Meta) != len(z.Meta) { + t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta) return } for k, v := range z.Meta { - got, ok := m[k] + got, ok := mr.Meta[k] if !ok { t.Errorf("Pure meta has no key %q", k) continue } if got != v { @@ -194,20 +197,11 @@ t.Errorf("Empty content for evaluated encoding %v", enc) } } } -func checkZid(t *testing.T, expected, got api.ZettelID) bool { - t.Helper() - if expected != got { - t.Errorf("Expected a Zid %q, but got %q", expected, got) - return false - } - return true -} - -func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) { +func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) { t.Helper() if got := api.ZettelID(l[pos].ID); got != expected { t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got) } } @@ -214,25 +208,21 @@ func TestGetZettelOrder(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate) + _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective) if err != nil { t.Error(err) return } - if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) { - return - } - l := rl.List - if got := len(l); got != 2 { + if got := len(metaSeq); got != 2 { t.Errorf("Expected list of length 2, got %d", got) return } - checkListZid(t, l, 0, api.ZidTemplateNewZettel) - checkListZid(t, l, 1, api.ZidTemplateNewUser) + checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel) + checkListZid(t, metaSeq, 1, api.ZidTemplateNewUser) } // func TestGetZettelContext(t *testing.T) { // const ( // allUserZid = api.ZettelID("20211019200500") @@ -282,20 +272,16 @@ func TestGetUnlinkedReferences(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - zl, err := c.GetUnlinkedReferences(context.Background(), api.ZidDefaultHome, nil) + _, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective) if err != nil { t.Error(err) return } - if !checkZid(t, api.ZidDefaultHome, zl.ID) { - return - } - l := zl.List - if got := len(l); got != 1 { + if got := len(metaSeq); got != 1 { t.Errorf("Expected list of length 1, got %d", got) return } } @@ -337,11 +323,11 @@ func TestListTags(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - tm, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyTags) + agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags) if err != nil { t.Error(err) return } tags := []struct { @@ -350,51 +336,51 @@ }{ {"#invisible", 1}, {"#user", 4}, {"#test", 4}, } - if len(tm) != len(tags) { - t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm) + if len(agg) != len(tags) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(agg), agg) } for _, tag := range tags { - if zl, ok := tm[tag.key]; !ok { - t.Errorf("No tag %v: %v", tag.key, tm) + if zl, ok := agg[tag.key]; !ok { + t.Errorf("No tag %v: %v", tag.key, agg) } else if len(zl) != tag.size { t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl) } } - for i, id := range tm["#user"] { - if id != tm["#test"][i] { - t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"]) + for i, id := range agg["#user"] { + if id != agg["#test"][i] { + t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"]) } } } func TestListRoles(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("owner", "owner") - rl, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyRole) + agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole) if err != nil { t.Error(err) return } exp := []string{"configuration", "user", "zettel"} - if len(rl) != len(exp) { - t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl) + if len(agg) != len(exp) { + t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(agg), agg) } for _, id := range exp { - if _, found := rl[id]; !found { + if _, found := agg[id]; !found { t.Errorf("Role map expected key %q", id) } } } func TestVersion(t *testing.T) { t.Parallel() c := getClient() - ver, err := c.GetVersionJSON(context.Background()) + ver, err := c.GetVersionInfo(context.Background()) if err != nil { t.Error(err) return } if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" { Index: tests/client/crud_test.go ================================================================== --- tests/client/crud_test.go +++ tests/client/crud_test.go @@ -13,12 +13,12 @@ import ( "context" "strings" "testing" - "zettelstore.de/c/api" - "zettelstore.de/c/client" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/client" ) // --------------------------------------------------------------------------- // Tests that change the Zettelstore must nor run parallel to other tests. @@ -57,15 +57,15 @@ } doDelete(t, c, newZid) } -func TestCreateGetRenameDeleteZettelJSON(t *testing.T) { +func TestCreateGetRenameDeleteZettelData(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("creator", "creator") - zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ + zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ Meta: nil, Encoding: "", Content: "Example", }) if err != nil { @@ -86,26 +86,26 @@ c.SetAuth("owner", "owner") doDelete(t, c, newZid) } -func TestCreateGetDeleteZettelJSON(t *testing.T) { +func TestCreateGetDeleteZettelData(t *testing.T) { // Is not to be allowed to run in parallel with other tests. c := getClient() c.SetAuth("owner", "owner") wrongModified := "19691231115959" - zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{ + zid, err := c.CreateZettelData(context.Background(), api.ZettelData{ Meta: api.ZettelMeta{ api.KeyTitle: "A\nTitle", // \n must be converted into a space api.KeyModified: wrongModified, }, }) if err != nil { t.Error("Cannot create zettel:", err) return } - z, err := c.GetZettelJSON(context.Background(), zid) + z, err := c.GetZettelData(context.Background(), zid) if err != nil { t.Error("Cannot get zettel:", zid, err) } else { exp := "A Title" if got := z.Meta[api.KeyTitle]; got != exp { @@ -150,14 +150,14 @@ } // Must delete to clean up for next tests doDelete(t, c, api.ZidDefaultHome) } -func TestUpdateZettelJSON(t *testing.T) { +func TestUpdateZettelData(t *testing.T) { c := getClient() c.SetAuth("writer", "writer") - z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) + z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := z.Meta[api.KeyTitle]; got != "Home" { @@ -166,16 +166,16 @@ } newTitle := "New Home" z.Meta[api.KeyTitle] = newTitle wrongModified := "19691231235959" z.Meta[api.KeyModified] = wrongModified - err = c.UpdateZettelJSON(context.Background(), api.ZidDefaultHome, z) + err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z) if err != nil { t.Error(err) return } - zt, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome) + zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome) if err != nil { t.Error(err) return } if got := zt.Meta[api.KeyTitle]; got != newTitle { Index: tests/client/embed_test.go ================================================================== --- tests/client/embed_test.go +++ tests/client/embed_test.go @@ -13,11 +13,11 @@ import ( "context" "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" ) const ( abcZid = api.ZettelID("20211020121000") abc10Zid = api.ZettelID("20211020121100") @@ -75,11 +75,11 @@ func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) { t.Parallel() c := getClient() c.SetAuth("reader", "reader") - zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji) + zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji) if err != nil { t.Error(err) return } expectedEnc := "base64" Index: tests/markdown_test.go ================================================================== --- tests/markdown_test.go +++ tests/markdown_test.go @@ -16,11 +16,11 @@ "fmt" "os" "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" Index: tests/regression_test.go ================================================================== --- tests/regression_test.go +++ tests/regression_test.go @@ -19,11 +19,11 @@ "os" "path/filepath" "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/encoder" Index: tools/build.go ================================================================== --- tools/build.go +++ tools/build.go @@ -26,11 +26,12 @@ "time" "zettelstore.de/z/strfun" ) -var directProxy = []string{"GOPROXY=direct"} +var envDirectProxy = []string{"GOPROXY=direct"} +var envGoVCS = []string{"GOVCS=zettelstore.de:fossil"} func executeCommand(env []string, name string, arg ...string) (string, error) { logCommand("EXEC", env, name, arg) var out strings.Builder cmd := prepareCommand(env, name, arg, &out) @@ -137,13 +138,16 @@ } return checkFossilExtra() } func checkGoTest(pkg string, testParams ...string) error { + var env []string + env = append(env, envDirectProxy...) + env = append(env, envGoVCS...) args := []string{"test", pkg} args = append(args, testParams...) - out, err := executeCommand(directProxy, "go", args...) + out, err := executeCommand(env, "go", args...) if err != nil { for _, line := range strfun.SplitLines(out) { if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") { continue } @@ -152,11 +156,11 @@ } return err } func checkGoVet() error { - out, err := executeCommand(nil, "go", "vet", "./...") + out, err := executeCommand(envGoVCS, "go", "vet", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } @@ -167,11 +171,11 @@ func checkShadow(forRelease bool) error { path, err := findExecStrict("shadow", forRelease) if path == "" { return err } - out, err := executeCommand(nil, path, "-strict", "./...") + out, err := executeCommand(envGoVCS, path, "-strict", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some shadowed variables found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } @@ -178,11 +182,11 @@ } return err } func checkStaticcheck() error { - out, err := executeCommand(nil, "staticcheck", "./...") + out, err := executeCommand(envGoVCS, "staticcheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some staticcheck problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } @@ -193,11 +197,11 @@ func checkUnparam(forRelease bool) error { path, err := findExecStrict("unparam", forRelease) if path == "" { return err } - out, err := executeCommand(nil, path, "./...") + out, err := executeCommand(envGoVCS, path, "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some unparam problems found") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } @@ -212,11 +216,11 @@ } return err } func checkGoVulncheck() error { - out, err := executeCommand(nil, "govulncheck", "./...") + out, err := executeCommand(envGoVCS, "govulncheck", "./...") if err != nil { fmt.Fprintln(os.Stderr, "Some checks failed") if len(out) > 0 { fmt.Fprintln(os.Stderr, out) } @@ -281,11 +285,11 @@ info.adminAddress = ":2323" name, arg := "go", []string{ "run", "cmd/zettelstore/main.go", "run", "-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]} logCommand("FORK", nil, name, arg) - cmd := prepareCommand(nil, name, arg, &info.out) + cmd := prepareCommand(envGoVCS, name, arg, &info.out) if !verbose { cmd.Stderr = nil } err := cmd.Start() time.Sleep(2 * time.Second) @@ -294,10 +298,11 @@ if addressInUse(info.adminAddress) { info.cmd = cmd return err } } + time.Sleep(4 * time.Second) // Wait for all zettel to be indexed. return errors.New("zettelstore did not start") } func stopZettelstore(i *zsInfo) error { conn, err := net.Dial("tcp", i.adminAddress) @@ -319,15 +324,16 @@ conn.Close() return true } func cmdBuild() error { - return doBuild(directProxy, getVersion(), "bin/zettelstore") + return doBuild(envDirectProxy, getVersion(), "bin/zettelstore") } func doBuild(env []string, version, target string) error { env = append(env, "CGO_ENABLED=0") + env = append(env, envGoVCS...) out, err := executeCommand( env, "go", "build", "-tags", "osusergo,netgo", "-trimpath", @@ -425,11 +431,12 @@ {"amd64", "windows", nil, "zettelstore.exe"}, } for _, rel := range releases { env := append([]string{}, rel.env...) env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os) - env = append(env, directProxy...) + env = append(env, envDirectProxy...) + env = append(env, envGoVCS...) zsName := filepath.Join("releases", rel.name) if err := doBuild(env, base, zsName); err != nil { return err } zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch) Index: usecase/authenticate.go ================================================================== --- usecase/authenticate.go +++ usecase/authenticate.go @@ -14,40 +14,31 @@ "context" "math/rand" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/cred" "zettelstore.de/z/logger" - "zettelstore.de/z/query" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -// AuthenticatePort is the interface used by this use case. -type AuthenticatePort interface { - GetMeta(context.Context, id.Zid) (*meta.Meta, error) - SelectMeta(context.Context, *query.Query) ([]*meta.Meta, error) -} - // Authenticate is the data for this use case. type Authenticate struct { log *logger.Logger token auth.TokenManager - port AuthenticatePort - ucGetUser GetUser + ucGetUser *GetUser } // NewAuthenticate creates a new use case. -func NewAuthenticate(log *logger.Logger, token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate { +func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate { return Authenticate{ log: log, token: token, - port: port, - ucGetUser: NewGetUser(authz, port), + ucGetUser: ucGetUser, } } // Run executes the use case. // Index: usecase/create_zettel.go ================================================================== --- usecase/create_zettel.go +++ usecase/create_zettel.go @@ -12,11 +12,11 @@ import ( "context" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" @@ -72,25 +72,33 @@ origMeta := origZettel.Meta m := meta.New(id.Invalid) if title, found := origMeta.Get(api.KeyTitle); found { m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of ")) } - m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, "")) - m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, "")) - m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, "")) + updateMetaRoleTagsSyntax(m, origMeta) m.Set(api.KeyPrecursor, origMeta.Zid.String()) return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} } + +// PrepareChild the zettel for further modification. +func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel { + origMeta := origZettel.Meta + m := origMeta.Clone() + if title, found := m.Get(api.KeyTitle); found { + m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of ")) + } + updateMetaRoleTagsSyntax(m, origMeta) + m.Set(api.KeySuperior, origMeta.Zid.String()) + return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)} +} // PrepareNew the zettel for further modification. func (*CreateZettel) PrepareNew(origZettel zettel.Zettel) zettel.Zettel { m := meta.New(id.Invalid) om := origZettel.Meta m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, "")) - m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, "")) - m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, "")) - m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, "")) + updateMetaRoleTagsSyntax(m, om) const prefixLen = len(meta.NewPrefix) for _, pair := range om.PairsRest() { if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix { m.Set(key[prefixLen:], pair.Value) @@ -98,10 +106,16 @@ } content := origZettel.Content content.TrimSpace() return zettel.Zettel{Meta: m, Content: content} } + +func updateMetaRoleTagsSyntax(m, orig *meta.Meta) { + m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, "")) + m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, "")) + m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, "")) +} func prependTitle(title, s0, s1 string) string { if len(title) > 0 { return s1 + title } Index: usecase/evaluate.go ================================================================== --- usecase/evaluate.go +++ usecase/evaluate.go @@ -23,39 +23,38 @@ "zettelstore.de/z/zettel/meta" ) // Evaluate is the data for this use case. type Evaluate struct { - rtConfig config.Config - getZettel GetZettel - getMeta GetMeta - listMeta ListMeta + rtConfig config.Config + ucGetZettel *GetZettel + ucQuery *Query } // NewEvaluate creates a new use case. -func NewEvaluate(rtConfig config.Config, getZettel GetZettel, getMeta GetMeta, listMeta ListMeta) Evaluate { +func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate { return Evaluate{ - rtConfig: rtConfig, - getZettel: getZettel, - getMeta: getMeta, - listMeta: listMeta, + rtConfig: rtConfig, + ucGetZettel: ucGetZettel, + ucQuery: ucQuery, } } // Run executes the use case. func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) { - zettel, err := uc.getZettel.Run(ctx, zid) + zettel, err := uc.ucGetZettel.Run(ctx, zid) if err != nil { return nil, err } - zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil - if err != nil { - return nil, err - } + return uc.RunZettel(ctx, zettel, syntax), nil +} +// RunZettel executes the use case for a given zettel. +func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode { + zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig) evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn) - return zn, nil + return zn } // RunBlockNode executes the use case for a metadata list. func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice { if bn == nil { @@ -71,19 +70,14 @@ is := parser.ParseMetadata(value) evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is) return is } -// GetMeta retrieves the metadata of a given zettel identifier. -func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - return uc.getMeta.Run(ctx, zid) +// GetZettel retrieves the full zettel of a given zettel identifier. +func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { + return uc.ucGetZettel.Run(ctx, zid) } -// GetZettel retrieves the full zettel of a given zettel identifier. -func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - return uc.getZettel.Run(ctx, zid) -} - -// SelectMeta returns a list of metadata that comply to the given selection criteria. -func (uc *Evaluate) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { - return uc.listMeta.Run(ctx, q) +// QueryMeta returns a list of metadata that comply to the given selection criteria. +func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { + return uc.ucQuery.Run(ctx, q) } DELETED usecase/get_all_meta.go Index: usecase/get_all_meta.go ================================================================== --- usecase/get_all_meta.go +++ /dev/null @@ -1,39 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 usecase - -import ( - "context" - - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// GetAllMetaPort is the interface used by this use case. -type GetAllMetaPort interface { - // GetAllMeta retrieves just the meta data of a specific zettel. - GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) -} - -// GetAllMeta is the data for this use case. -type GetAllMeta struct { - port GetAllMetaPort -} - -// NewGetAllMeta creates a new use case. -func NewGetAllMeta(port GetAllMetaPort) GetAllMeta { - return GetAllMeta{port: port} -} - -// Run executes the use case. -func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) { - return uc.port.GetAllMeta(ctx, zid) -} ADDED usecase/get_all_zettel.go Index: usecase/get_all_zettel.go ================================================================== --- /dev/null +++ usecase/get_all_zettel.go @@ -0,0 +1,38 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 usecase + +import ( + "context" + + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" +) + +// GetAllZettelPort is the interface used by this use case. +type GetAllZettelPort interface { + GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) +} + +// GetAllZettel is the data for this use case. +type GetAllZettel struct { + port GetAllZettelPort +} + +// NewGetAllZettel creates a new use case. +func NewGetAllZettel(port GetAllZettelPort) GetAllZettel { + return GetAllZettel{port: port} +} + +// Run executes the use case. +func (uc GetAllZettel) Run(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { + return uc.port.GetAllZettel(ctx, zid) +} DELETED usecase/get_meta.go Index: usecase/get_meta.go ================================================================== --- usecase/get_meta.go +++ /dev/null @@ -1,39 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 usecase - -import ( - "context" - - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// GetMetaPort is the interface used by this use case. -type GetMetaPort interface { - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) -} - -// GetMeta is the data for this use case. -type GetMeta struct { - port GetMetaPort -} - -// NewGetMeta creates a new use case. -func NewGetMeta(port GetMetaPort) GetMeta { - return GetMeta{port: port} -} - -// Run executes the use case. -func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) { - return uc.port.GetMeta(ctx, zid) -} Index: usecase/get_user.go ================================================================== --- usecase/get_user.go +++ usecase/get_user.go @@ -11,25 +11,26 @@ package usecase import ( "context" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/query" + "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // Use case: return user identified by meta key ident. // --------------------------------------------------- // GetUserPort is the interface used by this use case. type GetUserPort interface { - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // GetUser is the data for this use case. type GetUser struct { authz auth.AuthzManager @@ -46,17 +47,17 @@ ctx = box.NoEnrichContext(ctx) // It is important to try first with the owner. First, because another user // could give herself the same ''ident''. Second, in most cases the owner // will authenticate. - identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner()) - if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident { - return identMeta, nil + identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner()) + if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident { + return identZettel.Meta, nil } // Owner was not found or has another ident. Try via list search. q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident) - metaList, err := uc.port.SelectMeta(ctx, q) + metaList, err := uc.port.SelectMeta(ctx, nil, q) if err != nil { return nil, err } if len(metaList) < 1 { return nil, nil @@ -67,11 +68,11 @@ // Use case: return an user identified by zettel id and assert given ident value. // ------------------------------------------------------------------------------ // GetUserByZidPort is the interface used by this use case. type GetUserByZidPort interface { - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // GetUserByZid is the data for this use case. type GetUserByZid struct { port GetUserByZidPort @@ -82,15 +83,16 @@ return GetUserByZid{port: port} } // GetUser executes the use case. func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) { - userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid) + userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid) if err != nil { return nil, err } + userMeta := userZettel.Meta if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident { return nil, nil } return userMeta, nil } Index: usecase/lists.go ================================================================== --- usecase/lists.go +++ usecase/lists.go @@ -11,44 +11,22 @@ package usecase import ( "context" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/zettel/meta" ) -// ListMetaPort is the interface used by this use case. -type ListMetaPort interface { - // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) -} - -// ListMeta is the data for this use case. -type ListMeta struct { - port ListMetaPort -} - -// NewListMeta creates a new use case. -func NewListMeta(port ListMetaPort) ListMeta { - return ListMeta{port: port} -} - -// Run executes the use case. -func (uc ListMeta) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { - return uc.port.SelectMeta(ctx, q) -} - -// -------- List roles ------------------------------------------------------- +// -------- List syntax ------------------------------------------------------ // ListSyntaxPort is the interface used by this use case. type ListSyntaxPort interface { - // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // ListSyntax is the data for this use case. type ListSyntax struct { port ListSyntaxPort @@ -60,11 +38,11 @@ } // Run executes the use case. func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } result := meta.CreateArrangement(metas, api.KeySyntax) for _, syn := range parser.GetSyntaxes() { @@ -77,12 +55,11 @@ // -------- List roles ------------------------------------------------------- // ListRolesPort is the interface used by this use case. type ListRolesPort interface { - // SelectMeta returns all zettel metadata that match the selection criteria. - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) } // ListRoles is the data for this use case. type ListRoles struct { port ListRolesPort @@ -94,11 +71,11 @@ } // Run executes the use case. func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) { q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key - metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q) + metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q) if err != nil { return nil, err } return meta.CreateArrangement(metas, api.KeyRole), nil } DELETED usecase/order.go Index: usecase/order.go ================================================================== --- usecase/order.go +++ /dev/null @@ -1,54 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 usecase - -import ( - "context" - - "zettelstore.de/z/collect" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// ZettelOrderPort is the interface used by this use case. -type ZettelOrderPort interface { - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) -} - -// ZettelOrder is the data for this use case. -type ZettelOrder struct { - port ZettelOrderPort - evaluate Evaluate -} - -// NewZettelOrder creates a new use case. -func NewZettelOrder(port ZettelOrderPort, evaluate Evaluate) ZettelOrder { - return ZettelOrder{port: port, evaluate: evaluate} -} - -// Run executes the use case. -func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) ( - start *meta.Meta, result []*meta.Meta, err error, -) { - zn, err := uc.evaluate.Run(ctx, zid, syntax) - if err != nil { - return nil, nil, err - } - for _, ref := range collect.Order(zn) { - if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { - if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil { - result = append(result, m) - } - } - } - return zn.Meta, result, nil -} ADDED usecase/query.go Index: usecase/query.go ================================================================== --- /dev/null +++ usecase/query.go @@ -0,0 +1,276 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 usecase + +import ( + "context" + "errors" + "fmt" + "strings" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/z/ast" + "zettelstore.de/z/box" + "zettelstore.de/z/collect" + "zettelstore.de/z/parser" + "zettelstore.de/z/query" + "zettelstore.de/z/strfun" + "zettelstore.de/z/zettel" + "zettelstore.de/z/zettel/id" + "zettelstore.de/z/zettel/meta" +) + +// QueryPort is the interface used by this use case. +type QueryPort interface { + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) +} + +// Query is the data for this use case. +type Query struct { + port QueryPort + ucEvaluate Evaluate +} + +// NewQuery creates a new use case. +func NewQuery(port QueryPort) Query { + return Query{port: port} +} + +// SetEvaluate sets the usecase Evaluate, because of circular dependencies. +func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate } + +// Run executes the use case. +func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) { + zids := q.GetZids() + if zids == nil { + return uc.port.SelectMeta(ctx, nil, q) + } + if len(zids) == 0 { + return nil, nil + } + metaSeq, err := uc.getMetaZid(ctx, zids) + if err != nil { + return metaSeq, err + } + if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 { + return uc.port.SelectMeta(ctx, metaSeq, q) + } + return nil, nil +} + +func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) { + metaSeq := make([]*meta.Meta, 0, len(zids)) + for _, zid := range zids { + m, err := uc.port.GetMeta(ctx, zid) + if err == nil { + metaSeq = append(metaSeq, m) + continue + } + if errors.Is(err, &box.ErrNotAllowed{}) { + continue + } + return metaSeq, err + } + return metaSeq, nil +} + +func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta { + if len(directives) == 0 { + return metaSeq + } + for _, dir := range directives { + if len(metaSeq) == 0 { + return nil + } + switch ds := dir.(type) { + case *query.ContextSpec: + metaSeq = uc.processContextDirective(ctx, ds, metaSeq) + case *query.IdentSpec: + // Nothing to do. + case *query.ItemsSpec: + metaSeq = uc.processItemsDirective(ctx, ds, metaSeq) + case *query.UnlinkedSpec: + metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq) + default: + panic(fmt.Sprintf("Unknown directive %T", ds)) + } + } + if len(metaSeq) == 0 { + return nil + } + return metaSeq +} +func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta { + return spec.Execute(ctx, metaSeq, uc.port) +} + +func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta { + result := make([]*meta.Meta, 0, len(metaSeq)) + for _, m := range metaSeq { + zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, "")) + if err != nil { + continue + } + for _, ref := range collect.Order(zn) { + if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil { + if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil { + result = append(result, z.Meta) + } + } + } + } + return result +} + +func (uc *Query) processUnlinkedDirective(ctx context.Context, spec *query.UnlinkedSpec, metaSeq []*meta.Meta) []*meta.Meta { + words := spec.GetWords(metaSeq) + if len(words) == 0 { + return metaSeq + } + var sb strings.Builder + for _, word := range words { + sb.WriteString(" :") + sb.WriteString(word) + } + q := (*query.Query)(nil).Parse(sb.String()) + candidates, err := uc.port.SelectMeta(ctx, nil, q) + if err != nil { + return nil + } + metaZids := id.NewSetCap(len(metaSeq)) + refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel + for _, m := range metaSeq { + metaZids.Zid(m.Zid) + refZids.Zid(m.Zid) + for _, pair := range m.ComputedPairsRest() { + switch meta.Type(pair.Key) { + case meta.TypeID: + if zid, errParse := id.Parse(pair.Value); errParse == nil { + refZids.Zid(zid) + } + case meta.TypeIDSet: + for _, value := range meta.ListFromValue(pair.Value) { + if zid, errParse := id.Parse(value); errParse == nil { + refZids.Zid(zid) + } + } + } + } + } + candidates = filterByZid(candidates, refZids) + return uc.filterCandidates(ctx, candidates, words) +} + +func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta { + result := make([]*meta.Meta, 0, len(candidates)) + for _, m := range candidates { + if !ignoreSeq.Contains(m.Zid) { + result = append(result, m) + } + } + return result +} + +func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { + result := make([]*meta.Meta, 0, len(candidates)) +candLoop: + for _, cand := range candidates { + zettel, err := uc.port.GetZettel(ctx, cand.Zid) + if err != nil { + continue + } + v := unlinkedVisitor{ + words: words, + found: false, + } + v.text = v.joinWords(words) + + for _, pair := range zettel.Meta.Pairs() { + if meta.Type(pair.Key) != meta.TypeZettelmarkup { + continue + } + is := uc.ucEvaluate.RunMetadata(ctx, pair.Value) + ast.Walk(&v, &is) + if v.found { + result = append(result, cand) + continue candLoop + } + } + + syntax := zettel.Meta.GetDefault(api.KeySyntax, "") + if !parser.IsASTParser(syntax) { + continue + } + zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax) + ast.Walk(&v, &zn.Ast) + if v.found { + result = append(result, cand) + } + } + return result +} + +func (*unlinkedVisitor) joinWords(words []string) string { + return " " + strings.ToLower(strings.Join(words, " ")) + " " +} + +type unlinkedVisitor struct { + words []string + text string + found bool +} + +func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.InlineSlice: + v.checkWords(n) + return nil + case *ast.HeadingNode: + return nil + case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: + return nil + } + return v +} + +func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { + if len(*is) < 2*len(v.words)-1 { + return + } + for _, text := range v.splitInlineTextList(is) { + if strings.Contains(text, v.text) { + v.found = true + } + } +} + +func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { + var result []string + var curList []string + for _, in := range *is { + switch n := in.(type) { + case *ast.TextNode: + curList = append(curList, strfun.MakeWords(n.Text)...) + case *ast.SpaceNode: + default: + if curList != nil { + result = append(result, v.joinWords(curList)) + curList = nil + } + } + } + if curList != nil { + result = append(result, v.joinWords(curList)) + } + return result +} Index: usecase/rename_zettel.go ================================================================== --- usecase/rename_zettel.go +++ usecase/rename_zettel.go @@ -13,20 +13,17 @@ import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/logger" + "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // RenameZettelPort is the interface used by this use case. type RenameZettelPort interface { - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - - // Rename changes the current id to a new id. + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error } // RenameZettel is the data for this use case. type RenameZettel struct { @@ -47,19 +44,19 @@ } // Run executes the use case. func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error { noEnrichCtx := box.NoEnrichContext(ctx) - if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil { + if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil { return err } if newZid == curZid { // Nothing to do return nil } - if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil { + if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil { return &ErrZidInUse{Zid: newZid} } err := uc.port.RenameZettel(ctx, curZid, newZid) uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel") return err } DELETED usecase/unlinked_refs.go Index: usecase/unlinked_refs.go ================================================================== --- usecase/unlinked_refs.go +++ /dev/null @@ -1,181 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 usecase - -import ( - "context" - "strings" - "unicode" - - "zettelstore.de/c/api" - "zettelstore.de/z/ast" - "zettelstore.de/z/config" - "zettelstore.de/z/encoder/textenc" - "zettelstore.de/z/evaluator" - "zettelstore.de/z/parser" - "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -// UnlinkedReferencesPort is the interface used by this use case. -type UnlinkedReferencesPort interface { - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) -} - -// UnlinkedReferences is the data for this use case. -type UnlinkedReferences struct { - port UnlinkedReferencesPort - rtConfig config.Config - encText *textenc.Encoder -} - -// NewUnlinkedReferences creates a new use case. -func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences { - return UnlinkedReferences{ - port: port, - rtConfig: rtConfig, - encText: textenc.Create(), - } -} - -// Run executes the usecase with already evaluated title value. -func (uc *UnlinkedReferences) Run(ctx context.Context, phrase string, q *query.Query) ([]*meta.Meta, error) { - words := makeWords(phrase) - if len(words) == 0 { - return nil, nil - } - var sb strings.Builder - for _, word := range words { - sb.WriteString(" :") - sb.WriteString(word) - } - q = q.Parse(sb.String()) - - // Limit applies to the filtering process, not to SelectMeta - limit := q.GetLimit() - q = q.SetLimit(0) - - candidates, err := uc.port.SelectMeta(ctx, q) - if err != nil { - return nil, err - } - q = q.SetLimit(limit) // Restore limit - return q.Limit(uc.filterCandidates(ctx, candidates, words)), nil -} - -func makeWords(text string) []string { - return strings.FieldsFunc(text, func(r rune) bool { - return unicode.In(r, unicode.C, unicode.P, unicode.Z) - }) -} - -func (uc *UnlinkedReferences) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta { - result := make([]*meta.Meta, 0, len(candidates)) -candLoop: - for _, cand := range candidates { - zettel, err := uc.port.GetZettel(ctx, cand.Zid) - if err != nil { - continue - } - v := unlinkedVisitor{ - words: words, - found: false, - } - v.text = v.joinWords(words) - - for _, pair := range zettel.Meta.Pairs() { - if meta.Type(pair.Key) != meta.TypeZettelmarkup { - continue - } - is := parser.ParseMetadata(pair.Value) - evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is) - ast.Walk(&v, &is) - if v.found { - result = append(result, cand) - continue candLoop - } - } - - syntax := zettel.Meta.GetDefault(api.KeySyntax, "") - if !parser.IsASTParser(syntax) { - continue - } - zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil - if err != nil { - continue - } - evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn) - ast.Walk(&v, &zn.Ast) - if v.found { - result = append(result, cand) - } - } - return result -} - -func (*unlinkedVisitor) joinWords(words []string) string { - return " " + strings.ToLower(strings.Join(words, " ")) + " " -} - -type unlinkedVisitor struct { - words []string - text string - found bool -} - -func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor { - switch n := node.(type) { - case *ast.InlineSlice: - v.checkWords(n) - return nil - case *ast.HeadingNode: - return nil - case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode: - return nil - } - return v -} - -func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) { - if len(*is) < 2*len(v.words)-1 { - return - } - for _, text := range v.splitInlineTextList(is) { - if strings.Contains(text, v.text) { - v.found = true - } - } -} - -func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string { - var result []string - var curList []string - for _, in := range *is { - switch n := in.(type) { - case *ast.TextNode: - curList = append(curList, makeWords(n.Text)...) - case *ast.SpaceNode: - default: - if curList != nil { - result = append(result, v.joinWords(curList)) - curList = nil - } - } - } - if curList != nil { - result = append(result, v.joinWords(curList)) - } - return result -} Index: usecase/update_zettel.go ================================================================== --- usecase/update_zettel.go +++ usecase/update_zettel.go @@ -11,11 +11,11 @@ package usecase import ( "context" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" Index: web/adapter/api/api.go ================================================================== --- web/adapter/api/api.go +++ web/adapter/api/api.go @@ -15,11 +15,11 @@ "bytes" "context" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" Index: web/adapter/api/command.go ================================================================== --- web/adapter/api/command.go +++ web/adapter/api/command.go @@ -12,11 +12,11 @@ import ( "context" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/usecase" ) // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( Index: web/adapter/api/create_zettel.go ================================================================== --- web/adapter/api/create_zettel.go +++ web/adapter/api/create_zettel.go @@ -12,17 +12,22 @@ import ( "bytes" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" ) + +type zidJSON struct { + ID api.ZettelID `json:"id"` +} // MakePostCreateZettelHandler creates a new HTTP handler to store content of // an existing zettel. func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -31,10 +36,12 @@ var zettel zettel.Zettel var err error switch enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, id.Invalid) + case api.EncoderData: + zettel, err = buildZettelFromData(r, id.Invalid) case api.EncoderJson: zettel, err = buildZettelFromJSONData(r, id.Invalid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return @@ -56,13 +63,16 @@ location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String())) switch enc { case api.EncoderPlain: result = newZid.Bytes() contentType = content.PlainText + case api.EncoderData: + result = []byte(sx.Int64(newZid).Repr()) + contentType = content.SXPF case api.EncoderJson: var buf bytes.Buffer - err = encodeJSONData(&buf, api.ZidJSON{ID: api.ZettelID(newZid.String())}) + err = encodeJSONData(&buf, zidJSON{ID: api.ZettelID(newZid.String())}) if err != nil { a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } Index: web/adapter/api/get_data.go ================================================================== --- web/adapter/api/get_data.go +++ web/adapter/api/get_data.go @@ -9,36 +9,26 @@ //----------------------------------------------------------------------------- package api import ( - "bytes" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/usecase" - "zettelstore.de/z/web/content" + "zettelstore.de/z/zettel/id" ) // MakeGetDataHandler creates a new HTTP handler to return zettelstore data. func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { version := ucVersion.Run() - result := api.VersionJSON{ - Major: version.Major, - Minor: version.Minor, - Patch: version.Patch, - Info: version.Info, - Hash: version.Hash, - } - var buf bytes.Buffer - err := encodeJSONData(&buf, result) - if err != nil { - a.log.Fatal().Err(err).Msg("Unable to version info in buffer") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = writeBuffer(w, &buf, content.JSON) + err := a.writeObject(w, id.Invalid, sx.MakeList( + sx.Int64(version.Major), + sx.Int64(version.Minor), + sx.Int64(version.Patch), + sx.MakeString(version.Info), + sx.MakeString(version.Hash), + )) a.log.IfErr(err).Msg("Write Version Info") } } DELETED web/adapter/api/get_order.go Index: web/adapter/api/get_order.go ================================================================== --- web/adapter/api/get_order.go +++ /dev/null @@ -1,40 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 api - -import ( - "net/http" - - "zettelstore.de/c/api" - "zettelstore.de/z/usecase" - "zettelstore.de/z/zettel/id" -) - -// MakeGetOrderHandler creates a new API handler to return zettel references -// of a given zettel. -func (a *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - ctx := r.Context() - q := r.URL.Query() - start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax)) - if err != nil { - a.reportUsecaseError(w, err) - return - } - err = a.writeMetaList(ctx, w, start, metas) - a.log.IfErr(err).Zid(zid).Msg("Write Zettel Order") - } -} DELETED web/adapter/api/get_unlinked_refs.go Index: web/adapter/api/get_unlinked_refs.go ================================================================== --- web/adapter/api/get_unlinked_refs.go +++ /dev/null @@ -1,79 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern -// -// This file is part of Zettelstore. -// -// Zettelstore 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 api - -import ( - "bytes" - "net/http" - - "zettelstore.de/c/api" - "zettelstore.de/z/parser" - "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel/id" -) - -// MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references". -func (a *API) MakeListUnlinkedMetaHandler(getMeta usecase.GetMeta, unlinkedRefs usecase.UnlinkedReferences) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - zid, err := id.Parse(r.URL.Path[1:]) - if err != nil { - http.NotFound(w, r) - return - } - ctx := r.Context() - zm, err := getMeta.Run(ctx, zid) - if err != nil { - a.reportUsecaseError(w, err) - return - } - - que := r.URL.Query() - phrase := que.Get(api.QueryKeyPhrase) - if phrase == "" { - if title, found := zm.Get(api.KeyTitle); found { - phrase = parser.NormalizedSpacedText(title) - } - } - - metaList, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm)) - if err != nil { - a.reportUsecaseError(w, err) - return - } - - result := api.ZidMetaRelatedList{ - ID: api.ZettelID(zid.String()), - Meta: zm.Map(), - Rights: a.getRights(ctx, zm), - List: make([]api.ZidMetaJSON, 0, len(metaList)), - } - for _, m := range metaList { - result.List = append(result.List, api.ZidMetaJSON{ - ID: api.ZettelID(m.Zid.String()), - Meta: m.Map(), - Rights: a.getRights(ctx, m), - }) - } - - var buf bytes.Buffer - err = encodeJSONData(&buf, result) - if err != nil { - a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store unlinked references in buffer") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = writeBuffer(w, &buf, content.JSON) - a.log.IfErr(err).Zid(zid).Msg("Write Unlinked References") - } -} Index: web/adapter/api/get_zettel.go ================================================================== --- web/adapter/api/get_zettel.go +++ web/adapter/api/get_zettel.go @@ -14,26 +14,25 @@ "bytes" "context" "fmt" "net/http" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/sexp" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/encoder" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" - "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings. -func (a *API) MakeGetZettelHandler(getMeta usecase.GetMeta, getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc { +func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { zid, err := id.Parse(r.URL.Path[1:]) if err != nil { http.NotFound(w, r) return @@ -42,17 +41,17 @@ q := r.URL.Query() part := getPart(q, partContent) ctx := r.Context() switch enc, encStr := getEncoding(r, q); enc { case api.EncoderPlain: - a.writePlainData(w, ctx, zid, part, getMeta, getZettel) + a.writePlainData(w, ctx, zid, part, getZettel) case api.EncoderData: - a.writeSzData(w, ctx, zid, part, getMeta, getZettel) + a.writeSzData(w, ctx, zid, part, getZettel) case api.EncoderJson: - a.writeJSONData(w, ctx, zid, part, getMeta, getZettel) + a.writeJSONData(w, ctx, zid, part, getZettel) default: var zn *ast.ZettelNode var em func(value string) ast.InlineSlice if q.Has(api.QueryKeyParseOnly) { @@ -71,45 +70,36 @@ a.writeEncodedZettelPart(w, zn, em, enc, encStr, part) } } } -func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) { +func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { var buf bytes.Buffer var contentType string var err error + + z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) + if err != nil { + a.reportUsecaseError(w, err) + return + } switch part { case partZettel: - z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } - _, err2 = z.Meta.Write(&buf) - if err2 == nil { - err2 = buf.WriteByte('\n') - } - if err2 == nil { + _, err = z.Meta.Write(&buf) + if err == nil { + err = buf.WriteByte('\n') + } + if err == nil { _, err = z.Content.Write(&buf) } case partMeta: - m, err2 := getMeta.Run(box.NoEnrichContext(ctx), zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } contentType = content.PlainText - _, err = m.Write(&buf) + _, err = z.Meta.Write(&buf) case partContent: - z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, "")) _, err = z.Content.Write(&buf) } if err != nil { @@ -119,111 +109,82 @@ } err = writeBuffer(w, &buf, contentType) a.log.IfErr(err).Zid(zid).Msg("Write Plain data") } -func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) { - var obj sxpf.Object - switch part { - case partZettel: - z, err := getZettel.Run(ctx, zid) - if err != nil { - a.reportUsecaseError(w, err) - return - } - obj = zettel2sz(z, a.getRights(ctx, z.Meta)) - - case partMeta: - m, err := getMeta.Run(ctx, zid) - if err != nil { - a.reportUsecaseError(w, err) - return - } - obj = metaRights2sz(m, a.getRights(ctx, m)) - } - - var buf bytes.Buffer - _, err := sxpf.Print(&buf, obj) - if err != nil { - a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store zettel/part in buffer") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - err = writeBuffer(w, &buf, content.PlainText) - a.log.IfErr(err).Zid(zid).Msg("Write data") -} - -func zettel2sz(z zettel.Zettel, rights api.ZettelRights) sxpf.Object { - zContent, encoding := z.Content.Encode() - sf := sxpf.MakeMappedFactory() - return sxpf.MakeList( - sf.MustMake("zettel"), - sxpf.MakeList(sf.MustMake("id"), sxpf.MakeString(z.Meta.Zid.String())), - meta2sz(z.Meta, sf), - sxpf.MakeList(sf.MustMake("rights"), sxpf.Int64(int64(rights))), - sxpf.MakeList(sf.MustMake("encoding"), sxpf.MakeString(encoding)), - sxpf.MakeList(sf.MustMake("content"), sxpf.MakeString(zContent)), - ) -} -func metaRights2sz(m *meta.Meta, rights api.ZettelRights) sxpf.Object { - sf := sxpf.MakeMappedFactory() - return sxpf.MakeList( - sf.MustMake("list"), - meta2sz(m, sf), - sxpf.MakeList(sf.MustMake("rights"), sxpf.Int64(int64(rights))), - ) -} -func meta2sz(m *meta.Meta, sf sxpf.SymbolFactory) sxpf.Object { - result := sxpf.Nil().Cons(sf.MustMake("meta")) - curr := result - for _, p := range m.ComputedPairs() { - val := sxpf.MakeList(sf.MustMake(p.Key), sxpf.MakeString(p.Value)) - curr = curr.AppendBang(val) - } - return result -} - -func (a *API) writeJSONData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) { - var buf bytes.Buffer - var err error - - switch part { - case partZettel: - z, err2 := getZettel.Run(ctx, zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } - zContent, encoding := z.Content.Encode() - err = encodeJSONData(&buf, api.ZettelJSON{ +func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { + z, err := getZettel.Run(ctx, zid) + if err != nil { + a.reportUsecaseError(w, err) + return + } + var obj sx.Object + switch part { + case partZettel: + zContent, zEncoding := z.Content.Encode() + obj = sexp.EncodeZettel(api.ZettelData{ + Meta: z.Meta.Map(), + Rights: a.getRights(ctx, z.Meta), + Encoding: zEncoding, + Content: zContent, + }) + + case partMeta: + obj = sexp.EncodeMetaRights(api.MetaRights{ + Meta: z.Meta.Map(), + Rights: a.getRights(ctx, z.Meta), + }) + } + err = a.writeObject(w, zid, obj) + a.log.IfErr(err).Zid(zid).Msg("write sx data") +} + +type zettelJSON struct { + ID api.ZettelID `json:"id"` + Meta api.ZettelMeta `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` + Rights api.ZettelRights `json:"rights"` +} +type zettelMetaJSON struct { + Meta api.ZettelMeta `json:"meta"` + Rights api.ZettelRights `json:"rights"` +} +type zettelContentJSON struct { + Encoding string `json:"encoding"` + Content string `json:"content"` +} + +func (a *API) writeJSONData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) { + z, err := getZettel.Run(ctx, zid) + if err != nil { + a.reportUsecaseError(w, err) + return + } + + var buf bytes.Buffer + switch part { + case partZettel: + zContent, encoding := z.Content.Encode() + err = encodeJSONData(&buf, zettelJSON{ ID: api.ZettelID(zid.String()), Meta: z.Meta.Map(), Encoding: encoding, Content: zContent, Rights: a.getRights(ctx, z.Meta), }) case partMeta: - m, err2 := getMeta.Run(ctx, zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } - err = encodeJSONData(&buf, api.MetaJSON{ + m := z.Meta + err = encodeJSONData(&buf, zettelMetaJSON{ Meta: m.Map(), Rights: a.getRights(ctx, m), }) case partContent: - z, err2 := getZettel.Run(ctx, zid) - if err2 != nil { - a.reportUsecaseError(w, err2) - return - } zContent, encoding := z.Content.Encode() - err = encodeJSONData(&buf, api.ZettelContentJSON{ + err = encodeJSONData(&buf, zettelContentJSON{ Encoding: encoding, Content: zContent, }) } if err != nil { Index: web/adapter/api/json.go ================================================================== --- web/adapter/api/json.go +++ web/adapter/api/json.go @@ -9,18 +9,15 @@ //----------------------------------------------------------------------------- package api import ( - "bytes" - "context" "encoding/json" "io" "net/http" - "zettelstore.de/c/api" - "zettelstore.de/z/web/content" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -28,38 +25,21 @@ enc := json.NewEncoder(w) enc.SetEscapeHTML(false) return enc.Encode(data) } -func (a *API) writeMetaList(ctx context.Context, w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error { - outList := make([]api.ZidMetaJSON, len(metaList)) - for i, m := range metaList { - outList[i].ID = api.ZettelID(m.Zid.String()) - outList[i].Meta = m.Map() - outList[i].Rights = a.getRights(ctx, m) - } - - var buf bytes.Buffer - err := encodeJSONData(&buf, api.ZidMetaRelatedList{ - ID: api.ZettelID(m.Zid.String()), - Meta: m.Map(), - Rights: a.getRights(ctx, m), - List: outList, - }) - if err != nil { - a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store meta list in buffer") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return nil - } - - return writeBuffer(w, &buf, content.JSON) +type zettelDataJSON struct { + Meta api.ZettelMeta `json:"meta"` + Encoding string `json:"encoding"` + Content string `json:"content"` } func buildZettelFromJSONData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { var zettel zettel.Zettel + defer r.Body.Close() dec := json.NewDecoder(r.Body) - var zettelData api.ZettelDataJSON + var zettelData zettelDataJSON if err := dec.Decode(&zettelData); err != nil { return zettel, err } m := meta.New(zid) for k, v := range zettelData.Meta { Index: web/adapter/api/login.go ================================================================== --- web/adapter/api/login.go +++ web/adapter/api/login.go @@ -9,20 +9,18 @@ //----------------------------------------------------------------------------- package api import ( - "bytes" "net/http" "time" - "codeberg.org/t73fde/sxpf" - + "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" - "zettelstore.de/z/web/content" + "zettelstore.de/z/zettel/id" ) // MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API. func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -94,20 +92,11 @@ a.log.IfErr(err).Msg("Write renewed token") } } func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error { - lst := sxpf.MakeList( - sxpf.MakeString("Bearer"), - sxpf.MakeString(token), - sxpf.Int64(int64(lifetime/time.Second)), - ) - var buf bytes.Buffer - _, err := sxpf.Print(&buf, lst) - if err != nil { - a.log.Fatal().Err(err).Msg("Unable to store token in buffer") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return nil - } - - return writeBuffer(w, &buf, content.PlainText) + return a.writeObject(w, id.Invalid, sx.MakeList( + sx.MakeString("Bearer"), + sx.MakeString(token), + sx.Int64(int64(lifetime/time.Second)), + )) } Index: web/adapter/api/query.go ================================================================== --- web/adapter/api/query.go +++ web/adapter/api/query.go @@ -16,26 +16,28 @@ "io" "net/http" "strconv" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/sexp" + "zettelstore.de/sx.fossil" "zettelstore.de/z/query" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel/meta" ) // MakeQueryHandler creates a new HTTP handler to perform a query. -func (a *API) MakeQueryHandler(listMeta usecase.ListMeta) http.HandlerFunc { +func (a *API) MakeQueryHandler(queryMeta *usecase.Query) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() sq := adapter.GetQuery(q) - metaList, err := listMeta.Run(ctx, sq) + metaSeq, err := queryMeta.Run(ctx, sq) if err != nil { a.reportUsecaseError(w, err) return } @@ -44,11 +46,19 @@ switch enc, _ := getEncoding(r, q); enc { case api.EncoderPlain: encoder = &plainZettelEncoder{} contentType = content.PlainText - case api.EncoderJson: + case api.EncoderData: + encoder = &dataZettelEncoder{ + sf: sx.MakeMappedFactory(), + sq: sq, + getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, + } + contentType = content.SXPF + + case api.EncoderJson: // DEPRECATED encoder = &jsonZettelEncoder{ sq: sq, getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) }, } contentType = content.JSON @@ -57,11 +67,11 @@ http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } var buf bytes.Buffer - err = queryAction(&buf, encoder, metaList, sq) + err = queryAction(&buf, encoder, metaSeq, sq) if err != nil { a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action") http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -87,35 +97,48 @@ } } acts = append(acts, act) } for _, act := range acts { - key := strings.ToLower(act) - switch meta.Type(key) { + switch act { + case "KEYS": + return encodeKeysArrangement(w, enc, ml, act) + } + switch key := strings.ToLower(act); meta.Type(key) { case meta.TypeWord, meta.TypeTagSet: - return encodeKeyArrangement(w, enc, ml, key, min, max) + return encodeMetaKeyArrangement(w, enc, ml, key, min, max) } } } return enc.writeMetaList(w, ml) } -func encodeKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error { +func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error { + arr := make(meta.Arrangement, 128) + for _, m := range ml { + for k := range m.Map() { + arr[k] = append(arr[k], m) + } + } + return enc.writeArrangement(w, act, arr) +} + +func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error { arr0 := meta.CreateArrangement(ml, key) arr := make(meta.Arrangement, len(arr0)) for k0, ml0 := range arr0 { if len(ml0) < min || (max > 0 && len(ml0) > max) { continue } arr[k0] = ml0 } - return enc.writeArrangement(w, arr) + return enc.writeArrangement(w, key, arr) } type zettelEncoder interface { writeMetaList(w io.Writer, ml []*meta.Meta) error - writeArrangement(w io.Writer, arr meta.Arrangement) error + writeArrangement(w io.Writer, act string, arr meta.Arrangement) error } type plainZettelEncoder struct{} func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { @@ -125,11 +148,11 @@ return err } } return nil } -func (*plainZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error { +func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { for key, ml := range arr { _, err := io.WriteString(w, key) if err != nil { return err } @@ -153,38 +176,105 @@ } } return nil } +type dataZettelEncoder struct { + sf sx.SymbolFactory + sq *query.Query + getRights func(*meta.Meta) api.ZettelRights +} + +func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { + sf := dze.sf + result := make([]sx.Object, len(ml)+1) + result[0] = sf.MustMake("list") + symID, symZettel := sf.MustMake("id"), sf.MustMake("zettel") + for i, m := range ml { + msz := sexp.EncodeMetaRights(api.MetaRights{ + Meta: m.Map(), + Rights: dze.getRights(m), + }) + msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel) + result[i+1] = msz + } + + _, err := sx.Print(w, sx.MakeList( + sf.MustMake("meta-list"), + sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())), + sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())), + sx.MakeList(result...), + )) + return err +} +func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error { + sf := dze.sf + result := sx.Nil() + for aggKey, metaList := range arr { + sxMeta := sx.Nil() + for i := len(metaList) - 1; i >= 0; i-- { + sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid)) + } + sxMeta = sxMeta.Cons(sx.MakeString(aggKey)) + result = result.Cons(sxMeta) + } + _, err := sx.Print(w, sx.MakeList( + sf.MustMake("aggregate"), + sx.MakeString(act), + sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())), + sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())), + result.Cons(sf.MustMake("list")), + )) + return err +} + +// jsonZettelEncoder is DEPRECATED type jsonZettelEncoder struct { sq *query.Query getRights func(*meta.Meta) api.ZettelRights } + +type zidMetaJSON struct { + ID api.ZettelID `json:"id"` + Meta api.ZettelMeta `json:"meta"` + Rights api.ZettelRights `json:"rights"` +} + +type zettelListJSON struct { + Query string `json:"query"` + Human string `json:"human"` + List []zidMetaJSON `json:"list"` +} func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error { - result := make([]api.ZidMetaJSON, 0, len(ml)) + result := make([]zidMetaJSON, 0, len(ml)) for _, m := range ml { - result = append(result, api.ZidMetaJSON{ + result = append(result, zidMetaJSON{ ID: api.ZettelID(m.Zid.String()), Meta: m.Map(), Rights: jze.getRights(m), }) } - err := encodeJSONData(w, api.ZettelListJSON{ + err := encodeJSONData(w, zettelListJSON{ Query: jze.sq.String(), Human: jze.sq.Human(), List: result, }) return err } -func (*jsonZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error { - mm := make(api.MapMeta, len(arr)) + +type mapListJSON struct { + Map api.Aggregate `json:"map"` +} + +func (*jsonZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error { + mm := make(api.Aggregate, len(arr)) for key, metaList := range arr { zidList := make([]api.ZettelID, 0, len(metaList)) for _, m := range metaList { zidList = append(zidList, api.ZettelID(m.Zid.String())) } mm[key] = zidList } - return encodeJSONData(w, api.MapListJSON{Map: mm}) + return encodeJSONData(w, mapListJSON{Map: mm}) } Index: web/adapter/api/rename_zettel.go ================================================================== --- web/adapter/api/rename_zettel.go +++ web/adapter/api/rename_zettel.go @@ -12,11 +12,11 @@ import ( "net/http" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/usecase" "zettelstore.de/z/zettel/id" ) // MakeRenameZettelHandler creates a new HTTP handler to update a zettel. Index: web/adapter/api/request.go ================================================================== --- web/adapter/api/request.go +++ web/adapter/api/request.go @@ -13,11 +13,13 @@ import ( "io" "net/http" "net/url" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/sexp" + "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/input" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) @@ -99,10 +101,11 @@ } return p.String() } func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { + defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { return zettel.Zettel{}, err } inp := input.NewInput(b) @@ -109,7 +112,36 @@ m := meta.NewFromInput(zid, inp) return zettel.Zettel{ Meta: m, Content: zettel.NewContent(inp.Src[inp.Pos:]), }, nil +} + +func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) { + defer r.Body.Close() + rdr := sxreader.MakeReader(r.Body) + obj, err := rdr.Read() + if err != nil { + return zettel.Zettel{}, err + } + zd, err := sexp.ParseZettel(obj) + if err != nil { + return zettel.Zettel{}, err + } + + m := meta.New(zid) + for k, v := range zd.Meta { + if !meta.IsComputed(k) { + m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v)) + } + } + + var content zettel.Content + if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil { + return zettel.Zettel{}, err + } + return zettel.Zettel{ + Meta: m, + Content: content, + }, nil } ADDED web/adapter/api/response.go Index: web/adapter/api/response.go ================================================================== --- /dev/null +++ web/adapter/api/response.go @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-present Detlef Stern +// +// This file is part of Zettelstore. +// +// Zettelstore 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 api + +import ( + "bytes" + "net/http" + + "zettelstore.de/sx.fossil" + "zettelstore.de/z/web/content" + "zettelstore.de/z/zettel/id" +) + +func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error { + var buf bytes.Buffer + if _, err := sx.Print(&buf, obj); err != nil { + msg := a.log.Fatal().Err(err) + if msg != nil { + if zid.IsValid() { + msg = msg.Zid(zid) + } + msg.Msg("Unable to store object in buffer") + } + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return nil + } + return writeBuffer(w, &buf, content.SXPF) +} Index: web/adapter/api/update_zettel.go ================================================================== --- web/adapter/api/update_zettel.go +++ web/adapter/api/update_zettel.go @@ -11,11 +11,11 @@ package api import ( "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" ) @@ -32,10 +32,12 @@ q := r.URL.Query() var zettel zettel.Zettel switch enc, _ := getEncoding(r, q); enc { case api.EncoderPlain: zettel, err = buildZettelFromPlainData(r, zid) + case api.EncoderData: + zettel, err = buildZettelFromData(r, zid) case api.EncoderJson: zettel, err = buildZettelFromJSONData(r, zid) default: http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return Index: web/adapter/request.go ================================================================== --- web/adapter/request.go +++ web/adapter/request.go @@ -14,14 +14,13 @@ "net/http" "net/url" "strconv" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/kernel" "zettelstore.de/z/query" - "zettelstore.de/z/zettel/meta" ) // GetCredentialsViaForm retrieves the authentication credentions from a form. func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) { err := r.ParseForm() @@ -51,31 +50,5 @@ } } } return result } - -// AddUnlinkedRefsToQuery inspects metadata and enhances the given query to ignore -// some zettel identifier. -func AddUnlinkedRefsToQuery(q *query.Query, m *meta.Meta) *query.Query { - var sb strings.Builder - sb.WriteString(api.KeyID) - sb.WriteString("!:") - sb.WriteString(m.Zid.String()) - for _, pair := range m.ComputedPairsRest() { - switch meta.Type(pair.Key) { - case meta.TypeID: - sb.WriteByte(' ') - sb.WriteString(api.KeyID) - sb.WriteString("!:") - sb.WriteString(pair.Value) - case meta.TypeIDSet: - for _, value := range meta.ListFromValue(pair.Value) { - sb.WriteByte(' ') - sb.WriteString(api.KeyID) - sb.WriteString("!:") - sb.WriteString(value) - } - } - } - return q.Parse(sb.String()) -} Index: web/adapter/response.go ================================================================== --- web/adapter/response.go +++ web/adapter/response.go @@ -13,11 +13,11 @@ import ( "errors" "fmt" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" ) // WriteData emits the given data to the response writer. Index: web/adapter/webui/const.go ================================================================== --- web/adapter/webui/const.go +++ web/adapter/webui/const.go @@ -14,10 +14,11 @@ const queryKeyAction = "action" // Values for queryKeyAction const ( + valueActionChild = "child" valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionVersion = "version" ) @@ -24,25 +25,26 @@ // Enumeration for queryKeyAction type createAction uint8 const ( - actionCopy createAction = iota + actionChild createAction = iota + actionCopy actionFolge actionNew actionVersion ) -func getCreateAction(s string) createAction { - switch s { - case valueActionCopy: - return actionCopy - case valueActionFolge: - return actionFolge - case valueActionNew: - return actionNew - case valueActionVersion: - return actionVersion - default: - return actionCopy - } +var createActionMap = map[string]createAction{ + valueActionChild: actionChild, + valueActionCopy: actionCopy, + valueActionFolge: actionFolge, + valueActionNew: actionNew, + valueActionVersion: actionVersion, +} + +func getCreateAction(s string) createAction { + if action, found := createActionMap[s]; found { + return action + } + return actionCopy } Index: web/adapter/webui/create_zettel.go ================================================================== --- web/adapter/webui/create_zettel.go +++ web/adapter/webui/create_zettel.go @@ -14,12 +14,12 @@ "bytes" "context" "net/http" "strings" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/box" "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" @@ -50,19 +50,21 @@ return } roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax) switch op { + case actionChild: + wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData) case actionCopy: wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData) - case actionVersion: - wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) case actionFolge: wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData) case actionNew: title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle()) wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData) + case actionVersion: + wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData) } } } func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) { @@ -98,17 +100,17 @@ sb.WriteString(": ") sb.WriteString(p.Value) sb.WriteByte('\n') } env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user) - rb.bindString("heading", sxpf.MakeString(title)) - rb.bindString("form-action-url", sxpf.MakeString(formActionURL)) + rb.bindString("heading", sx.MakeString(title)) + rb.bindString("form-action-url", sx.MakeString(formActionURL)) rb.bindString("role-data", makeStringList(roleData)) rb.bindString("syntax-data", makeStringList(syntaxData)) - rb.bindString("meta", sxpf.MakeString(sb.String())) + rb.bindString("meta", sx.MakeString(sb.String())) if !ztl.Content.IsBinary() { - rb.bindString("content", sxpf.MakeString(ztl.Content.AsString())) + rb.bindString("content", sx.MakeString(ztl.Content.AsString())) } wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env) } @@ -148,22 +150,22 @@ } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( - listMeta usecase.ListMeta, evaluate *usecase.Evaluate, + queryMeta *usecase.Query, evaluate *usecase.Evaluate, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) ctx := r.Context() - metaList, err := listMeta.Run(box.NoEnrichQuery(ctx, q), q) + metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q) if err != nil { wui.reportError(ctx, w, err) return } - bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig)) + bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig)) enc := zmkenc.Create() var zmkContent bytes.Buffer _, err = enc.WriteBlocks(&zmkContent, &bns) if err != nil { wui.reportError(ctx, w, err) Index: web/adapter/webui/delete_zettel.go ================================================================== --- web/adapter/webui/delete_zettel.go +++ web/adapter/webui/delete_zettel.go @@ -11,13 +11,13 @@ package webui import ( "net/http" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/sx.fossil" "zettelstore.de/z/box" "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" @@ -24,36 +24,36 @@ "zettelstore.de/z/zettel/meta" ) // MakeGetDeleteZettelHandler creates a new HTTP handler to display the // HTML delete view of a zettel. -func (wui *WebUI) MakeGetDeleteZettelHandler(getMeta usecase.GetMeta, getAllMeta usecase.GetAllMeta) http.HandlerFunc { +func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - ms, err := getAllMeta.Run(ctx, zid) + zs, err := getAllZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } - m := ms[0] + m := zs[0].Meta user := server.GetUser(ctx) env, rb := wui.createRenderEnv( ctx, "delete", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user) - if len(ms) > 1 { - rb.bindString("shadowed-box", sxpf.MakeString(ms[1].GetDefault(api.KeyBoxNumber, "???"))) + if len(zs) > 1 { + rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???"))) rb.bindString("incoming", nil) } else { rb.bindString("shadowed-box", nil) - rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getMeta))) + rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) } wui.bindCommonZettelData(ctx, &rb, user, m, nil) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env) @@ -62,11 +62,11 @@ wui.reportError(ctx, w, err) } } } -func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sxpf.List { +func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair { zidMap := make(strfun.Set) addListValues(zidMap, m, api.KeyBackward) for _, kd := range meta.GetSortedKeyDescriptions() { inverseKey := kd.Inverse if inverseKey == "" { Index: web/adapter/webui/edit_zettel.go ================================================================== --- web/adapter/webui/edit_zettel.go +++ web/adapter/webui/edit_zettel.go @@ -11,11 +11,11 @@ package webui import ( "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) Index: web/adapter/webui/forms.go ================================================================== --- web/adapter/webui/forms.go +++ web/adapter/webui/forms.go @@ -17,11 +17,11 @@ "net/http" "regexp" "strings" "unicode" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" @@ -53,19 +53,24 @@ } if postTitle, ok := trimmedFormValue(r, "title"); ok { m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle)) } if postTags, ok := trimmedFormValue(r, "tags"); ok { - if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { + if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 { + for i, tag := range tags { + if tag[0] != '#' { + tags[i] = "#" + tag + } + } m.SetList(api.KeyTags, tags) } } if postRole, ok := trimmedFormValue(r, "role"); ok { - m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole)) + m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole)) } if postSyntax, ok := trimmedFormValue(r, "syntax"); ok { - m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) + m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax)) } if data := textContent(r); data != nil { return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil } Index: web/adapter/webui/get_info.go ================================================================== --- web/adapter/webui/get_info.go +++ web/adapter/webui/get_info.go @@ -14,32 +14,32 @@ "context" "net/http" "sort" "strings" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/encoder" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/query" + "zettelstore.de/z/strfun" "zettelstore.de/z/usecase" - "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" ) // MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel". func (wui *WebUI) MakeGetInfoHandler( - parseZettel usecase.ParseZettel, - evaluate *usecase.Evaluate, - getMeta usecase.GetMeta, - getAllMeta usecase.GetAllMeta, - unlinkedRefs usecase.UnlinkedReferences, + ucParseZettel usecase.ParseZettel, + ucEvaluate *usecase.Evaluate, + ucGetZettel usecase.GetZettel, + ucGetAllMeta usecase.GetAllZettel, + ucQuery *usecase.Query, ) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() q := r.URL.Query() @@ -47,27 +47,27 @@ if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) + zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax)) if err != nil { wui.reportError(ctx, w, err) return } enc := wui.getSimpleHTMLEncoder() - getTextTitle := wui.makeGetTextTitle(ctx, getMeta) + getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel) evalMeta := func(val string) ast.InlineSlice { - return evaluate.RunMetadata(ctx, val) + return ucEvaluate.RunMetadata(ctx, val) } pairs := zn.Meta.ComputedPairs() - metadata := sxpf.Nil() + metadata := sx.Nil() for i := len(pairs) - 1; i >= 0; i-- { key := pairs[i].Key sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc) - metadata = metadata.Cons(sxpf.Cons(sxpf.MakeString(key), sxval)) + metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval)) } summary := collect.References(zn) locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...)) @@ -74,34 +74,34 @@ title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) phrase := q.Get(api.QueryKeyPhrase) if phrase == "" { phrase = title } - phrase = strings.TrimSpace(phrase) - unlinkedMeta, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(query.Parse("ORDER id"), zn.InhMeta)) + unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase)) if err != nil { wui.reportError(ctx, w, err) return } - bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) + + bns := ucEvaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig)) unlinkedContent, _, err := enc.BlocksSxn(&bns) if err != nil { wui.reportError(ctx, w, err) return } encTexts := encodingTexts() - shadowLinks := getShadowLinks(ctx, zid, getAllMeta) + shadowLinks := getShadowLinks(ctx, zid, ucGetAllMeta) user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user) rb.bindString("metadata", metadata) rb.bindString("local-links", locLinks) rb.bindString("query-links", queryLinks) rb.bindString("ext-links", extLinks) rb.bindString("unlinked-content", unlinkedContent) - rb.bindString("phrase", sxpf.MakeString(phrase)) - rb.bindString("query-key-phrase", sxpf.MakeString(api.QueryKeyPhrase)) + rb.bindString("phrase", sx.MakeString(phrase)) + rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase)) rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts)) rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts)) rb.bindString("shadow-links", shadowLinks) wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content) if rb.err == nil { @@ -111,31 +111,49 @@ wui.reportError(ctx, w, err) } } } -func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sxpf.List) { +func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) { for i := len(links) - 1; i >= 0; i-- { ref := links[i] if ref.State == ast.RefStateSelf || ref.IsZettel() { continue } if ref.State == ast.RefStateQuery { queries = queries.Cons( - sxpf.Cons( - sxpf.MakeString(ref.Value), - sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String()))) + sx.Cons( + sx.MakeString(ref.Value), + sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String()))) continue } if ref.IsExternal() { - extLinks = extLinks.Cons(sxpf.MakeString(ref.String())) + extLinks = extLinks.Cons(sx.MakeString(ref.String())) continue } - locLinks = locLinks.Cons(sxpf.Cons(sxpf.MakeBoolean(ref.IsValid()), sxpf.MakeString(ref.String()))) + locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.MakeString(ref.String()))) } return locLinks, queries, extLinks } + +func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query { + var sb strings.Builder + sb.Write(zid.Bytes()) + sb.WriteByte(' ') + sb.WriteString(api.UnlinkedDirective) + for _, word := range strfun.MakeWords(phrase) { + sb.WriteByte(' ') + sb.WriteString(api.PhraseDirective) + sb.WriteByte(' ') + sb.WriteString(word) + } + sb.WriteByte(' ') + sb.WriteString(api.OrderDirective) + sb.WriteByte(' ') + sb.WriteString(api.KeyID) + return query.Parse(sb.String()) +} func encodingTexts() []string { encodings := encoder.GetEncodings() encTexts := make([]string, 0, len(encodings)) for _, f := range encodings { @@ -145,67 +163,66 @@ return encTexts } var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent} -func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sxpf.List { - matrix := sxpf.Nil() +func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair { + matrix := sx.Nil() u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) for ip := len(apiParts) - 1; ip >= 0; ip-- { part := apiParts[ip] - row := sxpf.Nil() + row := sx.Nil() for je := len(encTexts) - 1; je >= 0; je-- { enc := encTexts[je] if parseOnly { u.AppendKVQuery(api.QueryKeyParseOnly, "") } u.AppendKVQuery(api.QueryKeyPart, part) u.AppendKVQuery(api.QueryKeyEncoding, enc) - row = row.Cons(sxpf.Cons(sxpf.MakeString(enc), sxpf.MakeString(u.String()))) + row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String()))) u.ClearQuery() } - matrix = matrix.Cons(sxpf.Cons(sxpf.MakeString(part), row)) + matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row)) } return matrix } -func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sxpf.List { +func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair { matrix := wui.infoAPIMatrix(zid, true, encTexts) - // apiZid := api.ZettelID(zid.String()) u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String())) for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() { - line, isLine := sxpf.GetList(row.Car()) + line, isLine := sx.GetPair(row.Car()) if !isLine || line == nil { continue } last := line.LastPair() part := apiParts[i] u.AppendKVQuery(api.QueryKeyPart, part) - last = last.AppendBang(sxpf.Cons(sxpf.MakeString("plain"), sxpf.MakeString(u.String()))) + last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String()))) u.ClearQuery() if i < 2 { u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData) u.AppendKVQuery(api.QueryKeyPart, part) - last = last.AppendBang(sxpf.Cons(sxpf.MakeString("data"), sxpf.MakeString(u.String()))) + last = last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String()))) u.ClearQuery() u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson) u.AppendKVQuery(api.QueryKeyPart, part) - last.AppendBang(sxpf.Cons(sxpf.MakeString("json"), sxpf.MakeString(u.String()))) + last.AppendBang(sx.Cons(sx.MakeString("json"), sx.MakeString(u.String()))) u.ClearQuery() } i++ } return matrix } -func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) *sxpf.List { - result := sxpf.Nil() - if ml, err := getAllMeta.Run(ctx, zid); err == nil { - for i := len(ml) - 1; i >= 1; i-- { - if boxNo, ok := ml[i].Get(api.KeyBoxNumber); ok { - result = result.Cons(sxpf.MakeString(boxNo)) +func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair { + result := sx.Nil() + if zl, err := getAllZettel.Run(ctx, zid); err == nil { + for i := len(zl) - 1; i >= 1; i-- { + if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok { + result = result.Cons(sx.MakeString(boxNo)) } } } return result } Index: web/adapter/webui/get_zettel.go ================================================================== --- web/adapter/webui/get_zettel.go +++ web/adapter/webui/get_zettel.go @@ -12,22 +12,22 @@ import ( "context" "net/http" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel". -func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc { +func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) @@ -54,22 +54,27 @@ wui.reportError(ctx, w, err) return } user := server.GetUser(ctx) - getTextTitle := wui.makeGetTextTitle(ctx, getMeta) + getTextTitle := wui.makeGetTextTitle(ctx, getZettel) title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle()) env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user) rb.bindSymbol(wui.symMetaHeader, metaObj) - rb.bindString("css-role-url", sxpf.MakeString(cssRoleURL)) - rb.bindString("heading", sxpf.MakeString(title)) + rb.bindString("css-role-url", sx.MakeString(cssRoleURL)) + rb.bindString("heading", sx.MakeString(title)) + if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" { + rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String())) + } + if role, found := zn.InhMeta.Get(api.KeyFolgeRole); found && role != "" { + rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String())) + } rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, "")))) rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle)) rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle)) rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle)) - rb.bindString("ext-url", wui.urlFromMeta(zn.InhMeta, api.KeyURL)) rb.bindString("content", content) rb.bindString("endnotes", endnotes) rb.bindString("folge-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyFolge, getTextTitle)) rb.bindString("subordinate-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySubordinates, getTextTitle)) rb.bindString("back-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyBack, getTextTitle)) @@ -93,46 +98,38 @@ return "", nil } return wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String(), nil } -func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sxpf.List { +func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { if values, ok := m.GetList(key); ok { return wui.transformIdentifierSet(values, getTextTitle) } - return sxpf.Nil() + return nil } -func (wui *WebUI) urlFromMeta(m *meta.Meta, key string) sxpf.Object { - val, found := m.Get(key) - if !found || val == "" { - return sxpf.Nil() - } - return wui.transformURL(val) -} - -func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sxpf.List { +func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair { values, ok := m.GetList(key) if !ok || len(values) == 0 { return nil } return wui.zidLinksSxn(values, getTextTitle) } -func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sxpf.List) { +func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) { for i := len(values) - 1; i >= 0; i-- { val := values[i] zid, err := id.Parse(val) if err != nil { continue } if title, found := getTextTitle(zid); found > 0 { - url := sxpf.MakeString(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String()) + url := sx.MakeString(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String()) if title == "" { - lst = lst.Cons(sxpf.Cons(sxpf.MakeString(val), url)) + lst = lst.Cons(sx.Cons(sx.MakeString(val), url)) } else { - lst = lst.Cons(sxpf.Cons(sxpf.MakeString(title), url)) + lst = lst.Cons(sx.Cons(sx.MakeString(title), url)) } } } return lst } Index: web/adapter/webui/home.go ================================================================== --- web/adapter/webui/home.go +++ web/adapter/webui/home.go @@ -13,21 +13,20 @@ import ( "context" "errors" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/web/server" + "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) type getRootStore interface { - // GetMeta retrieves just the meta data of a specific zettel. - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) + GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) } // MakeGetRootHandler creates a new HTTP handler to show the root URL. func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -37,17 +36,17 @@ return } homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel)) apiHomeZid := api.ZettelID(homeZid.String()) if homeZid != id.DefaultHomeZid { - if _, err := s.GetMeta(ctx, homeZid); err == nil { + if _, err := s.GetZettel(ctx, homeZid); err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } homeZid = id.DefaultHomeZid } - _, err := s.GetMeta(ctx, homeZid) + _, err := s.GetZettel(ctx, homeZid) if err == nil { wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid)) return } if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil { Index: web/adapter/webui/htmlgen.go ================================================================== --- web/adapter/webui/htmlgen.go +++ web/adapter/webui/htmlgen.go @@ -12,17 +12,17 @@ import ( "net/url" "strings" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/eval" - "zettelstore.de/c/api" - "zettelstore.de/c/attrs" - "zettelstore.de/c/maps" - "zettelstore.de/c/shtml" - "zettelstore.de/c/sz" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/attrs" + "zettelstore.de/client.fossil/maps" + "zettelstore.de/client.fossil/shtml" + "zettelstore.de/client.fossil/sz" + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" @@ -35,11 +35,11 @@ } type htmlGenerator struct { tx *szenc.Transformer th *shtml.Transformer - symAt *sxpf.Symbol + symAt *sx.Symbol } func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator { th := shtml.NewTransformer(1, wui.sf) symA := wui.symA @@ -49,30 +49,30 @@ symHref := wui.symHref symClass := th.Make("class") symTarget := th.Make("target") symRel := th.Make("rel") - findA := func(obj sxpf.Object) (attr, assoc, rest *sxpf.List) { - lst, ok := sxpf.GetList(obj) - if !ok || !symA.IsEqual(lst.Car()) { + findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) { + pair, isPair := sx.GetPair(obj) + if !isPair || !symA.IsEqual(pair.Car()) { return nil, nil, nil } - rest = lst.Tail() + rest = pair.Tail() if rest == nil { return nil, nil, nil } objA := rest.Car() - attr, ok = sxpf.GetList(objA) - if !ok || !symAttr.IsEqual(attr.Car()) { + attr, isPair = sx.GetPair(objA) + if !isPair || !symAttr.IsEqual(attr.Car()) { return nil, nil, nil } return attr, attr.Tail(), rest.Tail() } - linkZettel := func(args []sxpf.Object, prevFn eval.Callable) sxpf.Object { + linkZettel := func(args []sx.Object, prevFn sxeval.Callable) sx.Object { obj, err := prevFn.Call(nil, nil, args) if err != nil { - return sxpf.Nil() + return sx.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } @@ -79,61 +79,61 @@ hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } - href, ok := sxpf.GetString(hrefP.Cdr()) + href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } zid, fragment, hasFragment := strings.Cut(href.String(), "#") u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid)) if hasFragment { u = u.SetFragment(fragment) } - assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) + assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAttr)).Cons(symA) } th.SetRebinder(func(te *shtml.TransformEnv) { te.Rebind(sz.NameSymLinkZettel, linkZettel) te.Rebind(sz.NameSymLinkFound, linkZettel) - te.Rebind(sz.NameSymLinkBased, func(args []sxpf.Object, prevFn eval.Callable) sxpf.Object { + te.Rebind(sz.NameSymLinkBased, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { obj, err := prevFn.Call(nil, nil, args) if err != nil { - return sxpf.Nil() + return sx.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } - href, ok := sxpf.GetString(hrefP.Cdr()) + href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } u := builder.NewURLBuilder('/').SetRawLocal(href.String()) - assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) + assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAttr)).Cons(symA) }) - te.Rebind(sz.NameSymLinkQuery, func(args []sxpf.Object, prevFn eval.Callable) sxpf.Object { + te.Rebind(sz.NameSymLinkQuery, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { obj, err := prevFn.Call(nil, nil, args) if err != nil { - return sxpf.Nil() + return sx.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } hrefP := assoc.Assoc(symHref) if hrefP == nil { return obj } - href, ok := sxpf.GetString(hrefP.Cdr()) + href, ok := sx.GetString(hrefP.Cdr()) if !ok { return obj } ur, err := url.Parse(href.String()) if err != nil { @@ -142,56 +142,56 @@ q := ur.Query().Get(api.QueryKeyQuery) if q == "" { return obj } u := builder.NewURLBuilder('h').AppendQuery(q) - assoc = assoc.Cons(sxpf.Cons(symHref, sxpf.MakeString(u.String()))) + assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String()))) return rest.Cons(assoc.Cons(symAttr)).Cons(symA) }) - te.Rebind(sz.NameSymLinkExternal, func(args []sxpf.Object, prevFn eval.Callable) sxpf.Object { + te.Rebind(sz.NameSymLinkExternal, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { obj, err := prevFn.Call(nil, nil, args) if err != nil { - return sxpf.Nil() + return sx.Nil() } attr, assoc, rest := findA(obj) if attr == nil { return obj } - assoc = assoc.Cons(sxpf.Cons(symClass, sxpf.MakeString("external"))). - Cons(sxpf.Cons(symTarget, sxpf.MakeString("_blank"))). - Cons(sxpf.Cons(symRel, sxpf.MakeString("noopener noreferrer"))) + assoc = assoc.Cons(sx.Cons(symClass, sx.MakeString("external"))). + Cons(sx.Cons(symTarget, sx.MakeString("_blank"))). + Cons(sx.Cons(symRel, sx.MakeString("noopener noreferrer"))) return rest.Cons(assoc.Cons(symAttr)).Cons(symA) }) - te.Rebind(sz.NameSymEmbed, func(args []sxpf.Object, prevFn eval.Callable) sxpf.Object { + te.Rebind(sz.NameSymEmbed, func(args []sx.Object, prevFn sxeval.Callable) sx.Object { obj, err := prevFn.Call(nil, nil, args) if err != nil { - return sxpf.Nil() + return sx.Nil() } - lst, ok := sxpf.GetList(obj) - if !ok || !symImg.IsEqual(lst.Car()) { + pair, isPair := sx.GetPair(obj) + if !isPair || !symImg.IsEqual(pair.Car()) { return obj } - attr, ok := sxpf.GetList(lst.Tail().Car()) - if !ok || !symAttr.IsEqual(attr.Car()) { + attr, isPair := sx.GetPair(pair.Tail().Car()) + if !isPair || !symAttr.IsEqual(attr.Car()) { return obj } symSrc := th.Make("src") srcP := attr.Tail().Assoc(symSrc) if srcP == nil { return obj } - src, ok := sxpf.GetString(srcP.Cdr()) - if !ok { + src, isString := sx.GetString(srcP.Cdr()) + if !isString { return obj } zid := api.ZettelID(src) if !zid.IsValid() { return obj } u := builder.NewURLBuilder('z').SetZid(zid) - imgAttr := attr.Tail().Cons(sxpf.Cons(symSrc, sxpf.MakeString(u.String()))).Cons(symAttr) - return lst.Tail().Tail().Cons(imgAttr).Cons(symImg) + imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.MakeString(u.String()))).Cons(symAttr) + return pair.Tail().Tail().Cons(imgAttr).Cons(symImg) }) }) return &htmlGenerator{ tx: szenc.NewTransformer(), @@ -206,42 +206,42 @@ var mapMetaKey = map[string]string{ api.KeyCopyright: "copyright", api.KeyLicense: "license", } -func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.List { +func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair { tm := g.tx.GetMeta(m, evalMeta) hm, err := g.th.Transform(tm) if err != nil { return nil } ignore := strfun.NewSet(api.KeyTitle, api.KeyLang) - metaMap := make(map[string]*sxpf.List, m.Length()) + metaMap := make(map[string]*sx.Pair, m.Length()) if tags, ok := m.Get(api.KeyTags); ok { metaMap[api.KeyTags] = g.transformMetaTags(tags) ignore.Set(api.KeyTags) } for elem := hm; elem != nil; elem = elem.Tail() { - mlst, ok := sxpf.GetList(elem.Car()) - if !ok { + mlst, isPair := sx.GetPair(elem.Car()) + if !isPair { continue } - att, ok := sxpf.GetList(mlst.Tail().Car()) - if !ok { + att, isPair := sx.GetPair(mlst.Tail().Car()) + if !isPair { continue } if !att.Car().IsEqual(g.symAt) { continue } a := make(attrs.Attributes, 32) for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() { - if p, ok2 := sxpf.GetList(aelem.Car()); ok2 { + if p, ok := sx.GetPair(aelem.Car()); ok { key := p.Car() val := p.Cdr() - if tail, ok3 := sxpf.GetList(val); ok3 { + if tail, isTail := sx.GetPair(val); isTail { val = tail.Car() } a = a.Set(key.String(), val.String()) } } @@ -255,34 +255,34 @@ continue } a = a.Set("name", newName) metaMap[newName] = g.th.TransformMeta(a) } - result := sxpf.Nil() + result := sx.Nil() keys := maps.Keys(metaMap) for i := len(keys) - 1; i >= 0; i-- { result = result.Cons(metaMap[keys[i]]) } return result } -func (g *htmlGenerator) transformMetaTags(tags string) *sxpf.List { +func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair { var sb strings.Builder for i, val := range meta.ListFromValue(tags) { if i > 0 { sb.WriteString(", ") } sb.WriteString(strings.TrimPrefix(val, "#")) } metaTags := sb.String() if len(metaTags) == 0 { - return sxpf.Nil() + return nil } return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags}) } -func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sxpf.List, _ error) { +func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) { if bs == nil || len(*bs) == 0 { return nil, nil, nil } sx := g.tx.GetSz(bs) sh, err := g.th.Transform(sx) @@ -291,16 +291,16 @@ } return sh, g.th.Endnotes(), nil } // InlinesSxHTML returns an inline slice, encoded as a SxHTML object. -func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sxpf.List { +func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair { if is == nil || len(*is) == 0 { - return sxpf.Nil() + return nil } sx := g.tx.GetSz(is) sh, err := g.th.Transform(sx) if err != nil { - return sxpf.Nil() + return nil } return sh } Index: web/adapter/webui/htmlmeta.go ================================================================== --- web/adapter/webui/htmlmeta.go +++ web/adapter/webui/htmlmeta.go @@ -11,17 +11,14 @@ package webui import ( "context" "errors" - "fmt" - "net/url" - "time" - "codeberg.org/t73fde/sxhtml" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/zettel/id" @@ -31,147 +28,122 @@ func (wui *WebUI) writeHTMLMetaValue( key, value string, getTextTitle getTextTitleFunc, evalMetadata evalMetadataFunc, gen *htmlGenerator, -) sxpf.Object { - var sval sxpf.Object = sxpf.Nil() +) sx.Object { switch kt := meta.Type(key); kt { case meta.TypeCredential: - sval = sxpf.MakeString(value) + return sx.MakeString(value) case meta.TypeEmpty: - sval = sxpf.MakeString(value) + return sx.MakeString(value) case meta.TypeID: - sval = wui.transformIdentifier(value, getTextTitle) + return wui.transformIdentifier(value, getTextTitle) case meta.TypeIDSet: - sval = wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) + return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle) case meta.TypeNumber: - sval = wui.transformLink(key, value, value) + return wui.transformLink(key, value, value) case meta.TypeString: - sval = sxpf.MakeString(value) + return sx.MakeString(value) case meta.TypeTagSet: - sval = wui.transformTagSet(key, meta.ListFromValue(value)) + return wui.transformTagSet(key, meta.ListFromValue(value)) case meta.TypeTimestamp: if ts, ok := meta.TimeValue(value); ok { - sval = wui.transformTimestamp(ts) + return sx.MakeList( + wui.sf.MustMake("time"), + sx.MakeList( + wui.symAttr, + sx.Cons(wui.sf.MustMake("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))), + ), + sx.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sx.MakeString(ts.Format("2006-01-02 15:04:05"))), + ) } + return sx.Nil() case meta.TypeURL: - sval = wui.transformURL(value) + text := sx.MakeString(value) + if res, err := wui.url2html([]sx.Object{text}); err == nil { + return res + } + return text case meta.TypeWord: - sval = wui.transformWord(key, value) + return wui.transformLink(key, value, value) case meta.TypeWordSet: - sval = wui.transformWordSet(key, meta.ListFromValue(value)) + return wui.transformWordSet(key, meta.ListFromValue(value)) case meta.TypeZettelmarkup: - sval = wui.transformZmkMetadata(value, evalMetadata, gen) + return wui.transformZmkMetadata(value, evalMetadata, gen) default: - sval = sxpf.Nil().Cons(sxpf.MakeString(fmt.Sprintf(" (Unhandled type: %v, key: %v)", kt, key))).Cons(wui.sf.MustMake("b")) + return sx.MakeList(wui.sf.MustMake("b"), sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name)) } - return sval } -func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sxpf.Object { - text := sxpf.MakeString(val) +func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object { + text := sx.MakeString(val) zid, err := id.Parse(val) if err != nil { return text } title, found := getTextTitle(zid) switch { case found > 0: ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())) - attrs := sxpf.Nil() - if title != "" { - attrs = attrs.Cons(sxpf.Cons(wui.sf.MustMake("title"), sxpf.MakeString(title))) - } - attrs = attrs.Cons(sxpf.Cons(wui.symHref, sxpf.MakeString(ub.String()))).Cons(wui.symAttr) - return sxpf.Nil().Cons(sxpf.MakeString(zid.String())).Cons(attrs).Cons(wui.symA) - case found == 0: - return sxpf.Nil().Cons(text).Cons(wui.sf.MustMake("s")) + attrs := sx.Nil() + if title != "" { + attrs = attrs.Cons(sx.Cons(wui.sf.MustMake("title"), sx.MakeString(title))) + } + attrs = attrs.Cons(sx.Cons(wui.symHref, sx.MakeString(ub.String()))).Cons(wui.symAttr) + return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(wui.symA) + case found == 0: + return sx.MakeList(wui.sf.MustMake("s"), text) default: // case found < 0: return text } } -func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sxpf.List { +func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair { if len(vals) == 0 { - return sxpf.Nil() + return nil } - space := sxpf.MakeString(" ") - text := make([]sxpf.Object, 0, 2*len(vals)) + space := sx.MakeString(" ") + text := make([]sx.Object, 0, 2*len(vals)) for _, val := range vals { text = append(text, space, wui.transformIdentifier(val, getTextTitle)) } - return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) + return sx.MakeList(text[1:]...).Cons(wui.symSpan) } -func (wui *WebUI) transformTagSet(key string, tags []string) *sxpf.List { +func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair { if len(tags) == 0 { - return sxpf.Nil() + return nil } - space := sxpf.MakeString(" ") - text := make([]sxpf.Object, 0, 2*len(tags)) + space := sx.MakeString(" ") + text := make([]sx.Object, 0, 2*len(tags)) for _, tag := range tags { text = append(text, space, wui.transformLink(key, tag, tag)) } - return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) -} - -func (wui *WebUI) transformTimestamp(ts time.Time) sxpf.Object { - return sxpf.MakeList( - wui.sf.MustMake("time"), - sxpf.MakeList( - wui.sf.MustMake(sxhtml.NameSymAttr), - sxpf.Cons(wui.sf.MustMake("datetime"), sxpf.MakeString(ts.Format("2006-01-02T15:04:05"))), - ), - sxpf.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sxpf.MakeString(ts.Format("2006-01-02 15:04:05"))), - ) -} - -func (wui *WebUI) transformURL(val string) sxpf.Object { - text := sxpf.MakeString(val) - u, err := url.Parse(val) - if err == nil { - if us := u.String(); us != "" { - return sxpf.MakeList( - wui.symA, - sxpf.MakeList( - wui.symAttr, - sxpf.Cons(wui.symHref, sxpf.MakeString(val)), - sxpf.Cons(wui.sf.MustMake("target"), sxpf.MakeString("_blank")), - sxpf.Cons(wui.sf.MustMake("rel"), sxpf.MakeString("noopener noreferrer")), - ), - text, - ) - } - } - return text -} - -func (wui *WebUI) transformWord(key, word string) sxpf.Object { - return wui.transformLink(key, word, word) -} - -func (wui *WebUI) transformWordSet(key string, words []string) sxpf.Object { - if len(words) == 0 { - return sxpf.Nil() - } - space := sxpf.MakeString(" ") - text := make([]sxpf.Object, 0, 2*len(words)) - for _, tag := range words { - text = append(text, space, wui.transformWord(key, tag)) - } - return sxpf.MakeList(text[1:]...).Cons(wui.sf.MustMake("span")) -} - -func (wui *WebUI) transformLink(key, value, text string) *sxpf.List { - return sxpf.MakeList( - wui.symA, - sxpf.MakeList( - wui.symAttr, - sxpf.Cons(wui.symHref, sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())), - ), - sxpf.MakeString(text), + return sx.MakeList(text[1:]...).Cons(wui.symSpan) +} + +func (wui *WebUI) transformWordSet(key string, words []string) sx.Object { + if len(words) == 0 { + return sx.Nil() + } + space := sx.MakeString(" ") + text := make([]sx.Object, 0, 2*len(words)) + for _, word := range words { + text = append(text, space, wui.transformLink(key, word, word)) + } + return sx.MakeList(text[1:]...).Cons(wui.symSpan) +} + +func (wui *WebUI) transformLink(key, value, text string) *sx.Pair { + return sx.MakeList( + wui.symA, + sx.MakeList( + wui.symAttr, + sx.Cons(wui.symHref, sx.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())), + ), + sx.MakeString(text), ) } type evalMetadataFunc = func(string) ast.InlineSlice @@ -179,22 +151,22 @@ return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) } } type getTextTitleFunc func(id.Zid) (string, int) -func (wui *WebUI) makeGetTextTitle(ctx context.Context, getMeta usecase.GetMeta) getTextTitleFunc { +func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc { return func(zid id.Zid) (string, int) { - m, err := getMeta.Run(box.NoEnrichContext(ctx), zid) + z, err := getZettel.Run(box.NoEnrichContext(ctx), zid) if err != nil { if errors.Is(err, &box.ErrNotAllowed{}) { return "", -1 } return "", 0 } - return parser.NormalizedSpacedText(m.GetTitle()), 1 + return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1 } } -func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sxpf.Object { +func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object { is := evalMetadata(value) - return gen.InlinesSxHTML(&is).Cons(wui.sf.MustMake("span")) + return gen.InlinesSxHTML(&is).Cons(wui.symSpan) } Index: web/adapter/webui/lists.go ================================================================== --- web/adapter/webui/lists.go +++ web/adapter/webui/lists.go @@ -12,14 +12,15 @@ import ( "context" "io" "net/http" + "strconv" "strings" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/ast" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/evaluator" @@ -30,32 +31,32 @@ "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML. -func (wui *WebUI) MakeListHTMLMetaHandler(listMeta usecase.ListMeta) http.HandlerFunc { +func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { q := adapter.GetQuery(r.URL.Query()) q = q.SetDeterministic() ctx := r.Context() - metaList, err := listMeta.Run(ctx, q) + metaSeq, err := queryMeta.Run(ctx, q) if err != nil { wui.reportError(ctx, w, err) return } if actions := q.Actions(); len(actions) > 0 { switch actions[0] { case "ATOM": - wui.renderAtom(w, q, metaList) + wui.renderAtom(w, q, metaSeq) return case "RSS": - wui.renderRSS(ctx, w, q, metaList) + wui.renderRSS(ctx, w, q, metaSeq) return } } - var content, endnotes *sxpf.List - if bn := evaluator.QueryAction(ctx, q, metaList, wui.rtConfig); bn != nil { + var content, endnotes *sx.Pair + if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil { enc := wui.getSimpleHTMLEncoder() content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn}) if err != nil { wui.reportError(ctx, w, err) return @@ -66,26 +67,31 @@ env, rb := wui.createRenderEnv( ctx, "list", wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), user) if q == nil { - rb.bindString("heading", sxpf.MakeString(wui.rtConfig.GetSiteName())) + rb.bindString("heading", sx.MakeString(wui.rtConfig.GetSiteName())) } else { var sb strings.Builder q.PrintHuman(&sb) - rb.bindString("heading", sxpf.MakeString(sb.String())) + rb.bindString("heading", sx.MakeString(sb.String())) } - rb.bindString("query-value", sxpf.MakeString(q.String())) + rb.bindString("query-value", sx.MakeString(q.String())) rb.bindString("content", content) rb.bindString("endnotes", endnotes) + apiURL := wui.NewURLBuilder('z').AppendQuery(q.String()) + seed, found := q.GetSeed() + if found { + apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed)) + } else { + seed = 0 + } + rb.bindString("plain-url", sx.MakeString(apiURL.String())) + rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String())) if wui.canCreate(ctx, user) { - seed, found := q.GetSeed() - if !found { - seed = 0 - } - rb.bindString("create-url", sxpf.MakeString(wui.createNewURL)) - rb.bindString("seed", sxpf.Int64(seed)) + rb.bindString("create-url", sx.MakeString(wui.createNewURL)) + rb.bindString("seed", sx.Int64(seed)) } if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env) } if err != nil { Index: web/adapter/webui/login.go ================================================================== --- web/adapter/webui/login.go +++ web/adapter/webui/login.go @@ -12,12 +12,12 @@ import ( "context" "net/http" - "codeberg.org/t73fde/sxpf" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) @@ -36,12 +36,11 @@ } } func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) { env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil) - rb.bindString("heading", sxpf.MakeString("Login")) - rb.bindString("retry", sxpf.MakeBoolean(retry)) + rb.bindString("retry", sx.MakeBoolean(retry)) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env) } if err := rb.err; err != nil { wui.reportError(ctx, w, err) Index: web/adapter/webui/rename_zettel.go ================================================================== --- web/adapter/webui/rename_zettel.go +++ web/adapter/webui/rename_zettel.go @@ -13,40 +13,41 @@ import ( "fmt" "net/http" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" ) // MakeGetRenameZettelHandler creates a new HTTP handler to display the // HTML rename view of a zettel. -func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta) http.HandlerFunc { +func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() zid, err := id.Parse(r.URL.Path[1:]) if err != nil { wui.reportError(ctx, w, box.ErrNotFound) return } - m, err := getMeta.Run(ctx, zid) + z, err := getZettel.Run(ctx, zid) if err != nil { wui.reportError(ctx, w, err) return } + m := z.Meta user := server.GetUser(ctx) env, rb := wui.createRenderEnv( ctx, "rename", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user) - rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getMeta))) + rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel))) wui.bindCommonZettelData(ctx, &rb, user, m, nil) if rb.err == nil { err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env) } if err != nil { @@ -64,15 +65,21 @@ wui.reportError(ctx, w, box.ErrNotFound) return } if err = r.ParseForm(); err != nil { + wui.log.Trace().Err(err).Msg("unable to read rename zettel form") wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form")) return } - if formCurZid, err1 := id.Parse( - r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid { + formCurZidStr := r.PostFormValue("curzid") + if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid { + if err1 != nil { + wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid") + } else if formCurZid != curZid { + wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)") + } wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form")) return } formNewZid := strings.TrimSpace(r.PostFormValue("newzid")) newZid, err := id.Parse(formNewZid) Index: web/adapter/webui/response.go ================================================================== --- web/adapter/webui/response.go +++ web/adapter/webui/response.go @@ -11,11 +11,11 @@ package webui import ( "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" ) func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) { us := ub.String() wui.log.Debug().Str("uri", us).Msg("redirect") Index: web/adapter/webui/template.go ================================================================== --- web/adapter/webui/template.go +++ web/adapter/webui/template.go @@ -12,25 +12,28 @@ import ( "bytes" "context" "fmt" + "io" "net/http" - - "codeberg.org/t73fde/sxhtml" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/builtins" - "codeberg.org/t73fde/sxpf/builtins/binding" - "codeberg.org/t73fde/sxpf/builtins/boolean" - "codeberg.org/t73fde/sxpf/builtins/callable" - "codeberg.org/t73fde/sxpf/builtins/cond" - "codeberg.org/t73fde/sxpf/builtins/env" - "codeberg.org/t73fde/sxpf/builtins/list" - "codeberg.org/t73fde/sxpf/builtins/quote" - "codeberg.org/t73fde/sxpf/eval" - "codeberg.org/t73fde/sxpf/reader" - "zettelstore.de/c/api" + "net/url" + + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxbuiltins" + "zettelstore.de/sx.fossil/sxbuiltins/binding" + "zettelstore.de/sx.fossil/sxbuiltins/boolean" + "zettelstore.de/sx.fossil/sxbuiltins/callable" + "zettelstore.de/sx.fossil/sxbuiltins/cond" + "zettelstore.de/sx.fossil/sxbuiltins/define" + "zettelstore.de/sx.fossil/sxbuiltins/env" + "zettelstore.de/sx.fossil/sxbuiltins/list" + "zettelstore.de/sx.fossil/sxbuiltins/quote" + "zettelstore.de/sx.fossil/sxeval" + "zettelstore.de/sx.fossil/sxhtml" + "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/box" "zettelstore.de/z/collect" "zettelstore.de/z/config" "zettelstore.de/z/parser" "zettelstore.de/z/web/adapter" @@ -38,135 +41,86 @@ "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) -func (wui *WebUI) createRenderEngine() *eval.Engine { - root := sxpf.MakeRootEnvironment() - engine := eval.MakeEngine(wui.sf, root, eval.MakeDefaultParser(), eval.MakeSimpleExecutor()) +func (wui *WebUI) createRenderEngine() *sxeval.Engine { + root := sx.MakeRootEnvironment() + engine := sxeval.MakeEngine(wui.sf, root) quote.InstallQuoteSyntax(root, wui.symQuote) quote.InstallQuasiQuoteSyntax(root, wui.symQQ, wui.symUQ, wui.symUQS) engine.BindSyntax("if", cond.IfS) engine.BindSyntax("and", boolean.AndS) engine.BindSyntax("or", boolean.OrS) engine.BindSyntax("lambda", callable.LambdaS) - engine.BindSyntax("let", binding.LetS) - engine.BindBuiltinEEA("bound?", env.BoundP) - engine.BindBuiltinEEA("map", callable.Map) - engine.BindBuiltinA("list", list.List) - engine.BindBuiltinA("car", list.Car) - engine.BindBuiltinA("cdr", list.Cdr) - engine.BindBuiltinA("pair-to-href", wui.sxnPairToHref) - engine.BindBuiltinA("pair-to-href-li", wui.sxnPairToHrefLi) - engine.BindBuiltinA("pairs-to-dl", wui.sxnPairsToDl) - engine.BindBuiltinA("make-enc-matrix", wui.sxnEncMatrix) - return engine -} - -func (wui *WebUI) sxnPairToHref(args []sxpf.Object) (sxpf.Object, error) { - err := builtins.CheckArgs(args, 1, 1) - pair, err := builtins.GetList(err, args, 0) - if err != nil { - return nil, err - } - href := sxpf.MakeList( - wui.symA, - sxpf.MakeList(wui.symAttr, sxpf.Cons(wui.symHref, pair.Cdr())), - pair.Car(), - ) - return href, nil -} -func (wui *WebUI) sxnPairToHrefLi(args []sxpf.Object) (sxpf.Object, error) { - href, err := wui.sxnPairToHref(args) - if err != nil { - return nil, err - } - return sxpf.MakeList(wui.symLi, href), nil -} -func (wui *WebUI) sxnPairsToDl(args []sxpf.Object) (sxpf.Object, error) { - err := builtins.CheckArgs(args, 1, 1) - pairs, err := builtins.GetList(err, args, 0) - if err != nil { - return nil, err - } - dl := sxpf.Cons(wui.symDl, nil) - curr := dl - for node := pairs; node != nil; node = node.Tail() { - if pair, isPair := sxpf.GetList(node.Car()); isPair { - curr = curr.AppendBang(sxpf.MakeList(wui.symDt, pair.Car())) - curr = curr.AppendBang(sxpf.MakeList(wui.symDd, pair.Cdr())) - } - } - return dl, nil -} - -func (wui *WebUI) sxnEncMatrix(args []sxpf.Object) (sxpf.Object, error) { - err := builtins.CheckArgs(args, 1, 1) - rows, err := builtins.GetList(err, args, 0) - if err != nil { - return nil, err - } - table := sxpf.Cons(wui.symTable, nil) - currRow := table - for node := rows; node != nil; node = node.Tail() { - row, isRow := sxpf.GetList(node.Car()) - if !isRow || row == nil { - continue - } - line := sxpf.Cons(sxpf.MakeList(wui.symTh, row.Car()), nil) - currLine := line - line = line.Cons(wui.symTr) - currRow = currRow.AppendBang(line) - for elem := row.Tail(); elem != nil; elem = elem.Tail() { - link, isLink := sxpf.GetList(elem.Car()) - if !isLink || link == nil { - continue - } - currLine = currLine.AppendBang(sxpf.MakeList( - wui.symTd, - sxpf.MakeList( - wui.symA, - sxpf.MakeList(wui.symAttr, sxpf.Cons(wui.symHref, link.Cdr())), - link.Car(), - ), - )) - } - } - return table, nil -} - -// createRenderEnv creates a new environment and populates it with all relevant data for the base template. -func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sxpf.Environment, renderBinder) { - userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) - env := sxpf.MakeChildEnvironment(wui.engine.RootEnvironment(), name, 128) - rb := makeRenderBinder(wui.sf, env, nil) - rb.bindString("lang", sxpf.MakeString(lang)) - rb.bindString("css-base-url", sxpf.MakeString(wui.cssBaseURL)) - rb.bindString("css-user-url", sxpf.MakeString(wui.cssUserURL)) - rb.bindString("css-role-url", sxpf.MakeString("")) - rb.bindString("title", sxpf.MakeString(title)) - rb.bindString("home-url", sxpf.MakeString(wui.homeURL)) - rb.bindString("with-auth", sxpf.MakeBoolean(wui.withAuth)) - rb.bindString("user-is-valid", sxpf.MakeBoolean(userIsValid)) - rb.bindString("user-zettel-url", sxpf.MakeString(userZettelURL)) - rb.bindString("user-ident", sxpf.MakeString(userIdent)) - rb.bindString("login-url", sxpf.MakeString(wui.loginURL)) - rb.bindString("logout-url", sxpf.MakeString(wui.logoutURL)) - rb.bindString("list-zettel-url", sxpf.MakeString(wui.listZettelURL)) - rb.bindString("list-roles-url", sxpf.MakeString(wui.listRolesURL)) - rb.bindString("list-tags-url", sxpf.MakeString(wui.listTagsURL)) - if wui.canRefresh(user) { - rb.bindString("refresh-url", sxpf.MakeString(wui.refreshURL)) - } - rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user)) - rb.bindString("search-url", sxpf.MakeString(wui.searchURL)) - rb.bindString("query-key-query", sxpf.MakeString(api.QueryKeyQuery)) - rb.bindString("query-key-seed", sxpf.MakeString(api.QueryKeySeed)) - rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer - rb.bindString("debug-mode", sxpf.MakeBoolean(wui.debug)) - rb.bindSymbol(wui.symMetaHeader, sxpf.Nil()) - rb.bindSymbol(wui.symDetail, sxpf.Nil()) + engine.BindSyntax("define", define.DefineS) + engine.BindSyntax("let", binding.LetS) + engine.BindBuiltinEEA("bound?", env.BoundP) + engine.BindBuiltinEEA("map", callable.Map) + engine.BindBuiltinEEA("apply", callable.Apply) + engine.BindBuiltinA("list", list.List) + engine.BindBuiltinA("append", list.Append) + engine.BindBuiltinA("car", list.Car) + engine.BindBuiltinA("cdr", list.Cdr) + + engine.BindBuiltinA("url-to-html", wui.url2html) + return engine +} + +func (wui *WebUI) url2html(args []sx.Object) (sx.Object, error) { + err := sxbuiltins.CheckArgs(args, 1, 1) + text, err := sxbuiltins.GetString(err, args, 0) + if err != nil { + return nil, err + } + if u, errURL := url.Parse(text.String()); errURL == nil { + if us := u.String(); us != "" { + return sx.MakeList( + wui.symA, + sx.MakeList( + wui.symAttr, + sx.Cons(wui.symHref, sx.MakeString(us)), + sx.Cons(wui.sf.MustMake("target"), sx.MakeString("_blank")), + sx.Cons(wui.sf.MustMake("rel"), sx.MakeString("noopener noreferrer")), + ), + text), nil + } + } + return text, nil +} + +// createRenderEnv creates a new environment and populates it with all relevant data for the base template. +func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sx.Environment, renderBinder) { + userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user) + env := sx.MakeChildEnvironment(wui.engine.RootEnvironment(), name, 128) + rb := makeRenderBinder(wui.sf, env, nil) + rb.bindString("lang", sx.MakeString(lang)) + rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL)) + rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL)) + rb.bindString("css-role-url", sx.MakeString("")) + rb.bindString("title", sx.MakeString(title)) + rb.bindString("home-url", sx.MakeString(wui.homeURL)) + rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth)) + rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid)) + rb.bindString("user-zettel-url", sx.MakeString(userZettelURL)) + rb.bindString("user-ident", sx.MakeString(userIdent)) + rb.bindString("login-url", sx.MakeString(wui.loginURL)) + rb.bindString("logout-url", sx.MakeString(wui.logoutURL)) + rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL)) + rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL)) + rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL)) + if wui.canRefresh(user) { + rb.bindString("refresh-url", sx.MakeString(wui.refreshURL)) + } + rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user)) + rb.bindString("search-url", sx.MakeString(wui.searchURL)) + rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery)) + rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed)) + rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer + rb.bindString("debug-mode", sx.MakeBoolean(wui.debug)) + rb.bindSymbol(wui.symMetaHeader, sx.Nil()) + rb.bindSymbol(wui.symDetail, sx.Nil()) return env, rb } func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) { if user == nil { @@ -175,34 +129,34 @@ return true, wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String(), user.GetDefault(api.KeyUserID, "") } type renderBinder struct { err error - make func(string) (*sxpf.Symbol, error) - bind func(*sxpf.Symbol, sxpf.Object) error + make func(string) (*sx.Symbol, error) + bind func(*sx.Symbol, sx.Object) error } -func makeRenderBinder(sf sxpf.SymbolFactory, env sxpf.Environment, err error) renderBinder { +func makeRenderBinder(sf sx.SymbolFactory, env sx.Environment, err error) renderBinder { return renderBinder{make: sf.Make, bind: env.Bind, err: err} } -func (rb *renderBinder) bindString(key string, obj sxpf.Object) { +func (rb *renderBinder) bindString(key string, obj sx.Object) { if rb.err == nil { sym, err := rb.make(key) if err == nil { rb.err = rb.bind(sym, obj) return } rb.err = err } } -func (rb *renderBinder) bindSymbol(sym *sxpf.Symbol, obj sxpf.Object) { +func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) { if rb.err == nil { rb.err = rb.bind(sym, obj) } } func (rb *renderBinder) bindKeyValue(key string, value string) { - rb.bindString("meta-"+key, sxpf.MakeString(value)) + rb.bindString("meta-"+key, sx.MakeString(value)) if kt := meta.Type(key); kt.IsSet { rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value))) } } @@ -209,53 +163,52 @@ func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) { strZid := m.Zid.String() apiZid := api.ZettelID(strZid) newURLBuilder := wui.NewURLBuilder - rb.bindString("zid", sxpf.MakeString(strZid)) - rb.bindString("web-url", sxpf.MakeString(wui.NewURLBuilder('h').SetZid(apiZid).String())) + rb.bindString("zid", sx.MakeString(strZid)) + rb.bindString("web-url", sx.MakeString(wui.NewURLBuilder('h').SetZid(apiZid).String())) if content != nil && wui.canWrite(ctx, user, m, *content) { - rb.bindString("edit-url", sxpf.MakeString(newURLBuilder('e').SetZid(apiZid).String())) + rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String())) } - rb.bindString("info-url", sxpf.MakeString(newURLBuilder('i').SetZid(apiZid).String())) + rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String())) if wui.canCreate(ctx, user) { if content != nil && !content.IsBinary() { - rb.bindString("copy-url", sxpf.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String())) + rb.bindString("copy-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String())) } - rb.bindString("version-url", sxpf.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) - rb.bindString("folge-url", sxpf.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) + rb.bindString("version-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String())) + rb.bindString("child-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String())) + rb.bindString("folge-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String())) } if wui.canRename(ctx, user, m) { - rb.bindString("rename-url", sxpf.MakeString(wui.NewURLBuilder('b').SetZid(apiZid).String())) + rb.bindString("rename-url", sx.MakeString(wui.NewURLBuilder('b').SetZid(apiZid).String())) } if wui.canDelete(ctx, user, m) { - rb.bindString("delete-url", sxpf.MakeString(wui.NewURLBuilder('d').SetZid(apiZid).String())) + rb.bindString("delete-url", sx.MakeString(wui.NewURLBuilder('d').SetZid(apiZid).String())) } if val, found := m.Get(api.KeyUselessFiles); found { - rb.bindString("useless", sxpf.Cons(sxpf.MakeString(val), nil)) + rb.bindString("useless", sx.Cons(sx.MakeString(val), nil)) } - rb.bindString("context-url", sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(api.ContextDirective+" "+strZid).String())) - rb.bindString("role-url", - sxpf.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+m.GetDefault(api.KeyRole, "")).String())) + rb.bindString("context-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(strZid+" "+api.ContextDirective).String())) // Ensure to have title, role, tags, and syntax included as "meta-*" rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, "")) rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, "")) rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, "")) rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, "")) - sentinel := sxpf.Cons(nil, nil) + sentinel := sx.Cons(nil, nil) curr := sentinel for _, p := range m.ComputedPairs() { key, value := p.Key, p.Value - curr = curr.AppendBang(sxpf.Cons(sxpf.MakeString(key), sxpf.MakeString(value))) + curr = curr.AppendBang(sx.Cons(sx.MakeString(key), sx.MakeString(value))) rb.bindKeyValue(key, value) } rb.bindString("metapairs", sentinel.Tail()) } -func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sxpf.List) { +func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) { if !wui.canCreate(ctx, user) { return nil } ctx = box.NoEnrichContext(ctx) menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid) @@ -266,88 +219,120 @@ for i := len(refs) - 1; i >= 0; i-- { zid, err2 := id.Parse(refs[i].URL.Path) if err2 != nil { continue } - m, err2 := wui.box.GetMeta(ctx, zid) + z, err2 := wui.box.GetZettel(ctx, zid) if err2 != nil { continue } - if !wui.policy.CanRead(user, m) { + if !wui.policy.CanRead(user, z.Meta) { continue } - text := sxpf.MakeString(parser.NormalizedSpacedText(m.GetTitle())) - link := sxpf.MakeString(wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())). + text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle())) + link := sx.MakeString(wui.NewURLBuilder('c').SetZid(api.ZettelID(zid.String())). AppendKVQuery(queryKeyAction, valueActionNew).String()) - lst = lst.Cons(sxpf.Cons(text, link)) + lst = lst.Cons(sx.Cons(text, link)) } return lst } -func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sxpf.List { +func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair { if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil { if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil { htmlEnc := wui.getSimpleHTMLEncoder().SetUnique("footer-") if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil { if content != nil && endnotes != nil { - content.LastPair().SetCdr(sxpf.Cons(endnotes, nil)) + content.LastPair().SetCdr(sx.Cons(endnotes, nil)) } return content } } } return nil } -func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sxpf.Environment) (eval.Expr, error) { - wui.mxCache.RLock() - t, ok := wui.templateCache[zid] - wui.mxCache.RUnlock() - if ok { +func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid) error { + if expr := wui.getSxnCache(zid); expr != nil { + return nil + } + rdr, err := wui.makeZettelReader(ctx, zid) + if err != nil { + return err + } + for { + form, err2 := rdr.Read() + if err2 != nil { + if err2 == io.EOF { + wui.setSxnCache(zid, sxeval.TrueExpr) // Hack to load only once + return nil + } + return err2 + } + wui.log.Trace().Str("form", form.Repr()).Msg("Load sxn code") + + _, err2 = wui.engine.Eval(wui.engine.GetToplevelEnv(), form) + if err2 != nil { + return err2 + } + } +} + +func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sxeval.Expr, error) { + if t := wui.getSxnCache(zid); t != nil { return t, nil } - templateZettel, err := wui.box.GetZettel(ctx, zid) + reader, err := wui.makeZettelReader(ctx, zid) if err != nil { return nil, err } - reader := reader.MakeReader(bytes.NewReader(templateZettel.Content.AsBytes()), reader.WithSymbolFactory(wui.sf)) - quote.InstallQuoteReader(reader, wui.symQuote, '\'') - quote.InstallQuasiQuoteReader(reader, wui.symQQ, '`', wui.symUQ, ',', wui.symUQS, '@') - objs, err := reader.ReadAll() if err != nil { wui.log.IfErr(err).Zid(zid).Msg("reading sxn template") return nil, err } if len(objs) != 1 { return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs)) } - t, err = wui.engine.Parse(env, objs[0]) + t, err := wui.engine.Parse(env, objs[0]) + if err != nil { + return nil, err + } + + wui.setSxnCache(zid, wui.engine.Rework(env, t)) + return t, nil +} +func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) { + ztl, err := wui.box.GetZettel(ctx, zid) if err != nil { return nil, err } - wui.mxCache.Lock() - wui.templateCache[zid] = t - wui.mxCache.Unlock() - return t, nil + reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()), sxreader.WithSymbolFactory(wui.sf)) + quote.InstallQuoteReader(reader, wui.symQuote, '\'') + quote.InstallQuasiQuoteReader(reader, wui.symQQ, '`', wui.symUQ, ',', wui.symUQS, '@') + return reader, nil } -func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sxpf.Environment) (sxpf.Object, error) { +func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sx.Object, error) { templateExpr, err := wui.getSxnTemplate(ctx, zid, env) if err != nil { return nil, err } return wui.engine.Execute(env, templateExpr) } -func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sxpf.Environment) error { +func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sx.Environment) error { return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env) } -func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sxpf.Environment) error { +func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sx.Environment) error { + err := wui.loadSxnCodeZettel(ctx, id.TemplateSxnZid) + if err != nil { + return err + } detailObj, err := wui.evalSxnTemplate(ctx, templateID, env) if err != nil { return err } env.Bind(wui.symDetail, detailObj) @@ -354,10 +339,11 @@ pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, env) if err != nil { return err } + wui.log.Debug().Str("page", pageObj.Repr()).Msg("render") gen := sxhtml.NewGenerator(wui.sf, sxhtml.WithNewline) var sb bytes.Buffer _, err = gen.WriteHTML(&sb, pageObj) if err != nil { @@ -371,29 +357,31 @@ func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) { code, text := adapter.CodeMessageFromError(err) if code == http.StatusInternalServerError { wui.log.Error().Msg(err.Error()) + } else { + wui.log.Trace().Err(err).Msg("reportError") } user := server.GetUser(ctx) env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user) - rb.bindString("heading", sxpf.MakeString(http.StatusText(code))) - rb.bindString("message", sxpf.MakeString(text)) + rb.bindString("heading", sx.MakeString(http.StatusText(code))) + rb.bindString("message", sx.MakeString(text)) if rb.err == nil { rb.err = wui.renderSxnTemplate(ctx, w, id.ErrorTemplateZid, env) } if errBind := rb.err; errBind != nil { wui.log.Error().Err(errBind).Msg("while rendering error message") fmt.Fprintf(w, "Error while rendering error message: %v", errBind) } } -func makeStringList(sl []string) *sxpf.List { +func makeStringList(sl []string) *sx.Pair { if len(sl) == 0 { return nil } - result := sxpf.Nil() + result := sx.Nil() for i := len(sl) - 1; i >= 0; i-- { - result = result.Cons(sxpf.MakeString(sl[i])) + result = result.Cons(sx.MakeString(sl[i])) } return result } Index: web/adapter/webui/webui.go ================================================================== --- web/adapter/webui/webui.go +++ web/adapter/webui/webui.go @@ -16,14 +16,14 @@ "net/http" "strings" "sync" "time" - "codeberg.org/t73fde/sxhtml" - "codeberg.org/t73fde/sxpf" - "codeberg.org/t73fde/sxpf/eval" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" + "zettelstore.de/sx.fossil" + "zettelstore.de/sx.fossil/sxeval" + "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" @@ -47,11 +47,11 @@ policy auth.Policy evalZettel *usecase.Evaluate mxCache sync.RWMutex - templateCache map[id.Zid]eval.Expr + templateCache map[id.Zid]sxeval.Expr mxRoleCSSMap sync.RWMutex roleCSSMap map[string]id.Zid tokenLifetime time.Duration @@ -66,40 +66,36 @@ loginURL string logoutURL string searchURL string createNewURL string - sf sxpf.SymbolFactory - engine *eval.Engine + sf sx.SymbolFactory + engine *sxeval.Engine genHTML *sxhtml.Generator - symQuote *sxpf.Symbol - symQQ, symUQ, symUQS *sxpf.Symbol - symMetaHeader *sxpf.Symbol - symDetail *sxpf.Symbol - symA, symHref *sxpf.Symbol - symAttr *sxpf.Symbol - symLi *sxpf.Symbol - symDl, symDt, symDd *sxpf.Symbol - symTable *sxpf.Symbol - symTr, symTh, symTd *sxpf.Symbol + symQuote, symQQ *sx.Symbol + symUQ, symUQS *sx.Symbol + symMetaHeader *sx.Symbol + symDetail *sx.Symbol + symA, symHref *sx.Symbol + symSpan *sx.Symbol + symAttr *sx.Symbol } type webuiBox interface { CanCreateZettel(ctx context.Context) bool GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) - GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool AllowRenameZettel(ctx context.Context, zid id.Zid) bool CanDeleteZettel(ctx context.Context, zid id.Zid) bool } // New creates a new WebUI struct. func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager, mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI { loginoutBase := ab.NewURLBuilder('i') - sf := sxpf.MakeMappedFactory() + sf := sx.MakeMappedFactory() wui := &WebUI{ log: log, debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool), ab: ab, @@ -132,50 +128,59 @@ symUQ: sf.MustMake("unquote"), symUQS: sf.MustMake("unquote-splicing"), symDetail: sf.MustMake("DETAIL"), symMetaHeader: sf.MustMake("META-HEADER"), symA: sf.MustMake("a"), - symAttr: sf.MustMake(sxhtml.NameSymAttr), symHref: sf.MustMake("href"), - symLi: sf.MustMake("li"), - symDl: sf.MustMake("dl"), - symDt: sf.MustMake("dt"), - symDd: sf.MustMake("dd"), - symTable: sf.MustMake("table"), - symTr: sf.MustMake("tr"), - symTh: sf.MustMake("th"), - symTd: sf.MustMake("td"), + symSpan: sf.MustMake("span"), + symAttr: sf.MustMake(sxhtml.NameSymAttr), } wui.engine = wui.createRenderEngine() wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid}) mgr.RegisterObserver(wui.observe) return wui } func (wui *WebUI) observe(ci box.UpdateInfo) { wui.mxCache.Lock() - if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid || ci.Zid == id.BaseTemplateZid+30000 { - wui.templateCache = make(map[id.Zid]eval.Expr, len(wui.templateCache)) + if ci.Reason == box.OnReload { + wui.templateCache = make(map[id.Zid]sxeval.Expr, len(wui.templateCache)) } else { delete(wui.templateCache, ci.Zid) } wui.mxCache.Unlock() + wui.mxRoleCSSMap.Lock() if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid { wui.roleCSSMap = nil } wui.mxRoleCSSMap.Unlock() } + +func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) { + wui.mxCache.Lock() + wui.templateCache[zid] = expr + wui.mxCache.Unlock() +} +func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr { + wui.mxCache.RLock() + expr, found := wui.templateCache[zid] + wui.mxCache.RUnlock() + if found { + return expr + } + return nil +} func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m *meta.Meta) (id.Zid, error) { wui.mxRoleCSSMap.RLock() if wui.roleCSSMap == nil { wui.mxRoleCSSMap.RUnlock() wui.mxRoleCSSMap.Lock() - mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid) + zMap, err := wui.box.GetZettel(ctx, id.RoleCSSMapZid) if err == nil { - wui.roleCSSMap = createRoleCSSMap(mMap) + wui.roleCSSMap = createRoleCSSMap(zMap.Meta) } wui.mxRoleCSSMap.Unlock() if err != nil { return id.Invalid, err } Index: web/content/content.go ================================================================== --- web/content/content.go +++ web/content/content.go @@ -14,11 +14,11 @@ import ( "mime" "net/http" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/meta" ) const ( @@ -28,18 +28,19 @@ mimeJPEG = "image/jpeg" mimeMarkdown = "text/markdown; charset=utf-8" JSON = "application/json" PlainText = "text/plain; charset=utf-8" mimePNG = "image/png" + SXPF = PlainText mimeWEBP = "image/webp" ) var encoding2mime = map[api.EncodingEnum]string{ api.EncoderHTML: mimeHTML, api.EncoderMD: mimeMarkdown, - api.EncoderSz: PlainText, - api.EncoderSHTML: PlainText, + api.EncoderSz: SXPF, + api.EncoderSHTML: SXPF, api.EncoderText: PlainText, api.EncoderZmk: PlainText, } // MIMEFromEncoding returns the MIME encoding for a given zettel encoding @@ -61,11 +62,11 @@ meta.SyntaxMD: mimeMarkdown, meta.SyntaxNone: "", meta.SyntaxPlain: PlainText, meta.SyntaxPNG: mimePNG, meta.SyntaxSVG: "image/svg+xml", - meta.SyntaxSxn: PlainText, + meta.SyntaxSxn: SXPF, meta.SyntaxText: PlainText, meta.SyntaxTxt: PlainText, meta.SyntaxWebp: mimeWEBP, meta.SyntaxZmk: "text/x-zmk; charset=utf-8", Index: web/server/impl/impl.go ================================================================== --- web/server/impl/impl.go +++ web/server/impl/impl.go @@ -14,11 +14,11 @@ import ( "context" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/meta" ) Index: web/server/impl/router.go ================================================================== --- web/server/impl/router.go +++ web/server/impl/router.go @@ -14,11 +14,11 @@ "io" "net/http" "regexp" "strings" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) Index: web/server/server.go ================================================================== --- web/server/server.go +++ web/server/server.go @@ -14,11 +14,11 @@ import ( "context" "net/http" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // UserRetriever allows to retrieve user data based on a given zettel identifier. Index: www/build.md ================================================================== --- www/build.md +++ www/build.md @@ -8,28 +8,25 @@ * [unparam](https://mvdan.cc/unparam), * [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). -See folder docs/development (a zettel box) for details. +See folder `docs/development` (a zettel box) for details. ## Clone the repository Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). 1. Create a directory to store your Fossil repositories. - Let's assume, you have created $HOME/fossils. + Let's assume, you have created `$HOME/fossils`. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`. 1. Create a working directory. - Let's assume, you have created $HOME/zettelstore. + Let's assume, you have created `$HOME/zettelstore`. 1. Change into this directory: `cd $HOME/zettelstore`. 1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`. -(If you are not able to use Fossil, you could try the GitHub mirror -.) - ## The build tool -In directory tools there is a Go file called build.go. +In the directory `tools` there is a Go file called `build.go`. It automates most aspects, (hopefully) platform-independent. The script is called as: ``` @@ -40,11 +37,11 @@ It outputs all commands called by the tool. Some important `COMMAND`s are: * `build`: builds the software with correct version information and puts it - into a freshly created directory bin. + into a freshly created directory `bin`. * `check`: checks the current state of the working directory to be ready for release (or commit). * `clean`: removes the build directories and cleans the Go cache. * `version`: prints the current version information. * `tools`: installs / updates the tools described above: staticcheck, shadow, @@ -60,5 +57,21 @@ In case of errors, please send the output of the verbose execution: ``` go run tools/build.go -v build ``` + +## A note on the use of Fossil +Zettelstore is managed by the Fossil version control system. +Fossil is an alternative to the ubiquitous Git version control system. +However, Go seems to prefer Git and popular platforms that just support Git. + +Some dependencies of Zettelstore, namely [Zettelstore client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are also managed by Fossil. +Depending on your development setup, some error messages might occur. + +If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zezzelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`). +Since the Go build system is coupled with Git and some special platforms, you allow ot to download a Fossil repository from the host `zettelstore.de`. +The build tool set `GOVCS` to the right value, but you may use other `go` commands that try to download a Fossil repository. + +On some operating systems, namely Termux on Android, an error message might state that an user cannot be determined (`cannot determine user`). +In this case, Fossil is allowed to download the repository, but cannot associate it with an user name. +Set the environment variable `USER` to any user name, like: `USER=nobody go run tools/build.go build`. Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,9 +1,68 @@ Change Log + +

Changes for Version 0.14.0 (pending)

+ -

Changes for Version 0.13.0 (pending)

+

Changes for Version 0.13.0 (2023-08-07)

+ * There are for new search operators: less, not less, greater, not greater. + These use the same syntax as the operators prefix, not prefix, suffix, not + suffix. The latter are no denoted as [, ![, ], + and !]. The first may operate numerically for metadata like + numbers, timestamps, and zettel identifier. They are not supported for + full-test search. + (breaking: api, webui) + * The API endpoint /o/{ID} (order of zettel ID) is no longer + available. Please use the query expression {ID} ITEMS instead. + (breaking: api) + * The API endpoint /u/{ID} (unlinked references of zettel ID) is no + longer available. Please use the query expression {ID} UNLINKED + instead. + (breaking: api) + * All API endpoints allow to encode zettel data with the data + encodings, incl. creating, updating, retrieving, and querying zettel. + (major: api) + * Change syntax for context query to zid ... CONTEXT. This will + allow to add more directives that operate on zettel identifier. Old syntax + CONTEXT zid will be removed in 0.14. + (major, deprecated) + * Add query directive ITEMS that will produce a list of metadata + of all zettel that are referenced by the originating zettel in a top-level + list. It replaces the API endpoint /o/{ID} (and makes it more + useful). + (major: api, webui) + * Add query directive UNLINKED that will produce a list of metadata + of all zettel that are mentioning the originating zettel in a top-level, + but do not mention them. It replaces the API endpoint /u/{ID} + (and makes it more useful). + (major: api, webui) + * Add query directive IDENT to distinguish a search for a zettel + identifier (“{ID}”), that will list all metadata of zettel + containing that zettel identifier, and a request to just list the metadata + of given zettel (“{ID} IDENT”). The latter could be filtered + further. + (minor: api, webui) + * Add support for metadata key folge-role. + (minor) + * Allow to create a child from a given zettel. + (minor: webui) + * Make zettel entry/edit form a little friendlier: auto-prepend missing '#' + to tags; ensure that role and syntax receive just a word. + (minor: webui) + * Use a zettel that defines builtins for evaluating WebUI templates. + (minor: webui) + * Add links to retrieve result of a query in other formats. + (minor: webui) + * Always log the found configuration file. + (minor: server) + * The use of the json zettel encoding is deprecated (since version + 0.12.0). Support for this encoding will be removed in version 0.14.0. + Please use the new data encoding instead. + (deprecated: api) + * Some smaller bug fixes and improvements, to the software and to the + documentation.

Changes for Version 0.12.0 (2023-06-05)

* Syntax of templates for the web user interface are changed from Mustache to Sxn (S-Expressions). Mustache is no longer supported, nowhere in the Index: www/download.wiki ================================================================== --- www/download.wiki +++ www/download.wiki @@ -7,20 +7,20 @@ * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

ZIP-ped Executables

-Build: v0.12.0 (2023-06-05). +Build: v0.13.0 (2023-08-07). - * [/uv/zettelstore-0.12.0-linux-amd64.zip|Linux] (amd64) - * [/uv/zettelstore-0.12.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) - * [/uv/zettelstore-0.12.0-darwin-arm64.zip|macOS] (arm64) - * [/uv/zettelstore-0.12.0-darwin-amd64.zip|macOS] (amd64) - * [/uv/zettelstore-0.12.0-windows-amd64.zip|Windows] (amd64) + * [/uv/zettelstore-0.13.0-linux-amd64.zip|Linux] (amd64) + * [/uv/zettelstore-0.13.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) + * [/uv/zettelstore-0.13.0-darwin-arm64.zip|macOS] (arm64) + * [/uv/zettelstore-0.13.0-darwin-amd64.zip|macOS] (amd64) + * [/uv/zettelstore-0.13.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual.

Zettel for the manual

As a starter, you can download the zettel for the manual -[/uv/manual-0.12.0.zip|here]. +[/uv/manual-0.13.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. Index: www/index.wiki ================================================================== --- www/index.wiki +++ www/index.wiki @@ -20,19 +20,19 @@ access Zettelstore via its API more easily, [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. -[https://twitter.com/search?q=%40t73fde%20zettelstore&f=live|Stay tuned] … +[https://mastodon.social/tags/Zettelstore|Stay tuned] …
-

Latest Release: 0.12.0 (2023-06-05)

+

Latest Release: 0.13.0 (2023-06-05)

* [./download.wiki|Download] - * [./changes.wiki#0_12|Change summary] - * [/timeline?p=v0.12.0&bt=v0.11.0&y=ci|Check-ins for version 0.12.0], - [/vdiff?to=v0.12.0&from=v0.11.0|content diff] - * [/timeline?df=v0.12.0&y=ci|Check-ins derived from the 0.12.0 release], - [/vdiff?from=v0.12.0&to=trunk|content diff] + * [./changes.wiki#0_13|Change summary] + * [/timeline?p=v0.13.0&bt=v0.12.0&y=ci|Check-ins for version 0.13.0], + [/vdiff?to=v0.13.0&from=v0.12.0|content diff] + * [/timeline?df=v0.13.0&y=ci|Check-ins derived from the 0.13.0 release], + [/vdiff?from=v0.13.0&to=trunk|content diff] * [./plan.wiki|Limitations and planned improvements] * [/timeline?t=release|Timeline of all past releases]

Build instructions

Index: www/plan.wiki ================================================================== --- www/plan.wiki +++ www/plan.wiki @@ -1,14 +1,12 @@ Limitations and planned improvements Here is a list of some shortcomings of Zettelstore. They are planned to be solved. -

Serious limitations

- * … - -

Smaller limitations

+ * Zettelstore must have indexed all zettel to make use of queries. + Otherwise not all zettel may be returned. * Quoted attribute values are not yet supported in Zettelmarkup: {key="value with space"}. * The horizontal tab character (U+0009) is not supported. * Missing support for citation keys. * Changing the content syntax is not reflected in file extension. Index: zettel/id/id.go ================================================================== --- zettel/id/id.go +++ zettel/id/id.go @@ -14,11 +14,11 @@ import ( "strconv" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" ) // Zid is the internal identifier of a zettel. Typically, it is a // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, @@ -44,10 +44,11 @@ InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) + TemplateSxnZid = MustParse(api.ZidSxnTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) Index: zettel/id/id_test.go ================================================================== --- zettel/id/id_test.go +++ zettel/id/id_test.go @@ -35,10 +35,11 @@ "04000000000000", "50000000000000", "99999999999999", "00001007030200", "20200310195100", + "12345678901234", } for i, sid := range validIDs { zid, err := id.Parse(sid) if err != nil { @@ -55,10 +56,11 @@ "", "0", "a", "00000000000000", "0000000000000a", "000000000000000", "20200310T195100", + "+1234567890123", } for i, sid := range invalidIDs { if zid, err := id.Parse(sid); err == nil { t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid) Index: zettel/meta/meta.go ================================================================== --- zettel/meta/meta.go +++ zettel/meta/meta.go @@ -16,12 +16,12 @@ "sort" "strings" "unicode" "unicode/utf8" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/input" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) @@ -138,10 +138,11 @@ registerKey(api.KeyCopyright, TypeString, usageUser, "") registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") registerKey(api.KeyCredential, TypeCredential, usageUser, "") registerKey(api.KeyDead, TypeIDSet, usageProperty, "") registerKey(api.KeyExpire, TypeTimestamp, usageUser, "") + registerKey(api.KeyFolgeRole, TypeWord, usageUser, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") registerKey(api.KeyLicense, TypeEmpty, usageUser, "") registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) @@ -240,12 +241,12 @@ // SetNonEmpty stores the given value under the given key, if the value is non-empty. // An empty value will delete the previous association. func (m *Meta) SetNonEmpty(key, value string) { if value == "" { delete(m.pairs, key) - } else if key != api.KeyID { - m.pairs[key] = trimValue(value) + } else { + m.Set(key, trimValue(value)) } } func trimValue(value string) string { return strings.TrimFunc(value, input.IsSpace) Index: zettel/meta/meta_test.go ================================================================== --- zettel/meta/meta_test.go +++ zettel/meta/meta_test.go @@ -12,11 +12,11 @@ import ( "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" ) const testID = id.Zid(98765432101234) Index: zettel/meta/parse.go ================================================================== --- zettel/meta/parse.go +++ zettel/meta/parse.go @@ -11,12 +11,12 @@ package meta import ( "strings" - "zettelstore.de/c/api" - "zettelstore.de/c/maps" + "zettelstore.de/client.fossil/api" + "zettelstore.de/client.fossil/maps" "zettelstore.de/z/input" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) Index: zettel/meta/parse_test.go ================================================================== --- zettel/meta/parse_test.go +++ zettel/meta/parse_test.go @@ -12,11 +12,11 @@ import ( "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/input" "zettelstore.de/z/zettel/meta" ) func parseMetaStr(src string) *meta.Meta { Index: zettel/meta/type.go ================================================================== --- zettel/meta/type.go +++ zettel/meta/type.go @@ -14,11 +14,11 @@ "strconv" "strings" "sync" "time" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" ) // DescriptionType is a description of a specific key type. type DescriptionType struct { @@ -109,10 +109,17 @@ values[i] = trimValue(val) } m.pairs[key] = strings.Join(values, " ") } } + +// SetWord stores the given word under the given key. +func (m *Meta) SetWord(key, word string) { + if slist := ListFromValue(word); len(slist) > 0 { + m.Set(key, slist[0]) + } +} // SetNow stores the current timestamp under the given key. func (m *Meta) SetNow(key string) { m.Set(key, time.Now().Local().Format(id.ZidLayout)) } Index: zettel/meta/values.go ================================================================== --- zettel/meta/values.go +++ zettel/meta/values.go @@ -11,11 +11,11 @@ package meta import ( "fmt" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" ) // Supported syntax values. const ( SyntaxCSS = api.ValueSyntaxCSS Index: zettel/meta/write_test.go ================================================================== --- zettel/meta/write_test.go +++ zettel/meta/write_test.go @@ -12,11 +12,11 @@ import ( "strings" "testing" - "zettelstore.de/c/api" + "zettelstore.de/client.fossil/api" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) const testID = id.Zid(98765432101234)