ADDED .deepsource.toml Index: .deepsource.toml ================================================================== --- .deepsource.toml +++ .deepsource.toml @@ -0,0 +1,8 @@ +version = 1 + +[[analyzers]] +name = "go" +enabled = true + + [analyzers.meta] +import_paths = ["github.com/zettelstore/zettelstore"] Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,2 +1,3 @@ bin/* releases/* +parser/pikchr/*.out Index: LICENSE.txt ================================================================== --- LICENSE.txt +++ LICENSE.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-present Detlef Stern +Copyright (c) 2020-2022 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the Index: Makefile ================================================================== --- Makefile +++ Makefile @@ -1,31 +1,31 @@ -## Copyright (c) 2020-present Detlef Stern +## Copyright (c) 2020-2021 Detlef Stern ## -## This file is part of Zettelstore. +## 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. -.PHONY: check relcheck api version build release clean +.PHONY: check relcheck api build release clean check: - go run tools/check/check.go + go run tools/build.go check relcheck: - go run tools/check/check.go -r + go run tools/build.go relcheck api: - go run tools/testapi/testapi.go + go run tools/build.go testapi version: - @echo $(shell go run tools/build/build.go version) + @echo $(shell go run tools/build.go version) build: - go run tools/build/build.go build + go run tools/build.go build release: - go run tools/build/build.go release + go run tools/build.go release clean: - go run tools/clean/clean.go + go run tools/build.go clean Index: README.md ================================================================== --- README.md +++ README.md @@ -11,16 +11,16 @@ To get an initial impression, take a look at the [manual](https://zettelstore.de/manual/). It is a live example of the zettelstore software, running in read-only mode. -[Zettelstore Client](https://t73f.de/r/zsc) provides client software to access -Zettelstore via its API more easily, [Zettelstore +[Zettelstore Client](https://zettelstore.de/client) provides client +software to access Zettelstore via its API more easily, [Zettelstore Contrib](https://zettelstore.de/contrib) contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. 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://mastodon.social/tags/Zettelstore) … +[Stay tuned](https://twitter.com/zettelstore) … Index: VERSION ================================================================== --- VERSION +++ VERSION @@ -1,1 +1,1 @@ -0.18.0-dev +0.8.0 Index: ast/ast.go ================================================================== --- ast/ast.go +++ ast/ast.go @@ -1,34 +1,31 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package ast provides the abstract syntax tree for parsed zettel content. package ast import ( "net/url" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // ZettelNode is the root node of the abstract syntax tree. // It is *not* part of the visitor pattern. type ZettelNode struct { Meta *meta.Meta // Original metadata - Content zettel.Content // Original content + Content domain.Content // Original content Zid id.Zid // Zettel identification. InhMeta *meta.Meta // Metadata of the zettel, with inherited values. Ast BlockSlice // Zettel abstract syntax tree is a sequence of block nodes. Syntax string // Syntax / parser that produced the Ast } Index: ast/block.go ================================================================== --- ast/block.go +++ ast/block.go @@ -1,21 +1,18 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast -import "t73f.de/r/zsc/attrs" +import "zettelstore.de/c/attrs" // Definition of Block nodes. // BlockSlice is a slice of BlockNodes. type BlockSlice []BlockNode @@ -90,10 +87,15 @@ func (*VerbatimNode) blockNode() { /* Just a marker */ } func (*VerbatimNode) itemNode() { /* Just a marker */ } // WalkChildren does nothing. func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ } + +// Supported syntax values for VerbatimEval. +const ( + VerbatimEvalSyntaxDraw = "draw" +) //-------------------------------------------------------------------------- // RegionNode encapsulates a region of block nodes. type RegionNode struct { @@ -282,14 +284,14 @@ //-------------------------------------------------------------------------- // BLOBNode contains just binary data that must be interpreted according to // a syntax. type BLOBNode struct { - Description InlineSlice - Syntax string - Blob []byte + Title string + Syntax string + Blob []byte } func (*BLOBNode) blockNode() { /* Just a marker */ } // WalkChildren does nothing. func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ } Index: ast/inline.go ================================================================== --- ast/inline.go +++ ast/inline.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" - "t73f.de/r/zsc/attrs" + "zettelstore.de/c/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. @@ -196,19 +193,18 @@ type FormatKind int // Constants for FormatCode const ( _ FormatKind = iota - FormatEmph // Emphasized text - FormatStrong // Strongly emphasized text - FormatInsert // Inserted text - FormatDelete // Deleted text - FormatSuper // Superscripted text - FormatSub // SubscriptedText - FormatQuote // Quoted text - FormatMark // Marked text - FormatSpan // Generic inline container + FormatEmph // Emphasized text. + FormatStrong // Strongly emphasized text. + FormatInsert // Inserted text. + FormatDelete // Deleted text. + FormatSuper // Superscripted text. + FormatSub // SubscriptedText. + FormatQuote // Quoted text. + FormatSpan // Generic inline container. ) func (*FormatNode) inlineNode() { /* Just a marker */ } // WalkChildren walks to the formatted text. Index: ast/ref.go ================================================================== --- ast/ref.go +++ ast/ref.go @@ -1,34 +1,30 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast import ( "net/url" "strings" - "t73f.de/r/zsc/api" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // QueryPrefix is the prefix that denotes a query expression. -const QueryPrefix = api.QueryPrefix +const QueryPrefix = "query:" // ParseReference parses a string and returns a reference. func ParseReference(s string) *Reference { - if invalidReference(s) { + if s == "" || s == "00000000000000" { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } if strings.HasPrefix(s, QueryPrefix) { return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery} } @@ -43,11 +39,11 @@ } u, err := url.Parse(s) if err != nil { return &Reference{URL: nil, Value: s, State: RefStateInvalid} } - if !externalURL(u) { + if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil { if _, err = id.Parse(u.Path); err == nil { return &Reference{URL: u, Value: s, State: RefStateZettel} } if u.Path == "" && u.Fragment != "" { return &Reference{URL: u, Value: s, State: RefStateSelf} @@ -54,15 +50,10 @@ } } return &Reference{URL: u, Value: s, State: RefStateExternal} } -func invalidReference(s string) bool { return s == "" || s == "00000000000000" } -func externalURL(u *url.URL) bool { - return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil -} - func localState(path string) (RefState, bool) { if len(path) > 0 && path[0] == '/' { if len(path) > 1 && path[1] == '/' { return RefStateBased, true } Index: ast/ref_test.go ================================================================== --- ast/ref_test.go +++ ast/ref_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( Index: ast/walk.go ================================================================== --- ast/walk.go +++ ast/walk.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast // Visitor is a visitor for walking the AST. Index: ast/walk_test.go ================================================================== --- ast/walk_test.go +++ ast/walk_test.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package ast_test import ( "testing" - "t73f.de/r/zsc/attrs" + "zettelstore.de/c/attrs" "zettelstore.de/z/ast" ) func BenchmarkWalk(b *testing.B) { root := ast.BlockSlice{ @@ -62,13 +59,13 @@ }, ), } v := benchVisitor{} b.ResetTimer() - for range b.N { + for n := 0; n < b.N; n++ { ast.Walk(&v, &root) } } type benchVisitor struct{} func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv } Index: auth/auth.go ================================================================== --- auth/auth.go +++ auth/auth.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package auth provides services for authentification / authorization. package auth @@ -17,12 +14,12 @@ import ( "time" "zettelstore.de/z/box" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // BaseManager allows to check some base auth modes. type BaseManager interface { // IsReadonly returns true, if the systems is configured to run in read-only-mode. @@ -43,12 +40,12 @@ type TokenKind int // Allowed values of token kind const ( _ TokenKind = iota - KindAPI - KindwebUI + KindJSON + KindHTML ) // TokenData contains some important elements from a token. type TokenData struct { Token []byte Index: auth/cred/cred.go ================================================================== --- auth/cred/cred.go +++ auth/cred/cred.go @@ -1,26 +1,23 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cred provides some function for handling credentials. package cred import ( "bytes" "golang.org/x/crypto/bcrypt" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // HashCredential returns a hashed vesion of the given credential func HashCredential(zid id.Zid, ident, credential string) (string, error) { fullCredential := createFullCredential(zid, ident, credential) @@ -45,12 +42,12 @@ return false, err } func createFullCredential(zid id.Zid, ident, credential string) []byte { var buf bytes.Buffer - buf.Write(zid.Bytes()) + buf.WriteString(zid.String()) buf.WriteByte(' ') buf.WriteString(ident) buf.WriteByte(' ') buf.WriteString(credential) return buf.Bytes() } DELETED auth/impl/digest.go Index: auth/impl/digest.go ================================================================== --- auth/impl/digest.go +++ auth/impl/digest.go @@ -1,89 +0,0 @@ -//----------------------------------------------------------------------------- -// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2023-present Detlef Stern -//----------------------------------------------------------------------------- - -package impl - -import ( - "bytes" - "crypto" - "crypto/hmac" - "encoding/base64" - - "t73f.de/r/sx" - "t73f.de/r/sx/sxreader" -) - -var encoding = base64.RawURLEncoding - -const digestAlg = crypto.SHA384 - -func sign(claim sx.Object, secret []byte) ([]byte, error) { - var buf bytes.Buffer - _, err := sx.Print(&buf, claim) - if err != nil { - return nil, err - } - 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 @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package impl provides services for authentification / authorization. package impl @@ -18,20 +15,20 @@ "errors" "hash/fnv" "io" "time" - "t73f.de/r/sx" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/sexp" + "github.com/pascaldekloe/jwt" + + "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) type myAuth struct { readonly bool owner id.Zid @@ -68,12 +65,11 @@ } // IsReadonly returns true, if the systems is configured to run in read-only-mode. func (a *myAuth) IsReadonly() bool { return a.readonly } -// ErrMalformedToken signals a broken token. -var ErrMalformedToken = errors.New("auth: malformed token") +const reqHash = jwt.HS512 // 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. @@ -88,66 +84,68 @@ if !ok || subject == "" { return nil, ErrNoIdent } now := time.Now().Round(time.Second) - 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) + 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 } // 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(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).GetValue() +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 if ident == "" { - 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 = string(ident) - tokenData.Issued = issued - tokenData.Now = now - tokenData.Expires = expires - tokenData.Zid = zid - return nil + 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 } func (a *myAuth) Owner() id.Zid { return a.owner } func (a *myAuth) IsOwner(zid id.Zid) bool { Index: auth/policy/anon.go ================================================================== --- auth/policy/anon.go +++ auth/policy/anon.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type anonPolicy struct { authConfig config.AuthConfig pre auth.Policy Index: auth/policy/box.go ================================================================== --- auth/policy/box.go +++ auth/policy/box.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( @@ -17,19 +14,23 @@ "context" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // BoxWithPolicy wraps the given box inside a policy box. -func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) { +func BoxWithPolicy( + manager auth.AuthzManager, + box box.Box, + authConfig config.AuthConfig, +) (box.Box, auth.Policy) { pol := newPolicy(manager, authConfig) return newBox(box, pol), pol } // polBox implements a policy box. @@ -52,36 +53,32 @@ func (pp *polBox) CanCreateZettel(ctx context.Context) bool { return pp.box.CanCreateZettel(ctx) } -func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { user := server.GetUser(ctx) if pp.policy.CanCreate(user, zettel.Meta) { return pp.box.CreateZettel(ctx, zettel) } return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid) } -func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { - z, err := pp.box.GetZettel(ctx, zid) +func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { + zettel, err := pp.box.GetZettel(ctx, zid) if err != nil { - return zettel.Zettel{}, err - } - user := server.GetUser(ctx) - if pp.policy.CanRead(user, z.Meta) { - return z, nil - } - return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) -} - -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) + return domain.Zettel{}, err + } + user := server.GetUser(ctx) + if pp.policy.CanRead(user, zettel.Meta) { + return zettel, nil + } + return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid) +} + +func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { + return pp.box.GetAllZettel(ctx, zid) } func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) { m, err := pp.box.GetMeta(ctx, zid) if err != nil { @@ -92,33 +89,41 @@ return m, nil } return nil, box.NewErrNotAllowed("GetMeta", user, zid) } -func (pp *polBox) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) { +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) { 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, metaSeq, q) + return pp.box.SelectMeta(ctx, q) } -func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { +func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { return pp.box.CanUpdateZettel(ctx, zettel) } -func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { +func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { zid := zettel.Meta.Zid user := server.GetUser(ctx) if !zid.IsValid() { - return box.ErrInvalidZid{Zid: zid.String()} + return &box.ErrInvalidID{Zid: zid} } // Write existing zettel - oldZettel, err := pp.box.GetZettel(ctx, zid) + oldMeta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } - if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) { + if pp.policy.CanWrite(user, oldMeta, zettel.Meta) { return pp.box.UpdateZettel(ctx, zettel) } return box.NewErrNotAllowed("Write", user, zid) } @@ -125,16 +130,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 { - z, err := pp.box.GetZettel(ctx, curZid) + meta, err := pp.box.GetMeta(ctx, curZid) if err != nil { return err } user := server.GetUser(ctx) - if pp.policy.CanRename(user, z.Meta) { + if pp.policy.CanRename(user, meta) { return pp.box.RenameZettel(ctx, curZid, newZid) } return box.NewErrNotAllowed("Rename", user, curZid) } @@ -141,16 +146,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 { - z, err := pp.box.GetZettel(ctx, zid) + meta, err := pp.box.GetMeta(ctx, zid) if err != nil { return err } user := server.GetUser(ctx) - if pp.policy.CanDelete(user, z.Meta) { + if pp.policy.CanDelete(user, meta) { return pp.box.DeleteZettel(ctx, zid) } return box.NewErrNotAllowed("Delete", user, zid) } @@ -159,13 +164,5 @@ if pp.policy.CanRefresh(user) { return pp.box.Refresh(ctx) } return box.NewErrNotAllowed("Refresh", user, id.Invalid) } -func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error { - user := server.GetUser(ctx) - if pp.policy.CanRefresh(user) { - // If a user is allowed to refresh all data, it it also allowed to re-index a zettel. - return pp.box.ReIndex(ctx, zid) - } - return box.NewErrNotAllowed("ReIndex", user, zid) -} Index: auth/policy/default.go ================================================================== --- auth/policy/default.go +++ auth/policy/default.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/auth" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type defaultPolicy struct { manager auth.AuthzManager } Index: auth/policy/owner.go ================================================================== --- auth/policy/owner.go +++ auth/policy/owner.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) type ownerPolicy struct { manager auth.AuthzManager authConfig config.AuthConfig Index: auth/policy/policy.go ================================================================== --- auth/policy/policy.go +++ auth/policy/policy.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package policy provides some interfaces and implementation for authorizsation policies. package policy import ( "zettelstore.de/z/auth" "zettelstore.de/z/config" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) // newPolicy creates a policy based on given constraints. func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy { var pol auth.Policy Index: auth/policy/policy_test.go ================================================================== --- auth/policy/policy_test.go +++ auth/policy/policy_test.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy import ( "fmt" "testing" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/auth" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) func TestPolicies(t *testing.T) { t.Parallel() testScene := []struct { Index: auth/policy/readonly.go ================================================================== --- auth/policy/readonly.go +++ auth/policy/readonly.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package policy -import "zettelstore.de/z/zettel/meta" +import "zettelstore.de/z/domain/meta" type roPolicy struct{} func (*roPolicy) CanCreate(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRead(_, _ *meta.Meta) bool { return true } func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false } func (*roPolicy) CanRename(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanDelete(_, _ *meta.Meta) bool { return false } func (*roPolicy) CanRefresh(user *meta.Meta) bool { return user != nil } Index: box/box.go ================================================================== --- box/box.go +++ box/box.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package box provides a generic interface to zettel boxes. package box @@ -17,27 +14,45 @@ import ( "context" "errors" "fmt" "io" + "net/url" + "strconv" "time" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // BaseBox is implemented by all Zettel boxes. type BaseBox interface { // Location returns some information where the box is located. // Format is dependent of the box. Location() string + // CanCreateZettel returns true, if box could possibly create a new zettel. + CanCreateZettel(ctx context.Context) bool + + // CreateZettel creates a new zettel. + // Returns the new zettel id (and an error indication). + CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) + // GetZettel retrieves a specific zettel. - GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) + GetZettel(ctx context.Context, zid id.Zid) (domain.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 domain.Zettel) bool + + // UpdateZettel updates an existing zettel. + UpdateZettel(ctx context.Context, zettel domain.Zettel) error // AllowRenameZettel returns true, if box will not disallow renaming the zettel. AllowRenameZettel(ctx context.Context, zid id.Zid) bool // RenameZettel changes the current Zid to a new Zid. @@ -48,26 +63,10 @@ // DeleteZettel removes the zettel from the box. DeleteZettel(ctx context.Context, zid id.Zid) error } -// WriteBox is a box that can create / update zettel content. -type WriteBox interface { - // CanCreateZettel returns true, if box could possibly create a new zettel. - CanCreateZettel(ctx context.Context) bool - - // CreateZettel creates a new zettel. - // Returns the new zettel id (and an error indication). - CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, 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 -} - // ZidFunc is a function that processes identifier of a zettel. type ZidFunc func(id.Zid) // MetaFunc is a function that processes metadata of a zettel. type MetaFunc func(*meta.Meta) @@ -74,13 +73,10 @@ // 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. ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error @@ -96,32 +92,14 @@ // Zettel is the number of zettel managed by the box. Zettel int } -// StartState enumerates the possible states of starting and stopping a box. -// -// StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped. -// -// Other transitions are also possible. -type StartState uint8 - -// Constant values of StartState -const ( - StartStateStopped StartState = iota - StartStateStarting - StartStateStarted - StartStateStopping -) - // StartStopper performs simple lifecycle management. type StartStopper interface { - // State the current status of the box. - State() StartState - // Start the box. Now all other functions of the box are allowed. - // Starting a box, which is not in state StartStateStopped is not allowed. + // Starting an already started box is not allowed. Start(ctx context.Context) error // Stop the started box. Now only the Start() function is allowed. Stop(ctx context.Context) } @@ -133,30 +111,25 @@ } // Box is to be used outside the box package and its descendants. type Box interface { BaseBox - WriteBox // 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. - // 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) + SelectMeta(ctx context.Context, 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) + GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.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 - - // ReIndex one zettel to update its index data. - ReIndex(context.Context, id.Zid) error } // Stats record stattistics about a box. type Stats struct { // ReadOnly indicates that boxes cannot be modified. @@ -208,18 +181,17 @@ type UpdateReason uint8 // Values for Reason const ( _ UpdateReason = iota - OnReady // Box is started and fully operational OnReload // Box was reloaded OnZettel // Something with a zettel happened ) // UpdateInfo contains all the data about a changed zettel. type UpdateInfo struct { - Box BaseBox + Box Box Reason UpdateReason Zid id.Zid } // UpdateFunc is a function to be called when a change is detected. @@ -254,18 +226,10 @@ func DoNotEnrich(ctx context.Context) bool { _, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType) return ok } -// NoEnrichQuery provides a context that signals not to enrich, if the query does not need this. -func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context { - if q.EnrichNeeded() { - return ctx - } - return NoEnrichContext(ctx) -} - // ErrNotAllowed is returned if the caller is not allowed to perform the operation. type ErrNotAllowed struct { Op string User *meta.Meta Zid id.Zid @@ -309,21 +273,44 @@ var ErrStopped = errors.New("box is stopped") // ErrReadOnly is returned if there is an attepmt to write to a read-only box. var ErrReadOnly = errors.New("read-only box") -// ErrZettelNotFound is returned if a zettel was not found in the box. -type ErrZettelNotFound struct{ Zid id.Zid } - -func (eznf ErrZettelNotFound) Error() string { return "zettel not found: " + eznf.Zid.String() } +// ErrNotFound is returned if a zettel was not found in the box. +var ErrNotFound = errors.New("zettel not found") // ErrConflict is returned if a box operation detected a conflict.. // One example: if calculating a new zettel identifier takes too long. var ErrConflict = errors.New("conflict") // ErrCapacity is returned if a box has reached its capacity. var ErrCapacity = errors.New("capacity exceeded") -// ErrInvalidZid is returned if the zettel id is not appropriate for the box operation. -type ErrInvalidZid struct{ Zid string } +// ErrInvalidID is returned if the zettel id is not appropriate for the box operation. +type ErrInvalidID struct{ Zid id.Zid } + +func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() } + +// GetQueryBool is a helper function to extract bool values from a box URI. +func GetQueryBool(u *url.URL, key string) bool { + _, ok := u.Query()[key] + return ok +} -func (err ErrInvalidZid) Error() string { return "invalid Zettel id: " + err.Zid } +// GetQueryInt is a helper function to extract int values of a specified range from a box URI. +func GetQueryInt(u *url.URL, key string, min, def, max int) int { + sVal := u.Query().Get(key) + if sVal == "" { + return def + } + iVal, err := strconv.Atoi(sVal) + if err != nil { + return def + } + if iVal < min { + return min + } + if iVal > max { + return max + } + return iVal +} Index: box/compbox/compbox.go ================================================================== --- box/compbox/compbox.go +++ box/compbox/compbox.go @@ -1,34 +1,31 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "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( " comp", @@ -50,11 +47,10 @@ }{ id.MustParse(api.ZidVersion): {genVersionBuildM, genVersionBuildC}, id.MustParse(api.ZidHost): {genVersionHostM, genVersionHostC}, id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC}, id.MustParse(api.ZidLog): {genLogM, genLogC}, - id.MustParse(api.ZidMemory): {genMemoryM, genMemoryC}, id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC}, id.MustParse(api.ZidMetadataKey): {genKeysM, genKeysC}, id.MustParse(api.ZidParser): {genParserM, genParserC}, id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC}, } @@ -72,37 +68,52 @@ // Setup remembers important values. func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() } func (*compBox) Location() string { return "" } -func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (*compBox) CanCreateZettel(context.Context) bool { return false } + +func (cb *compBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { + cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") + return id.Invalid, box.ErrReadOnly +} + +func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (domain.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("GetZettel/Content") - return zettel.Zettel{ + cb.log.Trace().Msg("GetMeta/Content") + return domain.Zettel{ Meta: m, - Content: zettel.NewContent(genContent(m)), + Content: domain.NewContent(genContent(m)), }, nil } - cb.log.Trace().Msg("GetZettel/NoContent") - return zettel.Zettel{Meta: m}, nil + cb.log.Trace().Msg("GetMeta/NoContent") + return domain.Zettel{Meta: m}, nil + } + } + cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err") + return domain.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 + } } } - err := box.ErrZettelNotFound{Zid: zid} - cb.log.Trace().Err(err).Msg("GetZettel/Err") - return zettel.Zettel{}, err -} - -func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool { - _, found := myZettel[zid] - return found + cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err") + return nil, box.ErrNotFound } func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { - cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid") + cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta") for zid, gen := range myZettel { if !constraint(zid) { continue } if genMeta := gen.meta; genMeta != nil { @@ -128,33 +139,38 @@ } } } return nil } + +func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } + +func (cb *compBox) UpdateZettel(context.Context, domain.Zettel) error { + cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") + return box.ErrReadOnly +} func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := myZettel[zid] return !ok } -func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { +func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { + err := box.ErrNotFound if _, ok := myZettel[curZid]; ok { err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: curZid} } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } -func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { +func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) error { + err := box.ErrNotFound if _, ok := myZettel[zid]; ok { err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } @@ -164,14 +180,14 @@ cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } func updateMeta(m *meta.Meta) { if _, ok := m.Get(api.KeySyntax); !ok { - m.Set(api.KeySyntax, meta.SyntaxZmk) + m.Set(api.KeySyntax, api.ValueSyntaxZmk) } m.Set(api.KeyRole, api.ValueRoleConfiguration) m.Set(api.KeyLang, api.ValueLangEN) m.Set(api.KeyReadOnly, api.ValueTrue) if _, ok := m.Get(api.KeyVisibility); !ok { m.Set(api.KeyVisibility, api.ValueVisibilityExpert) } } Index: box/compbox/config.go ================================================================== --- box/compbox/config.go +++ box/compbox/config.go @@ -1,27 +1,24 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genConfigZettelM(zid id.Zid) *meta.Meta { if myConfig == nil { return nil Index: box/compbox/keys.go ================================================================== --- box/compbox/keys.go +++ box/compbox/keys.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genKeysM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys") @@ -35,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|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty()) + "|%v|%v|%v|%v\n", 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 @@ -1,35 +1,32 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genLogM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Log") - m.Set(api.KeySyntax, meta.SyntaxText) + m.Set(api.KeySyntax, api.ValueSyntaxText) m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout)) + m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout)) return m } func genLogC(*meta.Meta) []byte { const tsFormat = "2006-01-02 15:04:05.999999" Index: box/compbox/manager.go ================================================================== --- box/compbox/manager.go +++ box/compbox/manager.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( "bytes" "fmt" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genManagerM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Box Manager") DELETED box/compbox/memory.go Index: box/compbox/memory.go ================================================================== --- box/compbox/memory.go +++ box/compbox/memory.go @@ -1,60 +0,0 @@ -//----------------------------------------------------------------------------- -// Copyright (c) 2024-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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2024-present Detlef Stern -//----------------------------------------------------------------------------- - -package compbox - -import ( - "bytes" - "fmt" - "os" - "runtime" - - "t73f.de/r/zsc/api" - "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -func genMemoryM(zid id.Zid) *meta.Meta { - if myConfig == nil { - return nil - } - m := meta.New(zid) - m.Set(api.KeyTitle, "Zettelstore Memory") - m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string)) - m.Set(api.KeyVisibility, api.ValueVisibilityExpert) - return m -} - -func genMemoryC(*meta.Meta) []byte { - pageSize := os.Getpagesize() - var m runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&m) - - var buf bytes.Buffer - buf.WriteString("|=Name|=Value>\n") - fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize) - fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize)) - fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects) - fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024) - fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024) - debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool) - if debug { - for i, bysize := range m.BySize { - fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d → %d\n", - i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees) - } - } - return buf.Bytes() -} Index: box/compbox/parser.go ================================================================== --- box/compbox/parser.go +++ box/compbox/parser.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( @@ -17,15 +14,15 @@ "bytes" "fmt" "sort" "strings" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func genParserM(zid id.Zid) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, "Zettelstore Supported Parser") @@ -34,11 +31,11 @@ return m } func genParserC(*meta.Meta) []byte { var buf bytes.Buffer - buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n") + buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Image Format?:\n") syntaxes := parser.GetSyntaxes() sort.Strings(syntaxes) for _, syntax := range syntaxes { info := parser.Get(syntax) if info.Name != syntax { @@ -45,10 +42,10 @@ continue } altNames := info.AltNames sort.Strings(altNames) fmt.Fprintf( - &buf, "|%v|%v|%v|%v|%v\n", - syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat) + &buf, "|%v|%v|%v|%v\n", + syntax, strings.Join(altNames, ", "), info.IsTextParser, info.IsImageFormat) } return buf.Bytes() } Index: box/compbox/version.go ================================================================== --- box/compbox/version.go +++ box/compbox/version.go @@ -1,25 +1,22 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package compbox import ( - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func getVersionMeta(zid id.Zid, title string) *meta.Meta { m := meta.New(zid) m.Set(api.KeyTitle, title) Index: box/constbox/base.css ================================================================== --- box/constbox/base.css +++ box/constbox/base.css @@ -1,19 +1,5 @@ -/*----------------------------------------------------------------------------- - * 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. - * - * SPDX-License-Identifier: EUPL-1.2 - * SPDX-FileCopyrightText: 2020-present Detlef Stern - *----------------------------------------------------------------------------- - */ - *,*::before,*::after { box-sizing: border-box; } html { font-size: 1rem; @@ -95,11 +81,10 @@ h3 { font-size:1.15rem; margin:.75rem 0 } h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold } h5 { font-size:1.05rem; margin:.8rem 0 } h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter } p { margin: .5rem 0 0 0 } - p.zs-meta-zettel { margin-top: .5rem; margin-left: 0.5rem } li,figure,figcaption,dl { margin: 0 } dt { margin: .5rem 0 0 0 } dt+dd { margin-top: 0 } dd { margin: .5rem 0 0 2rem } dd > p:first-child { margin: 0 0 0 0 } @@ -151,17 +136,12 @@ border-bottom:1px solid #ccc; width:100%; } input.zs-primary { float:right } input.zs-secondary { float:left } - input.zs-upload { - padding-left: 1em; - padding-right: 1em; - } a:not([class]) { text-decoration-skip-ink: auto } a.broken { text-decoration: line-through } - a.external::after { content: "➚"; display: inline-block } img { max-width: 100% } img.right { float: right } ol.zs-endnotes { padding-top: .5rem; border-top: 1px solid; ADDED box/constbox/base.mustache Index: box/constbox/base.mustache ================================================================== --- box/constbox/base.mustache +++ box/constbox/base.mustache @@ -0,0 +1,66 @@ + + + + + + + +{{{MetaHeader}}} + + +{{#CSSRoleURL}}{{/CSSRoleURL}} +{{Title}} + + + +
+{{{Content}}} +
+{{#FooterHTML}}{{/FooterHTML}} +{{#DebugMode}}
WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!
{{/DebugMode}} + + DELETED box/constbox/base.sxn Index: box/constbox/base.sxn ================================================================== --- box/constbox/base.sxn +++ box/constbox/base.sxn @@ -1,63 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(@@@@ -(html ,@(if lang `((@ (lang ,lang)))) -(head - (meta (@ (charset "utf-8"))) - (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0"))) - (meta (@ (name "generator") (content "Zettelstore"))) - (meta (@ (name "format-detection") (content "telephone=no"))) - ,@META-HEADER - (link (@ (rel "stylesheet") (href ,css-base-url))) - (link (@ (rel "stylesheet") (href ,css-user-url))) - ,@(ROLE-DEFAULT-meta (current-binding)) - (title ,title)) -(body - (nav (@ (class "zs-menu")) - (a (@ (href ,home-url)) "Home") - ,@(if with-auth - `((div (@ (class "zs-dropdown")) - (button "User") - (nav (@ (class "zs-dropdown-content")) - ,@(if user-is-valid - `((a (@ (href ,user-zettel-url)) ,user-ident) - (a (@ (href ,logout-url)) "Logout")) - `((a (@ (href ,login-url)) "Login")) - ) - ))) - ) - (div (@ (class "zs-dropdown")) - (button "Lists") - (nav (@ (class "zs-dropdown-content")) - (a (@ (href ,list-zettel-url)) "List Zettel") - (a (@ (href ,list-roles-url)) "List Roles") - (a (@ (href ,list-tags-url)) "List Tags") - ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh"))) - )) - ,@(if new-zettel-links - `((div (@ (class "zs-dropdown")) - (button "New") - (nav (@ (class "zs-dropdown-content")) - ,@(map wui-link new-zettel-links) - ))) - ) - (search (form (@ (action ,search-url)) - (input (@ (type "search") (inputmode "search") (name ,query-key-query) - (title "General search field, with same behaviour as search field in search result list") - (placeholder "Search..") (dir "auto"))))) - ) - (main (@ (class "content")) ,DETAIL) - ,@(if FOOTER `((footer (hr) ,@FOOTER))) - ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!")))) -))) Index: box/constbox/constbox.go ================================================================== --- box/constbox/constbox.go +++ box/constbox/constbox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package constbox puts zettel inside the executable. package constbox @@ -17,19 +14,19 @@ import ( "context" _ "embed" // Allow to embed file content "net/url" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "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( " const", @@ -46,11 +43,11 @@ type constHeader map[string]string type constZettel struct { header constHeader - content zettel.Content + content domain.Content } type constBox struct { log *logger.Logger number int @@ -58,23 +55,33 @@ enricher box.Enricher } func (*constBox) Location() string { return "const:" } -func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (*constBox) CanCreateZettel(context.Context) bool { return false } + +func (cb *constBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { + cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel") + return id.Invalid, box.ErrReadOnly +} + +func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { if z, ok := cb.zettel[zid]; ok { cb.log.Trace().Msg("GetZettel") - return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil - } - err := box.ErrZettelNotFound{Zid: zid} - cb.log.Trace().Err(err).Msg("GetZettel/Err") - return zettel.Zettel{}, err -} - -func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool { - _, found := cb.zettel[zid] - return found + return domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil + } + cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel") + return domain.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) 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 { @@ -94,33 +101,38 @@ handle(m) } } return nil } + +func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } + +func (cb *constBox) UpdateZettel(context.Context, domain.Zettel) error { + cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel") + return box.ErrReadOnly +} func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { _, ok := cb.zettel[zid] return !ok } -func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) { +func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error { + err := box.ErrNotFound if _, ok := cb.zettel[curZid]; ok { err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: curZid} } cb.log.Trace().Err(err).Msg("RenameZettel") return err } func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false } -func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) { +func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) error { + err := box.ErrNotFound if _, ok := cb.zettel[zid]; ok { err = box.ErrReadOnly - } else { - err = box.ErrZettelNotFound{Zid: zid} } cb.log.Trace().Err(err).Msg("DeleteZettel") return err } @@ -127,314 +139,225 @@ func (cb *constBox) ReadStats(st *box.ManagedBoxStats) { st.ReadOnly = true st.Zettel = len(cb.zettel) cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats") } + +const syntaxTemplate = "mustache" var constZettelMap = map[id.Zid]constZettel{ id.ConfigurationZid: { constHeader{ api.KeyTitle: "Zettelstore Runtime Configuration", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxNone, + api.KeySyntax: api.ValueSyntaxNone, api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityOwner, }, - zettel.NewContent(nil)}, + domain.NewContent(nil)}, id.MustParse(api.ZidLicense): { constHeader{ api.KeyTitle: "Zettelstore License", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxText, + api.KeySyntax: api.ValueSyntaxText, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyModified: "20220131153422", api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityPublic, }, - zettel.NewContent(contentLicense)}, + domain.NewContent(contentLicense)}, id.MustParse(api.ZidAuthors): { constHeader{ api.KeyTitle: "Zettelstore Contributors", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, + api.KeySyntax: api.ValueSyntaxZmk, api.KeyCreated: "20210504135842", api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, api.KeyVisibility: api.ValueVisibilityLogin, }, - zettel.NewContent(contentContributors)}, + domain.NewContent(contentContributors)}, id.MustParse(api.ZidDependencies): { constHeader{ api.KeyTitle: "Zettelstore Dependencies", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, + api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityPublic, + api.KeyVisibility: api.ValueVisibilityLogin, api.KeyCreated: "20210504135842", - api.KeyModified: "20240418095500", + api.KeyModified: "20221013105100", }, - zettel.NewContent(contentDependencies)}, + domain.NewContent(contentDependencies)}, id.BaseTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Base HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230510155100", - api.KeyModified: "20240219145300", + api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210504135842", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentBaseSxn)}, + domain.NewContent(contentBaseMustache)}, id.LoginTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Login Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, + api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentLoginSxn)}, + domain.NewContent(contentLoginMustache)}, id.ZettelTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230510155300", - api.KeyModified: "20240219145100", + api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentZettelSxn)}, + domain.NewContent(contentZettelMustache)}, id.InfoTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Info HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, + api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + domain.NewContent(contentInfoMustache)}, + id.ContextTemplateZid: { + constHeader{ + api.KeyTitle: "Zettelstore Context HTML Template", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210218181140", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentInfoSxn)}, + domain.NewContent(contentContextMustache)}, id.FormTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, + api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentFormSxn)}, + domain.NewContent(contentFormMustache)}, id.RenameTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Rename Form HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, + api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentRenameSxn)}, + domain.NewContent(contentRenameMustache)}, id.DeleteTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Delete HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, + api.KeySyntax: syntaxTemplate, api.KeyCreated: "20200804111624", - api.KeyModified: "20240219145200", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentDeleteSxn)}, + domain.NewContent(contentDeleteMustache)}, id.ListTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore List Zettel HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230704122100", - api.KeyModified: "20240219145200", + api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20200804111624", api.KeyVisibility: api.ValueVisibilityExpert, }, - zettel.NewContent(contentListZettelSxn)}, + domain.NewContent(contentListZettelMustache)}, id.ErrorTemplateZid: { constHeader{ api.KeyTitle: "Zettelstore Error HTML Template", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20210305133215", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, - }, - zettel.NewContent(contentErrorSxn)}, - id.StartSxnZid: { - constHeader{ - api.KeyTitle: "Zettelstore Sxn Start Code", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230824160700", - api.KeyModified: "20240219145200", - api.KeyVisibility: api.ValueVisibilityExpert, - api.KeyPrecursor: string(api.ZidSxnBase), - }, - zettel.NewContent(contentStartCodeSxn)}, - id.BaseSxnZid: { - constHeader{ - api.KeyTitle: "Zettelstore Sxn Base Code", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20230619132800", - api.KeyModified: "20240219144600", - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, - api.KeyPrecursor: string(api.ZidSxnPrelude), - }, - zettel.NewContent(contentBaseCodeSxn)}, - id.PreludeSxnZid: { - constHeader{ - api.KeyTitle: "Zettelstore Sxn Prelude", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxSxn, - api.KeyCreated: "20231006181700", - api.KeyModified: "20240222121200", - api.KeyReadOnly: api.ValueTrue, - api.KeyVisibility: api.ValueVisibilityExpert, - }, - zettel.NewContent(contentPreludeSxn)}, + api.KeySyntax: syntaxTemplate, + api.KeyCreated: "20210305133215", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + domain.NewContent(contentErrorMustache)}, id.MustParse(api.ZidBaseCSS): { constHeader{ api.KeyTitle: "Zettelstore Base CSS", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxCSS, + api.KeySyntax: "css", api.KeyCreated: "20200804111624", - api.KeyModified: "20231129112800", api.KeyVisibility: api.ValueVisibilityPublic, }, - zettel.NewContent(contentBaseCSS)}, + domain.NewContent(contentBaseCSS)}, id.MustParse(api.ZidUserCSS): { constHeader{ api.KeyTitle: "Zettelstore User CSS", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxCSS, + api.KeySyntax: "css", api.KeyCreated: "20210622110143", api.KeyVisibility: api.ValueVisibilityPublic, }, - zettel.NewContent([]byte("/* User-defined CSS */"))}, + domain.NewContent([]byte("/* User-defined CSS */"))}, + id.RoleCSSMapZid: { + constHeader{ + api.KeyTitle: "Zettelstore Role to CSS Map", + api.KeyRole: api.ValueRoleConfiguration, + api.KeySyntax: api.ValueSyntaxNone, + api.KeyCreated: "20220321183214", + api.KeyVisibility: api.ValueVisibilityExpert, + }, + domain.NewContent(nil)}, id.EmojiZid: { constHeader{ api.KeyTitle: "Zettelstore Generic Emoji", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxGif, + api.KeySyntax: api.ValueSyntaxGif, api.KeyReadOnly: api.ValueTrue, api.KeyCreated: "20210504175807", api.KeyVisibility: api.ValueVisibilityPublic, }, - zettel.NewContent(contentEmoji)}, + domain.NewContent(contentEmoji)}, id.TOCNewTemplateZid: { constHeader{ api.KeyTitle: "New Menu", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, + api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210217161829", - api.KeyModified: "20231129111800", api.KeyVisibility: api.ValueVisibilityCreator, }, - zettel.NewContent(contentNewTOCZettel)}, + domain.NewContent(contentNewTOCZettel)}, id.MustParse(api.ZidTemplateNewZettel): { constHeader{ - api.KeyTitle: "New Zettel", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20201028185209", - api.KeyModified: "20230929132900", - meta.NewPrefix + api.KeyRole: api.ValueRoleZettel, - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewRole): { - constHeader{ - api.KeyTitle: "New Role", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129110800", - meta.NewPrefix + api.KeyRole: api.ValueRoleRole, - meta.NewPrefix + api.KeyTitle: "", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidTemplateNewTag): { - constHeader{ - api.KeyTitle: "New Tag", - api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20230929132400", - meta.NewPrefix + api.KeyRole: api.ValueRoleTag, - meta.NewPrefix + api.KeyTitle: "#", - api.KeyVisibility: api.ValueVisibilityCreator, - }, - zettel.NewContent(nil)}, + api.KeyTitle: "New Zettel", + api.KeyRole: api.ValueRoleZettel, + api.KeySyntax: api.ValueSyntaxZmk, + api.KeyCreated: "20201028185209", + api.KeyVisibility: api.ValueVisibilityCreator, + }, + domain.NewContent(nil)}, id.MustParse(api.ZidTemplateNewUser): { constHeader{ api.KeyTitle: "New User", api.KeyRole: api.ValueRoleConfiguration, - api.KeySyntax: meta.SyntaxNone, + api.KeySyntax: api.ValueSyntaxNone, api.KeyCreated: "20201028185209", meta.NewPrefix + api.KeyCredential: "", meta.NewPrefix + api.KeyUserID: "", meta.NewPrefix + api.KeyUserRole: api.ValueUserRoleReader, api.KeyVisibility: api.ValueVisibilityOwner, }, - zettel.NewContent(nil)}, - id.MustParse(api.ZidRoleZettelZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleZettel, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129161400", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleZettel)}, - id.MustParse(api.ZidRoleConfigurationZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleConfiguration, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162800", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleConfiguration)}, - id.MustParse(api.ZidRoleRoleZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleRole, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162900", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleRole)}, - id.MustParse(api.ZidRoleTagZettel): { - constHeader{ - api.KeyTitle: api.ValueRoleTag, - api.KeyRole: api.ValueRoleRole, - api.KeySyntax: meta.SyntaxZmk, - api.KeyCreated: "20231129162000", - api.KeyLang: api.ValueLangEN, - api.KeyVisibility: api.ValueVisibilityLogin, - }, - zettel.NewContent(contentRoleTag)}, + domain.NewContent(nil)}, id.DefaultHomeZid: { constHeader{ api.KeyTitle: "Home", api.KeyRole: api.ValueRoleZettel, - api.KeySyntax: meta.SyntaxZmk, + api.KeySyntax: api.ValueSyntaxZmk, api.KeyLang: api.ValueLangEN, api.KeyCreated: "20210210190757", }, - zettel.NewContent(contentHomeZettel)}, + domain.NewContent(contentHomeZettel)}, } //go:embed license.txt var contentLicense []byte @@ -442,45 +365,39 @@ var contentContributors []byte //go:embed dependencies.zettel var contentDependencies []byte -//go:embed base.sxn -var contentBaseSxn []byte - -//go:embed login.sxn -var contentLoginSxn []byte - -//go:embed zettel.sxn -var contentZettelSxn []byte - -//go:embed info.sxn -var contentInfoSxn []byte - -//go:embed form.sxn -var contentFormSxn []byte - -//go:embed rename.sxn -var contentRenameSxn []byte - -//go:embed delete.sxn -var contentDeleteSxn []byte - -//go:embed listzettel.sxn -var contentListZettelSxn []byte - -//go:embed error.sxn -var contentErrorSxn []byte - -//go:embed start.sxn -var contentStartCodeSxn []byte - -//go:embed wuicode.sxn -var contentBaseCodeSxn []byte - -//go:embed prelude.sxn -var contentPreludeSxn []byte +//go:embed base.mustache +var contentBaseMustache []byte + +//go:embed login.mustache +var contentLoginMustache []byte + +//go:embed zettel.mustache +var contentZettelMustache []byte + +//go:embed info.mustache +var contentInfoMustache []byte + +//go:embed context.mustache +var contentContextMustache []byte + +//go:embed form.mustache +var contentFormMustache []byte + +//go:embed rename.mustache +var contentRenameMustache []byte + +//go:embed delete.mustache +var contentDeleteMustache []byte + +//go:embed listzettel.mustache +var contentListZettelMustache []byte + +//go:embed error.mustache +var contentErrorMustache []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif @@ -487,19 +404,7 @@ var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte -//go:embed rolezettel.zettel -var contentRoleZettel []byte - -//go:embed roleconfiguration.zettel -var contentRoleConfiguration []byte - -//go:embed rolerole.zettel -var contentRoleRole []byte - -//go:embed roletag.zettel -var contentRoleTag []byte - //go:embed home.zettel var contentHomeZettel []byte ADDED box/constbox/context.mustache Index: box/constbox/context.mustache ================================================================== --- box/constbox/context.mustache +++ box/constbox/context.mustache @@ -0,0 +1,11 @@ +
+

{{Title}}

+
+InfoBackwardBothForward +· Depth:{{#Depths}} {{{Text}}}{{/Depths}} +
+
+{{{Content}}} ADDED box/constbox/delete.mustache Index: box/constbox/delete.mustache ================================================================== --- box/constbox/delete.mustache +++ box/constbox/delete.mustache @@ -0,0 +1,43 @@ +
+
+

Delete Zettel {{Zid}}

+
+

Do you really want to delete this zettel?

+{{#HasShadows}} +
+

Infomation

+

If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.

+
+{{/HasShadows}} +{{#Incoming.Has}} +
+

Warning!

+

If you delete this zettel, incoming references from the following zettel will become invalid.

+
    +{{#Incoming.Links}} +
  • {{Text}}
  • +{{/Incoming.Links}} +
+
+{{/Incoming.Has}} +{{#HasUselessFiles}} +
+

Warning!

+

Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.

+
    +{{#UselessFiles}} +
  • {{{.}}}
  • +{{/UselessFiles}} +
+
+{{/HasUselessFiles}} +
+{{#MetaPairs}} +
{{Key}}:
{{Value}}
+{{/MetaPairs}} +
+
+ +
+
+{{end}} DELETED box/constbox/delete.sxn Index: box/constbox/delete.sxn ================================================================== --- box/constbox/delete.sxn +++ box/constbox/delete.sxn @@ -1,39 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 "Delete Zettel " ,zid)) - (p "Do you really want to delete this zettel?") - ,@(if shadowed-box - `((div (@ (class "zs-info")) - (h2 "Information") - (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.") - )) - ) - ,@(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 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 wui-item useless)) - )) - ) - ,(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 @@ -98,10 +98,89 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 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. ``` + +=== gopikchr +; URL & Source +: [[https://github.com/gopikchr/gopikchr]] +; License +: MIT License +; Remarks +: Author is [[Zellyn Hunter|https://github.com/zellyn]], he wrote a blog post [[gopikchr: a yakshave|https://zellyn.com/2022/01/gopikchr-a-yakshave/]] about his work. +: Gopikchr was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/pikchr''. + Later, the source code was changed to adapt it to the needs of Zettelstore. + For details, read README.txt in the appropriate source code folder. +``` +MIT License + +Copyright (c) 2022 gopikchr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +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. +``` + +=== hoisie/mustache / cbroglie/mustache +; URL & Source +: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]] +; License +: MIT License +; Remarks +: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]). + cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache. + cbroglie/mustache obviously continues with the original license. + +``` +Copyright (c) 2009 Michael Hoisie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +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. +``` + +=== 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 @@ -127,18 +206,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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. ``` - -=== Sx, SxHTML, Zettelstore-Client -These are companion projects, written by the main developer of Zettelstore. -They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. - -; URL & Source Sx -: [[https://t73f.de/r/sx]] -; URL & Source SxHTML -: [[https://t73f.de/r/sxhtml]] -; URL & Source Zettelstore-Client -: [[https://t73f.de/r/zsc]] -; License: -: European Union Public License, version 1.2 (EUPL v1.2), or later. ADDED box/constbox/error.mustache Index: box/constbox/error.mustache ================================================================== --- box/constbox/error.mustache +++ box/constbox/error.mustache @@ -0,0 +1,6 @@ +
+
+

{{ErrorTitle}}

+
+{{ErrorText}} +
DELETED box/constbox/error.sxn Index: box/constbox/error.sxn ================================================================== --- box/constbox/error.sxn +++ box/constbox/error.sxn @@ -1,17 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 ,heading)) - ,message -) ADDED box/constbox/form.mustache Index: box/constbox/form.mustache ================================================================== --- box/constbox/form.mustache +++ box/constbox/form.mustache @@ -0,0 +1,54 @@ +
+
+

{{Heading}}

+
+
+
+ + +
+
+
+ + +{{#HasRoleData}} + +{{#RoleData}} + +{{/HasRoleData}} +
+ + +
+
+ + +
+
+ + +{{#HasSyntaxData}} + +{{#SyntaxData}} + +{{/HasSyntaxData}}
+
+{{#IsTextContent}} + + +{{/IsTextContent}} +
+
+ + +
+
+
DELETED box/constbox/form.sxn Index: box/constbox/form.sxn ================================================================== --- box/constbox/form.sxn +++ box/constbox/form.sxn @@ -1,63 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 ,heading)) - (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data")) - (div - (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "ⓘ"))) - (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") - (title "Title of this zettel") - (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus)))) - (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") (pattern "\\w*") (id "zs-role") (name "role") - (title "One word, letters and digits, but no spaces, to set the main role of the zettel.") - (placeholder "role..") (value ,meta-role) (dir "auto") - ,@(if role-data '((list "zs-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") - (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces") - (placeholder "#tag") (value ,meta-tags) (dir "auto")))) - (div - (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "ⓘ"))) - (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") - (title "Additional metadata about the zettel") - (placeholder "metakey: metavalue") (dir "auto")) ,meta)) - (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") (pattern "\\w*") (id "zs-syntax") (name "syntax") - (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.") - (placeholder "syntax..") (value ,meta-syntax) (dir "auto") - ,@(if syntax-data '((list "zs-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") - (title "Zettel content, according to the given syntax") - (placeholder "Zettel content..") (dir "auto")) ,content) - )) - ) - (div - (input (@ (class "zs-primary") (type "submit") (value "Submit"))) - (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save"))) - (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file"))) - )) -) ADDED box/constbox/info.mustache Index: box/constbox/info.mustache ================================================================== --- box/constbox/info.mustache +++ box/constbox/info.mustache @@ -0,0 +1,72 @@ +
+
+

Information for Zettel {{Zid}}

+WebContext +{{#CanWrite}} · Edit{{/CanWrite}} +{{#CanCopy}} · Copy{{/CanCopy}} +{{#CanVersion}} · Version{{/CanVersion}} +{{#CanFolge}} · Folge{{/CanFolge}} +{{#CanRename}}· Rename{{/CanRename}} +{{#CanDelete}}· Delete{{/CanDelete}} +
+

Interpreted Metadata

+{{#MetaData}}{{/MetaData}}
{{Key}}{{{Value}}}
+

References

+{{#HasLocLinks}} +

Local

+ +{{/HasLocLinks}} +{{#QueryLinks.Has}} +

Queries

+ +{{/QueryLinks.Has}} +{{#HasExtLinks}} +

External

+ +{{/HasExtLinks}} +

Unlinked

+{{{UnLinksContent}}} +
+ + +
+

Parts and encodings

+ +{{#EvalMatrix}} + + +{{#Elements}} +{{/Elements}} + +{{/EvalMatrix}} +
{{Header}}{{Text}}
+

Parsed (not evaluated)

+ +{{#ParseMatrix}} + + +{{#Elements}} +{{/Elements}} + +{{/ParseMatrix}} +
{{Header}}{{Text}}
+{{#HasShadowLinks}} +

Shadowed Boxes

+ +{{/HasShadowLinks}} +{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}} +
DELETED box/constbox/info.sxn Index: box/constbox/info.sxn ================================================================== --- box/constbox/info.sxn +++ box/constbox/info.sxn @@ -1,48 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 "Information for Zettel " ,zid) - (p - (a (@ (href ,web-url)) "Web") - (@H " · ") (a (@ (href ,context-url)) "Context") - (@H " / ") (a (@ (href ,context-full-url)) "Full") - ,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit"))) - ,@(ROLE-DEFAULT-actions (current-binding)) - ,@(if (bound? 'reindex-url) `((@H " · ") (a (@ (href ,reindex-url)) "Reindex"))) - ,@(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 wui-info-meta-table-row metadata)) - (h2 "References") - ,@(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") - ,(wui-enc-matrix enc-eval) - (h3 "Parsed (not evaluated)") - ,(wui-enc-matrix enc-parsed) - ,@(if shadow-links - `((h2 "Shadowed Boxes") - (ul ,@(map wui-item shadow-links)) - ) - ) -) Index: box/constbox/license.txt ================================================================== --- box/constbox/license.txt +++ box/constbox/license.txt @@ -1,6 +1,6 @@ -Copyright (c) 2020-present Detlef Stern +Copyright (c) 2020-2022 Detlef Stern Licensed under the EUPL Zettelstore is licensed under the European Union Public License, version 1.2 or later (EUPL v. 1.2). The license is available in the official languages of the ADDED box/constbox/listzettel.mustache Index: box/constbox/listzettel.mustache ================================================================== --- box/constbox/listzettel.mustache +++ box/constbox/listzettel.mustache @@ -0,0 +1,7 @@ +
+

{{Title}}

+
+
+ +
+{{{Content}}} DELETED box/constbox/listzettel.sxn Index: box/constbox/listzettel.sxn ================================================================== --- box/constbox/listzettel.sxn +++ box/constbox/listzettel.sxn @@ -1,50 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 ,heading)) - (search (form (@ (action ,search-url)) - (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query) - (title "Contains the search that leads to the list below. You're allowed to modify it") - (placeholder "Search..") (value ,query-value) (dir "auto"))))) - ,@(if (bound? 'tag-zettel) - `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel)) - ) - ,@(if (bound? 'create-tag-zettel) - `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel)) - ) - ,@(if (bound? 'role-zettel) - `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel)) - ) - ,@(if (bound? 'create-role-zettel) - `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel)) - ) - ,@content - ,@endnotes - (form (@ (action ,(if (bound? 'create-url) create-url))) - ,(if (bound? 'data-url) - `(@L "Other encodings" - ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ") - (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"))) - ) - ) - ) -) ADDED box/constbox/login.mustache Index: box/constbox/login.mustache ================================================================== --- box/constbox/login.mustache +++ box/constbox/login.mustache @@ -0,0 +1,19 @@ +
+
+

{{Title}}

+
+{{#Retry}} +
Wrong user name / password. Try again.
+{{/Retry}} +
+
+ + +
+
+ + +
+
+
+
DELETED box/constbox/login.sxn Index: box/constbox/login.sxn ================================================================== --- box/constbox/login.sxn +++ box/constbox/login.sxn @@ -1,27 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (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)))) - (div - (label (@ (for "password")) "Password:") - (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password..")))) - (div - (input (@ (class "zs-primary") (type "submit") (value "Login")))) - ) -) Index: box/constbox/newtoc.zettel ================================================================== --- box/constbox/newtoc.zettel +++ box/constbox/newtoc.zettel @@ -1,6 +1,4 @@ This zettel lists all zettel that should act as a template for new zettel. These zettel will be included in the ""New"" menu of the WebUI. * [[New Zettel|00000000090001]] -* [[New Role|00000000090004]] -* [[New Tag|00000000090003]] * [[New User|00000000090002]] DELETED box/constbox/prelude.sxn Index: box/constbox/prelude.sxn ================================================================== --- box/constbox/prelude.sxn +++ box/constbox/prelude.sxn @@ -1,62 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -;;; This zettel contains sxn definitions that are independent of specific -;;; subsystems, such as WebUI, API, or other. It just contains generic code to -;;; be used in all places. It asumes that the symbols NIL and T are defined. - -;; not macro -(defmacro not (x) `(if ,x NIL T)) - -;; not= macro, to negate an equivalence -(defmacro not= args `(not (= ,@args))) - -;; let* macro -;; -;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings. -(defmacro let* (bindings . body) - (if (null? bindings) - `(begin ,@body) - `(let ((,(caar bindings) ,(cadar bindings))) - (let* ,(cdr bindings) ,@body)))) - -;; cond macro -;; -;; (cond ((COND EXPR) ...)) -(defmacro cond clauses - (if (null? clauses) - () - (let* ((clause (car clauses)) - (the-cond (car clause))) - (if (= the-cond T) - `(begin ,@(cdr clause)) - `(if ,the-cond - (begin ,@(cdr clause)) - (cond ,@(cdr clauses))))))) - -;; and macro -;; -;; (and EXPR ...) -(defmacro and args - (cond ((null? args) T) - ((null? (cdr args)) (car args)) - (T `(if ,(car args) (and ,@(cdr args)))))) - - -;; or macro -;; -;; (or EXPR ...) -(defmacro or args - (cond ((null? args) NIL) - ((null? (cdr args)) (car args)) - (T `(if ,(car args) T (or ,@(cdr args)))))) ADDED box/constbox/rename.mustache Index: box/constbox/rename.mustache ================================================================== --- box/constbox/rename.mustache +++ box/constbox/rename.mustache @@ -0,0 +1,41 @@ +
+
+

Rename Zettel {{Zid}}

+
+

Do you really want to rename this zettel?

+{{#Incoming.Has}} +
+

Warning!

+

If you rename this zettel, incoming references from the following zettel will become invalid.

+
    +{{#Incoming.Links}} +
  • {{Text}}
  • +{{/Incoming.Links}} +
+
+{{/Incoming.Has}} +{{#HasUselessFiles}} +
+

Warning!

+

Renaming this zettel will also delete the following files, so that they will not be interpreted as content for a zettel with identifier {{Zid}}.

+
    +{{#UselessFiles}} +
  • {{{.}}}
  • +{{/UselessFiles}} +
+
+{{/HasUselessFiles}} +
+
+ + +
+ +
+
+
+{{#MetaPairs}} +
{{Key}}:
{{Value}}
+{{/MetaPairs}} +
+
DELETED box/constbox/rename.sxn Index: box/constbox/rename.sxn ================================================================== --- box/constbox/rename.sxn +++ box/constbox/rename.sxn @@ -1,42 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header (h1 "Rename Zettel " ,zid)) - (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 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 wui-item useless)) - )) - ) - (form (@ (method "POST")) - (input (@ (type "hidden") (id "curzid") (name "curzid") (value ,zid))) - (div - (label (@ (for "newzid")) "New zettel id") - (input (@ (class "zs-input") (type "text") (inputmode "numeric") (id "newzid") (name "newzid") - (pattern "\\d{14}") - (title "New zettel identifier, must be unique") - (placeholder "ZID..") (value ,zid) (autofocus)))) - (div (input (@ (class "zs-primary") (type "submit") (value "Rename")))) - ) - ,(wui-meta-desc metapairs) -) DELETED box/constbox/roleconfiguration.zettel Index: box/constbox/roleconfiguration.zettel ================================================================== --- box/constbox/roleconfiguration.zettel +++ box/constbox/roleconfiguration.zettel @@ -1,20 +0,0 @@ -Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software. - -Typically, there are some public zettel that show the license of this software, its dependencies, some CSS code to make the default web user interface a litte bit nicer, and the defult image to singal a broken image. - -Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled. -In this case, one additional configuration zettel is the zettel containing the version number of this software. -Other zettel are showing the supported metadata keys and supported syntax values. -Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"". - -Most important is the zettel that contains the runtime configuration. -You may change its metadata value to change the behaviour of the software. - -One configuration is the ""expert mode"". -If enabled, and if you are authorized so see them, you will discover some more zettel. -For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more. - -You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel. - -By default, user zettel (for authentification) use also the role ""configuration"". -However, you are allowed to change this. DELETED box/constbox/rolerole.zettel Index: box/constbox/rolerole.zettel ================================================================== --- box/constbox/rolerole.zettel +++ box/constbox/rolerole.zettel @@ -1,10 +0,0 @@ -A zettel with the role ""role"" describes a specific role. -The described role must be the title of such a zettel. - -This zettel is such a zettel, as it describes the meaning of the role ""role"". -Therefore it has the title ""role"" too. -If you like, this zettel is a meta-role. - -You are free to create your own role-describing zettel. -For example, you want to document the intended meaning of the role. -You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel. DELETED box/constbox/roletag.zettel Index: box/constbox/roletag.zettel ================================================================== --- box/constbox/roletag.zettel +++ box/constbox/roletag.zettel @@ -1,6 +0,0 @@ -A zettel with role ""tag"" is a zettel that describes specific tag. -The tag name must be the title of such a zettel. - -Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"". -These zettel with the role ""tag"" describe specific tags. -These might form a hierarchy of meta-tags (and meta-roles). DELETED box/constbox/rolezettel.zettel Index: box/constbox/rolezettel.zettel ================================================================== --- box/constbox/rolezettel.zettel +++ box/constbox/rolezettel.zettel @@ -1,7 +0,0 @@ -A zettel with the role ""zettel"" is typically used to document your own thoughts. -Such zettel are the main reason to use the software Zettelstore. - -The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information. - -You are free to change this. -In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"". DELETED box/constbox/start.sxn Index: box/constbox/start.sxn ================================================================== --- box/constbox/start.sxn +++ box/constbox/start.sxn @@ -1,17 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -;;; This zettel is the start of the loading sequence for Sx code used in the -;;; Zettelstore. Via the precursor metadata, dependend zettel are evaluated -;;; before this zettel. You must always depend, directly or indirectly on the -;;; "Zettelstore Sxn Base Code" zettel. It provides the base definitions. DELETED box/constbox/wuicode.sxn Index: box/constbox/wuicode.sxn ================================================================== --- box/constbox/wuicode.sxn +++ box/constbox/wuicode.sxn @@ -1,143 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -;; Contains WebUI specific code, but not related to a specific template. - -;; wui-list-item returns the argument as a HTML list item. -(defun wui-item (s) `(li ,s)) - -;; wui-info-meta-table-row takes a pair and translates it into a HTML table row -;; with two columns. -(defun wui-info-meta-table-row (p) - `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(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. -(defun wui-valid-link (l) - (if (car l) - `(li (a (@ (href ,(cdr l))) ,(cdr l))) - `(li ,(cdr l)))) - -;; wui-link takes a link (title . url) and returns a HTML reference. -(defun 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. -(defun 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. -(defun 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. -(defun 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. -(defun wui-option-value (v) `(option (@ (value ,v)))) - -;; wui-datalist returns a HTML datalist with the given HTML identifier and a -;; list of values. -(defun 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. -(defun 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. -(defun 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. -(defun wui-enc-matrix (matrix) - `(table - ,@(map - (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row)))) - matrix))) - -;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel -;; identifier. It is used in the base template to update the metadata of the -;; HTML page to include some role specific CSS code. -;; Referenced in function "ROLE-DEFAULT-meta". -(defvar CSS-ROLE-map '()) - -;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role -;; specific code should include the returned list of this function. -(defun ROLE-DEFAULT-meta (binding) - `(,@(let* ((meta-role (binding-lookup 'meta-role binding)) - (entry (assoc CSS-ROLE-map meta-role))) - (if (pair? entry) - `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry)))))) - ) - ) - ) -) - -;; ACTION-SEPARATOR defines a HTML value that separates actions links. -(defvar ACTION-SEPARATOR '(@H " · ")) - -;; ROLE-DEFAULT-actions returns the default text for actions. -(defun ROLE-DEFAULT-actions (binding) - `(,@(let ((copy-url (binding-lookup 'copy-url binding))) - (if (defined? copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))) - ,@(let ((version-url (binding-lookup 'version-url binding))) - (if (defined? version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))) - ,@(let ((child-url (binding-lookup 'child-url binding))) - (if (defined? child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))) - ,@(let ((folge-url (binding-lookup 'folge-url binding))) - (if (defined? folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))) - ) -) - -;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag". -(defun ROLE-tag-actions (binding) - `(,@(ROLE-DEFAULT-actions binding) - ,@(let ((title (binding-lookup 'title binding))) - (if (and (defined? title) title) - `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "tags:" title)))) "Zettel")) - ) - ) - ) -) - -;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role". -(defun ROLE-role-actions (binding) - `(,@(ROLE-DEFAULT-actions binding) - ,@(let ((title (binding-lookup 'title binding))) - (if (and (defined? title) title) - `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel")) - ) - ) - ) -) - -;; ROLE-DEFAULT-heading returns the default text for headings, below the -;; references of a zettel. In most cases it should be called from an -;; overwriting function. -(defun ROLE-DEFAULT-heading (binding) - `(,@(let ((meta-url (binding-lookup 'meta-url binding))) - (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url)))) - ,@(let ((urls (binding-lookup 'urls binding))) - (if (defined? urls) - (map (lambda (u) `(@L (br) ,(car u) ": " ,(url-to-html (cdr u)))) urls) - ) - ) - ,@(let ((meta-author (binding-lookup 'meta-author binding))) - (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author))) - ) -) ADDED box/constbox/zettel.mustache Index: box/constbox/zettel.mustache ================================================================== --- box/constbox/zettel.mustache +++ box/constbox/zettel.mustache @@ -0,0 +1,52 @@ +
+
+

{{{HTMLTitle}}}

+
+{{#CanWrite}}Edit ·{{/CanWrite}} +{{Zid}} · +Info · +({{RoleText}}) +{{#Tags.Has}}· {{#Tags.Links}} {{Text}}{{/Tags.Links}}{{/Tags.Has}} +{{#CanCopy}}· Copy{{/CanCopy}} +{{#CanVersion}}· Version{{/CanVersion}} +{{#CanFolge}}· Folge{{/CanFolge}} +{{#PredecessorRefs}}
Predecessor: {{{PredecessorRefs}}}{{/PredecessorRefs}} +{{#PrecursorRefs}}
Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}} +{{#HasExtURL}}
URL: {{ExtURL}}{{/HasExtURL}} +{{#Author}}
By {{Author}}{{/Author}} +
+
+{{{Content}}} +
+{{#NeedBottomNav}}{{/NeedBottomNav}} DELETED box/constbox/zettel.sxn Index: box/constbox/zettel.sxn ================================================================== --- box/constbox/zettel.sxn +++ box/constbox/zettel.sxn @@ -1,43 +0,0 @@ -;;;---------------------------------------------------------------------------- -;;; 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. -;;; -;;; SPDX-License-Identifier: EUPL-1.2 -;;; SPDX-FileCopyrightText: 2023-present Detlef Stern -;;;---------------------------------------------------------------------------- - -`(article - (header - (h1 ,heading) - (div (@ (class "zs-meta")) - ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " · "))) - ,zid (@H " · ") - (a (@ (href ,info-url)) "Info") (@H " · ") - "(" ,@(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)) - ,@(ROLE-DEFAULT-actions (current-binding)) - ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs)) - ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs)) - ,@(if superior-refs `((br) "Superior: " ,superior-refs)) - ,@(ROLE-DEFAULT-heading (current-binding)) - ) - ) - ,@content - ,endnotes - ,@(if (or folge-links subordinate-links back-links successor-links) - `((nav - ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links))))) - ,@(if subordinate-links `((details (@ (,subordinate-open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links))))) - ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links))))) - ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links))))) - )) - ) -) Index: box/dirbox/dirbox.go ================================================================== --- box/dirbox/dirbox.go +++ box/dirbox/dirbox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package dirbox provides a directory-based zettel box. package dirbox @@ -23,16 +20,16 @@ "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/box/notify" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "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("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { var log *logger.Logger @@ -90,11 +87,11 @@ dirNotifySimple dirNotifyFS ) func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec { - for range 2 { + for count := 0; count < 2; count++ { switch notifyType { case kernel.BoxDirTypeNotify: return dirNotifyFS case kernel.BoxDirTypeSimple: return dirNotifySimple @@ -130,33 +127,15 @@ func (dp *dirBox) Location() string { return dp.location } -func (dp *dirBox) State() box.StartState { - if ds := dp.dirSrv; ds != nil { - switch ds.State() { - case notify.DsCreated: - return box.StartStateStopped - case notify.DsStarting: - return box.StartStateStarting - case notify.DsWorking: - return box.StartStateStarted - case notify.DsMissing: - return box.StartStateStarted - case notify.DsStopping: - return box.StartStateStopping - } - } - return box.StartStateStopped -} - func (dp *dirBox) Start(context.Context) error { dp.mxCmds.Lock() defer dp.mxCmds.Unlock() dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs) - for i := range dp.fSrvs { + for i := uint32(0); i < dp.fSrvs; i++ { cc := make(chan fileCmd) go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc) dp.fCmds = append(dp.fCmds, cc) } @@ -167,16 +146,15 @@ notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir) default: notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir) } if err != nil { - dp.log.Error().Err(err).Msg("Unable to create directory supervisor") + dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor") dp.stopFileServices() return err } dp.dirSrv = notify.NewDirService( - dp, dp.log.Clone().Str("sub", "dirsrv").Child(), notifier, dp.cdata.Notify, ) dp.dirSrv.Start() @@ -201,14 +179,14 @@ for _, c := range dp.fCmds { close(c) } } -func (dp *dirBox) notifyChanged(zid id.Zid) { +func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if chci := dp.cdata.Notify; chci != nil { - dp.log.Trace().Zid(zid).Msg("notifyChanged") - chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} + dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged") + chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd { // Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -224,11 +202,11 @@ func (dp *dirBox) CanCreateZettel(_ context.Context) bool { return !dp.readonly } -func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { if dp.readonly { return id.Invalid, box.ErrReadOnly } newZid, err := dp.dirSrv.SetNewDirEntry() @@ -242,31 +220,44 @@ err = dp.srvSetZettel(ctx, &entry, zettel) if err == nil { err = dp.dirSrv.UpdateDirEntry(&entry) } - dp.notifyChanged(meta.Zid) + dp.notifyChanged(box.OnZettel, meta.Zid) dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel") return meta.Zid, err } -func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { +func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} + return domain.Zettel{}, box.ErrNotFound } m, c, err := dp.srvGetMetaContent(ctx, entry, zid) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } - zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)} + zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)} dp.log.Trace().Zid(zid).Msg("GetZettel") return zettel, nil } -func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool { - return dp.dirSrv.GetDirEntry(zid).IsValid() +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) 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") @@ -291,23 +282,23 @@ handle(m) } return nil } -func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { +func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return !dp.readonly } -func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { +func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { if dp.readonly { return box.ErrReadOnly } meta := zettel.Meta zid := meta.Zid if !zid.IsValid() { - return box.ErrInvalidZid{Zid: zid.String()} + return &box.ErrInvalidID{Zid: zid} } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { // Existing zettel, but new in this box. entry = ¬ify.DirEntry{Zid: zid} @@ -314,17 +305,17 @@ } dp.updateEntryFromMetaContent(entry, meta, zettel.Content) dp.dirSrv.UpdateDirEntry(entry) err := dp.srvSetZettel(ctx, entry, zettel) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel") return err } -func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) { +func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content domain.Content) { entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax) } func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool { return !dp.readonly @@ -334,19 +325,19 @@ if curZid == newZid { return nil } curEntry := dp.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { - return box.ErrZettelNotFound{Zid: curZid} + return box.ErrNotFound } if dp.readonly { return box.ErrReadOnly } // Check whether zettel with new ID already exists in this box. - if dp.HasZettel(ctx, newZid) { - return box.ErrInvalidZid{Zid: newZid.String()} + if _, err := dp.doGetMeta(ctx, newZid); err == nil { + return &box.ErrInvalidID{Zid: newZid} } oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid) if err != nil { return err @@ -355,20 +346,20 @@ newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid) if err != nil { return err } oldMeta.Zid = newZid - newZettel := zettel.Zettel{Meta: oldMeta, Content: zettel.NewContent(oldContent)} + newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)} if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil { // "Rollback" rename. No error checking... dp.dirSrv.RenameDirEntry(&newEntry, curZid) return err } err = dp.srvDeleteZettel(ctx, curEntry, curZid) if err == nil { - dp.notifyChanged(curZid) - dp.notifyChanged(newZid) + dp.notifyChanged(box.OnZettel, curZid) + dp.notifyChanged(box.OnZettel, newZid) } dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel") return err } @@ -385,19 +376,19 @@ return box.ErrReadOnly } entry := dp.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return box.ErrZettelNotFound{Zid: zid} + return box.ErrNotFound } err := dp.dirSrv.DeleteDirEntry(zid) if err != nil { return nil } err = dp.srvDeleteZettel(ctx, entry, zid) if err == nil { - dp.notifyChanged(zid) + dp.notifyChanged(box.OnZettel, zid) } dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel") return err } Index: box/dirbox/dirbox_test.go ================================================================== --- box/dirbox/dirbox_test.go +++ box/dirbox/dirbox_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import "testing" @@ -33,11 +30,11 @@ } } } func TestMakePrime(t *testing.T) { - for i := range uint32(1500) { + for i := uint32(0); i < 1500; i++ { np := makePrime(i) if np < i { t.Errorf("makePrime(%d) < %d", i, np) continue } Index: box/dirbox/service.go ================================================================== --- box/dirbox/service.go +++ box/dirbox/service.go @@ -1,56 +1,52 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package dirbox import ( "context" - "fmt" "io" "os" "path/filepath" "time" - "t73f.de/r/zsc/input" "zettelstore.de/z/box/filebox" "zettelstore.de/z/box/notify" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) { // Something may panic. Ensure a running service. defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("FileService", ri) + if r := recover(); r != nil { + kernel.Main.LogRecover("FileService", r) go fileService(i, log, dirPath, cmds) } }() - log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") + log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started") for cmd := range cmds { - cmd.run(dirPath) + cmd.run(log, dirPath) } - log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") + log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped") } type fileCmd interface { - run(string) + run(*logger.Logger, string) } const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing. // COMMAND: srvGetMeta ---------------------------------------- @@ -77,22 +73,23 @@ type resGetMeta struct { meta *meta.Meta err error } -func (cmd *fileGetMeta) run(dirPath string) { +func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) { var m *meta.Meta var err error entry := cmd.entry zid := entry.Zid if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) - } else if entry.HasMetaInContent() { + log.Panic().Zid(zid).Msg("No meta, no content in getMeta") + } + if entry.HasMetaInContent() { m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName)) } else { m = filebox.CalcDefaultMeta(zid, contentExt) } } else { @@ -129,11 +126,11 @@ meta *meta.Meta content []byte err error } -func (cmd *fileGetMetaContent) run(dirPath string) { +func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) { var m *meta.Meta var content []byte var err error entry := cmd.entry @@ -141,12 +138,13 @@ contentName := entry.ContentName contentExt := entry.ContentExt contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" || contentExt == "" { - err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid) - } else if entry.HasMetaInContent() { + log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent") + } + if entry.HasMetaInContent() { m, content, err = parseMetaContentFile(zid, contentPath) } else { m = filebox.CalcDefaultMeta(zid, contentExt) content, err = os.ReadFile(contentPath) } @@ -168,11 +166,11 @@ // COMMAND: srvSetZettel ---------------------------------------- // // Writes a new or exsting zettel. -func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel zettel.Zettel) error { +func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel domain.Zettel) error { rc := make(chan resSetZettel, 1) dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc} ctx, cancel := context.WithTimeout(ctx, serviceTimeout) defer cancel() select { @@ -183,40 +181,38 @@ } } type fileSetZettel struct { entry *notify.DirEntry - zettel zettel.Zettel + zettel domain.Zettel rc chan<- resSetZettel } type resSetZettel = error -func (cmd *fileSetZettel) run(dirPath string) { - var err error +func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) { entry := cmd.entry zid := entry.Zid contentName := entry.ContentName m := cmd.zettel.Meta content := cmd.zettel.Content.AsBytes() metaName := entry.MetaName if metaName == "" { if contentName == "" { - err = fmt.Errorf("no meta, no content in setZettel, zid=%v", zid) - } else { - contentPath := filepath.Join(dirPath, contentName) - if entry.HasMetaInContent() { - err = writeZettelFile(contentPath, m, content) - cmd.rc <- err - return - } - err = writeFileContent(contentPath, content) - } + log.Panic().Zid(zid).Msg("No meta, no content in setZettel") + } + contentPath := filepath.Join(dirPath, contentName) + if entry.HasMetaInContent() { + err := writeZettelFile(contentPath, m, content) + cmd.rc <- err + return + } + err := writeFileContent(contentPath, content) cmd.rc <- err return } - err = writeMetaFile(filepath.Join(dirPath, metaName), m) + err := writeMetaFile(filepath.Join(dirPath, metaName), m) if err == nil && contentName != "" { err = writeFileContent(filepath.Join(dirPath, contentName), content) } cmd.rc <- err } @@ -239,11 +235,13 @@ func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error { zettelFile, err := openFileWrite(contentPath) if err != nil { return err } - err = writeMetaHeader(zettelFile, m) + if err == nil { + err = writeMetaHeader(zettelFile, m) + } if err == nil { _, err = zettelFile.Write(content) } if err1 := zettelFile.Close(); err == nil { err = err1 @@ -300,22 +298,21 @@ entry *notify.DirEntry rc chan<- resDeleteZettel } type resDeleteZettel = error -func (cmd *fileDeleteZettel) run(dirPath string) { +func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) { var err error entry := cmd.entry contentName := entry.ContentName contentPath := filepath.Join(dirPath, contentName) if metaName := entry.MetaName; metaName == "" { if contentName == "" { - err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid) - } else { - err = os.Remove(contentPath) + log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent") } + err = os.Remove(contentPath) } else { if contentName != "" { err = os.Remove(contentPath) } err1 := os.Remove(filepath.Join(dirPath, metaName)) Index: box/filebox/filebox.go ================================================================== --- box/filebox/filebox.go +++ box/filebox/filebox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package filebox provides boxes that are stored in a file. package filebox @@ -18,16 +15,16 @@ "errors" "net/url" "path/filepath" "strings" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func init() { manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) { path := getFilepathFromURL(u) Index: box/filebox/zipbox.go ================================================================== --- box/filebox/zipbox.go +++ box/filebox/zipbox.go @@ -1,35 +1,31 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package filebox import ( "archive/zip" "context" - "fmt" "io" "strings" - "t73f.de/r/zsc/input" "zettelstore.de/z/box" "zettelstore.de/z/box/notify" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" "zettelstore.de/z/logger" "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) type zipBox struct { log *logger.Logger number int @@ -44,36 +40,21 @@ return "file://" + zb.name } return "file:" + zb.name } -func (zb *zipBox) State() box.StartState { - if ds := zb.dirSrv; ds != nil { - switch ds.State() { - case notify.DsCreated: - return box.StartStateStopped - case notify.DsStarting: - return box.StartStateStarting - case notify.DsWorking: - return box.StartStateStarted - case notify.DsMissing: - return box.StartStateStarted - case notify.DsStopping: - return box.StartStateStopping - } - } - return box.StartStateStopped -} - func (zb *zipBox) Start(context.Context) error { reader, err := zip.OpenReader(zb.name) if err != nil { return err } reader.Close() - zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name) - zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify) + zipNotifier, err := notify.NewSimpleZipNotifier(zb.log, zb.name) + if err != nil { + return err + } + zb.dirSrv = notify.NewDirService(zb.log, zipNotifier, zb.notify) zb.dirSrv.Start() return nil } func (zb *zipBox) Refresh(_ context.Context) { @@ -81,21 +62,28 @@ zb.log.Trace().Msg("Refresh") } func (zb *zipBox) Stop(context.Context) { zb.dirSrv.Stop() - zb.dirSrv = nil +} + +func (*zipBox) CanCreateZettel(context.Context) bool { return false } + +func (zb *zipBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) { + err := box.ErrReadOnly + zb.log.Trace().Err(err).Msg("CreateZettel") + return id.Invalid, err } -func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} + return domain.Zettel{}, box.ErrNotFound } reader, err := zip.OpenReader(zb.name) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } defer reader.Close() var m *meta.Meta var src []byte @@ -102,16 +90,15 @@ var inMeta bool contentName := entry.ContentName if metaName := entry.MetaName; metaName == "" { if contentName == "" { - err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid) - return zettel.Zettel{}, err + zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel") } src, err = readZipFileContent(reader, entry.ContentName) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } if entry.HasMetaInContent() { inp := input.NewInput(src) m = meta.NewFromInput(zid, inp) src = src[inp.Pos:] @@ -119,28 +106,39 @@ m = CalcDefaultMeta(zid, entry.ContentExt) } } else { m, err = readZipMetaFile(reader, zid, metaName) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } inMeta = true if contentName != "" { src, err = readZipFileContent(reader, entry.ContentName) if err != nil { - return zettel.Zettel{}, err + return domain.Zettel{}, err } } } 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 + return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil } -func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool { - return zb.dirSrv.GetDirEntry(zid).IsValid() +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) 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") @@ -169,10 +167,18 @@ zb.enricher.Enrich(ctx, m, zb.number) handle(m) } return nil } + +func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false } + +func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error { + err := box.ErrReadOnly + zb.log.Trace().Err(err).Msg("UpdateZettel") + return err +} func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool { entry := zb.dirSrv.GetDirEntry(zid) return !entry.IsValid() } @@ -182,11 +188,11 @@ if curZid == newZid { err = nil } curEntry := zb.dirSrv.GetDirEntry(curZid) if !curEntry.IsValid() { - err = box.ErrZettelNotFound{Zid: curZid} + err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("RenameZettel") return err } @@ -194,11 +200,11 @@ func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error { err := box.ErrReadOnly entry := zb.dirSrv.GetDirEntry(zid) if !entry.IsValid() { - err = box.ErrZettelNotFound{Zid: zid} + err = box.ErrNotFound } zb.log.Trace().Err(err).Msg("DeleteZettel") return err } @@ -212,12 +218,13 @@ var inMeta bool if metaName := entry.MetaName; metaName == "" { contentName := entry.ContentName contentExt := entry.ContentExt if contentName == "" || contentExt == "" { - err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid) - } else if entry.HasMetaInContent() { + zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta") + } + if entry.HasMetaInContent() { m, err = readZipMetaFile(reader, zid, contentName) } else { m = CalcDefaultMeta(zid, contentExt) } } else { Index: box/helper.go ================================================================== --- box/helper.go +++ box/helper.go @@ -1,32 +1,27 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package box import ( - "net/url" - "strconv" "time" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // GetNewZid calculates a new and unused zettel identifier, based on the current date and time. func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) { withSeconds := false - for range 90 { // Must be completed within 9 seconds (less than web/server.writeTimeout) + for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout) zid := id.New(withSeconds) found, err := testZid(zid) if err != nil { return id.Invalid, err } @@ -37,30 +32,5 @@ time.Sleep(100 * time.Millisecond) withSeconds = true } return id.Invalid, ErrConflict } - -// GetQueryBool is a helper function to extract bool values from a box URI. -func GetQueryBool(u *url.URL, key string) bool { - _, ok := u.Query()[key] - return ok -} - -// GetQueryInt is a helper function to extract int values of a specified range from a box URI. -func GetQueryInt(u *url.URL, key string, min, def, max int) int { - sVal := u.Query().Get(key) - if sVal == "" { - return def - } - iVal, err := strconv.Atoi(sVal) - if err != nil { - return def - } - if iVal < min { - return min - } - if iVal > max { - return max - } - return iVal -} Index: box/manager/anteroom.go ================================================================== --- box/manager/anteroom.go +++ box/manager/anteroom.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "sync" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) type arAction int const ( @@ -26,87 +23,108 @@ arReload arZettel ) type anteroom struct { + num uint64 next *anteroom - waiting id.Set + waiting map[id.Zid]arAction curLoad int reload bool } -type anteroomQueue struct { +type anterooms struct { mx sync.Mutex + nextNum uint64 first *anteroom last *anteroom maxLoad int } -func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} } +func newAnterooms(maxLoad int) *anterooms { + return &anterooms{maxLoad: maxLoad} +} -func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) { +func (ar *anterooms) EnqueueZettel(zid id.Zid) { if !zid.IsValid() { return } ar.mx.Lock() defer ar.mx.Unlock() if ar.first == nil { - ar.first = ar.makeAnteroom(zid) + ar.first = ar.makeAnteroom(zid, arZettel) ar.last = ar.first return } for room := ar.first; room != nil; room = room.next { if room.reload { continue // Do not put zettel in reload room } if _, ok := room.waiting[zid]; ok { - // Zettel is already waiting. Nothing to do. + // Zettel is already waiting. return } } if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) { - room.waiting.Add(zid) + room.waiting[zid] = arZettel room.curLoad++ return } - room := ar.makeAnteroom(zid) + room := ar.makeAnteroom(zid, arZettel) ar.last.next = room ar.last = room } -func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom { - if zid == id.Invalid { - panic(zid) - } - waiting := id.NewSetCap(max(ar.maxLoad, 100), zid) - return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false} -} - -func (ar *anteroomQueue) Reset() { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true} - ar.last = ar.first -} - -func (ar *anteroomQueue) Reload(allZids id.Set) { - ar.mx.Lock() - defer ar.mx.Unlock() - ar.deleteReloadedRooms() - - if ns := len(allZids); ns > 0 { - ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: ns, reload: true} - if ar.first.next == nil { - ar.last = ar.first - } - } else { - ar.first = nil - ar.last = nil - } -} - -func (ar *anteroomQueue) deleteReloadedRooms() { +func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom { + c := ar.maxLoad + if c == 0 { + c = 100 + } + waiting := make(map[id.Zid]arAction, c) + waiting[zid] = action + ar.nextNum++ + return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false} +} + +func (ar *anterooms) Reset() { + ar.mx.Lock() + defer ar.mx.Unlock() + ar.first = ar.makeAnteroom(id.Invalid, arReload) + ar.last = ar.first +} + +func (ar *anterooms) Reload(newZids id.Set) uint64 { + ar.mx.Lock() + defer ar.mx.Unlock() + newWaiting := createWaitingSet(newZids) + ar.deleteReloadedRooms() + + if ns := len(newWaiting); ns > 0 { + ar.nextNum++ + ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns} + if ar.first.next == nil { + ar.last = ar.first + } + return ar.nextNum + } + + ar.first = nil + ar.last = nil + return 0 +} + +func createWaitingSet(zids id.Set) map[id.Zid]arAction { + waitingSet := make(map[id.Zid]arAction, len(zids)) + for zid := range zids { + if zid.IsValid() { + waitingSet[zid] = arZettel + } + } + return waitingSet +} + +func (ar *anterooms) deleteReloadedRooms() { room := ar.first for room != nil && room.reload { room = room.next } ar.first = room @@ -113,32 +131,24 @@ if room == nil { ar.last = nil } } -func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) { +func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) { ar.mx.Lock() defer ar.mx.Unlock() - first := ar.first - if first != nil { - if first.waiting == nil && first.reload { - ar.removeFirst() - return arReload, id.Invalid, false - } - for zid := range first.waiting { - delete(first.waiting, zid) - if len(first.waiting) == 0 { - ar.removeFirst() - } - return arZettel, zid, first.reload - } - ar.removeFirst() - } - return arNothing, id.Invalid, false -} - -func (ar *anteroomQueue) removeFirst() { - ar.first = ar.first.next - if ar.first == nil { - ar.last = nil - } + if ar.first == nil { + return arNothing, id.Invalid, 0 + } + for zid, action := range ar.first.waiting { + roomNo := ar.first.num + delete(ar.first.waiting, zid) + if len(ar.first.waiting) == 0 { + ar.first = ar.first.next + if ar.first == nil { + ar.last = nil + } + } + return action, zid, roomNo + } + return arNothing, id.Invalid, 0 } Index: box/manager/anteroom_test.go ================================================================== --- box/manager/anteroom_test.go +++ box/manager/anteroom_test.go @@ -1,33 +1,30 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "testing" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) func TestSimple(t *testing.T) { t.Parallel() - ar := newAnteroomQueue(2) + ar := newAnterooms(2) ar.EnqueueZettel(id.Zid(1)) - action, zid, lastReload := ar.Dequeue() - if zid != id.Zid(1) || action != arZettel || lastReload { - t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload) + action, zid, rno := ar.Dequeue() + if zid != id.Zid(1) || action != arZettel || rno != 1 { + t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno) } _, zid, _ = ar.Dequeue() if zid != id.Invalid { t.Errorf("Expected invalid Zid, but got %v", zid) } @@ -53,11 +50,11 @@ } } func TestReset(t *testing.T) { t.Parallel() - ar := newAnteroomQueue(1) + ar := newAnterooms(1) ar.EnqueueZettel(id.Zid(1)) ar.Reset() action, zid, _ := ar.Dequeue() if action != arReload || zid != id.Invalid { t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid) @@ -86,11 +83,11 @@ action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } - ar = newAnteroomQueue(1) + ar = newAnterooms(1) ar.Reload(id.NewSet(id.Zid(6))) action, zid, _ = ar.Dequeue() if zid != id.Zid(6) || action != arZettel { t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action) } @@ -97,13 +94,13 @@ action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } - ar = newAnteroomQueue(1) + ar = newAnterooms(1) ar.EnqueueZettel(id.Zid(8)) ar.Reload(nil) action, zid, _ = ar.Dequeue() if action != arNothing || zid != id.Invalid { t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid) } } Index: box/manager/box.go ================================================================== --- box/manager/box.go +++ box/manager/box.go @@ -1,189 +1,181 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( + "bytes" "context" "errors" - "strings" "zettelstore.de/z/box" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // Conatains all box.Box related functions // Location returns some information where the box is located. func (mgr *Manager) Location() string { if len(mgr.boxes) <= 2 { return "NONE" } - var sb strings.Builder - for i := range len(mgr.boxes) - 2 { + var buf bytes.Buffer + for i := 0; i < len(mgr.boxes)-2; i++ { if i > 0 { - sb.WriteString(", ") + buf.WriteString(", ") } - sb.WriteString(mgr.boxes[i].Location()) + buf.WriteString(mgr.boxes[i].Location()) } - return sb.String() + return buf.String() } // CanCreateZettel returns true, if box could possibly create a new zettel. func (mgr *Manager) CanCreateZettel(ctx context.Context) bool { - if mgr.State() != box.StartStateStarted { - return false - } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - return box.CanCreateZettel(ctx) - } - return false + return mgr.started && mgr.boxes[0].CanCreateZettel(ctx) } // CreateZettel creates a new zettel. -func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) { mgr.mgrLog.Debug().Msg("CreateZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return id.Invalid, box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) - zid, err := box.CreateZettel(ctx, zettel) - if err == nil { - mgr.idxUpdateZettel(ctx, zettel) - } - return zid, err - } - return id.Invalid, box.ErrReadOnly + return mgr.boxes[0].CreateZettel(ctx, zettel) } // GetZettel retrieves a specific zettel. -func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) { +func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel") - if mgr.State() != box.StartStateStarted { - return zettel.Zettel{}, box.ErrStopped - } mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() + if !mgr.started { + return domain.Zettel{}, box.ErrStopped + } for i, p := range mgr.boxes { - var errZNF box.ErrZettelNotFound - if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) { + if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound { if err == nil { mgr.Enrich(ctx, z.Meta, i+1) } return z, err } } - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} + return domain.Zettel{}, box.ErrNotFound } // GetAllZettel retrieves a specific zettel from all managed boxes. -func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) { +func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) { mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return nil, box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() - var result []zettel.Zettel + var result []domain.Zettel for i, p := range mgr.boxes { if z, err := p.GetZettel(ctx, zid); err == nil { mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } 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") + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + 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") + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + 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 { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return nil, box.ErrStopped } result := id.Set{} - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { - err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, func(id.Zid) bool { return true }) + err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true }) if err != nil { return nil, err } } return result, nil } -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, 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() - - 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 := map[id.Zid]*meta.Meta{} +type metaMap map[id.Zid]*meta.Meta + +// 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) { + if msg := mgr.mgrLog.Debug(); msg.Enabled() { + msg.Str("query", q.String()).Msg("SelectMeta") + } + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { + return nil, box.ErrStopped + } + + compSearch := q.RetrieveAndCompile(mgr) + selected := metaMap{} for _, term := range compSearch.Terms { rejected := id.Set{} handleMeta := func(m *meta.Meta) { zid := m.Zid - if rejected.ContainsOrNil(zid) { + if rejected.Contains(zid) { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected") return } if _, ok := selected[zid]; ok { mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected") @@ -191,67 +183,59 @@ } if compSearch.PreMatch(m) && term.Match(m) { selected[zid] = m mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match") } else { - rejected.Add(zid) + rejected.Zid(zid) mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject") } } for _, p := range mgr.boxes { - if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil { - return nil, err2 + if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil { + return nil, err } } } result := make([]*meta.Meta, 0, len(selected)) for _, m := range selected { result = append(result, m) } - result = compSearch.AfterSearch(result) - mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta") - return result, nil + return q.Sort(result), nil } // CanUpdateZettel returns true, if box could possibly update the given zettel. -func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool { - if mgr.State() != box.StartStateStarted { - return false - } +func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool { mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - return box.CanUpdateZettel(ctx, zettel) - } - return false - + return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel) } // UpdateZettel updates an existing zettel. -func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error { +func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error { mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return box.ErrStopped } - if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox { - zettel.Meta = mgr.cleanMetaProperties(zettel.Meta) - if err := box.UpdateZettel(ctx, zettel); err != nil { - return err - } - mgr.idxUpdateZettel(ctx, zettel) - return nil - } - return box.ErrReadOnly + // Remove all (computed) properties from metadata before storing the zettel. + zettel.Meta = zettel.Meta.Clone() + for _, p := range zettel.Meta.ComputedPairsRest() { + if mgr.propertyKeys.Has(p.Key) { + zettel.Meta.Delete(p.Key) + } + } + return mgr.boxes[0].UpdateZettel(ctx, zettel) } // AllowRenameZettel returns true, if box will not disallow renaming the zettel. func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return false } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if !p.AllowRenameZettel(ctx, zid) { return false } } @@ -259,36 +243,34 @@ } // RenameZettel changes the current zid to a new zid. func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error { mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for i, p := range mgr.boxes { err := p.RenameZettel(ctx, curZid, newZid) - var errZNF box.ErrZettelNotFound - if err != nil && !errors.As(err, &errZNF) { - for j := range i { + if err != nil && !errors.Is(err, box.ErrNotFound) { + for j := 0; j < i; j++ { mgr.boxes[j].RenameZettel(ctx, newZid, curZid) } return err } } - mgr.idxRenameZettel(ctx, curZid, newZid) return nil } // CanDeleteZettel returns true, if box could possibly delete the given zettel. func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool { - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return false } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { if p.CanDeleteZettel(ctx, zid) { return true } } @@ -296,34 +278,21 @@ } // DeleteZettel removes the zettel from the box. func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error { mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.RLock() + defer mgr.mgrMx.RUnlock() + if !mgr.started { return box.ErrStopped } - mgr.mgrMx.RLock() - defer mgr.mgrMx.RUnlock() for _, p := range mgr.boxes { err := p.DeleteZettel(ctx, zid) if err == nil { - mgr.idxDeleteZettel(ctx, zid) return nil } - var errZNF box.ErrZettelNotFound - if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) { + if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) { return err } } - return box.ErrZettelNotFound{Zid: zid} -} - -// Remove all (computed) properties from metadata before storing the zettel. -func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta { - result := m.Clone() - for _, p := range result.ComputedPairsRest() { - if mgr.propertyKeys.Has(p.Key) { - result.Delete(p.Key) - } - } - return result + return box.ErrNotFound } Index: box/manager/collect.go ================================================================== --- box/manager/collect.go +++ box/manager/collect.go @@ -1,27 +1,24 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "strings" "zettelstore.de/z/ast" "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain/id" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" ) type collectData struct { refs id.Set words store.WordSet @@ -77,8 +74,8 @@ } if !ref.IsZettel() { return } if zid, err := id.Parse(ref.URL.Path); err == nil { - data.refs.Add(zid) + data.refs.Zid(zid) } } Index: box/manager/enrich.go ================================================================== --- box/manager/enrich.go +++ box/manager/enrich.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( "context" "strconv" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/box" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // Enrich computes additional properties and updates the given metadata. func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) { @@ -35,13 +32,11 @@ // 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) - if boxNumber > 0 { - m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) - } + m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber)) mgr.idxStore.Enrich(ctx, m) } func computeCreated(zid id.Zid) string { if zid <= 10101000000 { @@ -64,33 +59,26 @@ if hours > 23 { hours = 23 } zid /= 100 day := zid % 100 - zid /= 100 - month := zid % 100 - year := zid / 100 - month, day = sanitizeMonthDay(year, month, day) - created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds - return created.String() -} - -func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) { if day < 1 { day = 1 } + zid /= 100 + month := zid % 100 if month < 1 { month = 1 } if month > 12 { month = 12 } - + year := zid / 100 switch month { case 1, 3, 5, 7, 8, 10, 12: if day > 31 { - day = 31 + day = 32 } case 4, 6, 9, 11: if day > 30 { day = 30 } @@ -103,11 +91,12 @@ if day > 29 { day = 29 } } } - return month, day + created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds + return created.String() } func computePublished(m *meta.Meta) { if _, ok := m.Get(api.KeyPublished); ok { return Index: box/manager/indexer.go ================================================================== --- box/manager/indexer.go +++ box/manager/indexer.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package manager import ( @@ -19,16 +16,16 @@ "net/url" "time" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // SearchEqual returns all zettel that contains the given exact word. // The word must be normalized through Unicode NKFD, trimmed and not empty. func (mgr *Manager) SearchEqual(word string) id.Set { @@ -76,12 +73,12 @@ // idxIndexer runs in the background and updates the index data structures. // This is the main service of the idxIndexer. func (mgr *Manager) idxIndexer() { // Something may panic. Ensure a running indexer. defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("Indexer", ri) + if r := recover(); r != nil { + kernel.Main.LogRecover("Indexer", r) go mgr.idxIndexer() } }() timerDuration := 15 * time.Second @@ -94,21 +91,25 @@ } } } func (mgr *Manager) idxWorkService(ctx context.Context) { + var roomNum uint64 var start time.Time for { - switch action, zid, lastReload := mgr.idxAr.Dequeue(); action { + switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action { case arNothing: return case arReload: mgr.idxLog.Debug().Msg("reload") + roomNum = 0 zids, err := mgr.FetchZids(ctx) if err == nil { start = time.Now() - mgr.idxAr.Reload(zids) + if rno := mgr.idxAr.Reload(zids); rno > 0 { + roomNum = rno + } mgr.idxMx.Lock() mgr.idxLastReload = time.Now().Local() mgr.idxSinceReload = 0 mgr.idxMx.Unlock() } @@ -116,21 +117,24 @@ mgr.idxLog.Debug().Zid(zid).Msg("zettel") zettel, err := mgr.GetZettel(ctx, zid) if err != nil { // Zettel was deleted or is not accessible b/c of other reasons mgr.idxLog.Trace().Zid(zid).Msg("delete") - mgr.idxDeleteZettel(ctx, zid) + mgr.idxMx.Lock() + mgr.idxSinceReload++ + mgr.idxMx.Unlock() + mgr.idxDeleteZettel(zid) continue } mgr.idxLog.Trace().Zid(zid).Msg("update") - mgr.idxUpdateZettel(ctx, zettel) mgr.idxMx.Lock() - if lastReload { + if arRoomNum == roomNum { mgr.idxDurReload = time.Since(start) } mgr.idxSinceReload++ mgr.idxMx.Unlock() + mgr.idxUpdateZettel(ctx, zettel) } } } func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool { @@ -151,17 +155,17 @@ return false } return true } -func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) { +func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) { var cData collectData cData.initialize() collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData) m := zettel.Meta - zi := store.NewZettelIndex(m) + zi := store.NewZettelIndex(m.Zid) mgr.idxCollectFromMeta(ctx, m, zi, &cData) mgr.idxProcessData(ctx, zi, &cData) toCheck := mgr.idxStore.UpdateReferences(ctx, zi) mgr.idxCheckZettel(toCheck) } @@ -185,34 +189,20 @@ case meta.TypeURL: if _, err := url.Parse(pair.Value); err == nil { cData.urls.Add(pair.Value) } default: - if descr.Type.IsSet { - for _, val := range meta.ListFromValue(pair.Value) { - idxCollectMetaValue(cData.words, val) - } - } else { - idxCollectMetaValue(cData.words, pair.Value) + for _, word := range strfun.NormalizeWords(pair.Value) { + cData.words.Add(word) } } } } -func idxCollectMetaValue(stWords store.WordSet, value string) { - if words := strfun.NormalizeWords(value); len(words) > 0 { - for _, word := range words { - stWords.Add(word) - } - } else { - stWords.Add(value) - } -} - func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) { for ref := range cData.refs { - if mgr.HasZettel(ctx, ref) { + if _, err := mgr.GetMeta(ctx, ref); err == nil { zi.AddBackRef(ref) } else { zi.AddDeadRef(ref) } } @@ -223,31 +213,26 @@ func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) { zid, err := id.Parse(value) if err != nil { return } - if !mgr.HasZettel(ctx, zid) { + if _, err = mgr.GetMeta(ctx, zid); err != nil { zi.AddDeadRef(zid) return } if inverseKey == "" { zi.AddBackRef(zid) return } - zi.AddInverseRef(inverseKey, zid) + zi.AddMetaRef(inverseKey, zid) } -func (mgr *Manager) idxRenameZettel(ctx context.Context, curZid, newZid id.Zid) { - toCheck := mgr.idxStore.RenameZettel(ctx, curZid, newZid) - mgr.idxCheckZettel(toCheck) -} - -func (mgr *Manager) idxDeleteZettel(ctx context.Context, zid id.Zid) { - toCheck := mgr.idxStore.DeleteZettel(ctx, zid) +func (mgr *Manager) idxDeleteZettel(zid id.Zid) { + toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid) mgr.idxCheckZettel(toCheck) } func (mgr *Manager) idxCheckZettel(s id.Set) { for zid := range s { mgr.idxAr.EnqueueZettel(zid) } } Index: box/manager/manager.go ================================================================== --- box/manager/manager.go +++ box/manager/manager.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package manager coordinates the various boxes and indexes of a Zettelstore. package manager @@ -19,20 +16,21 @@ "io" "net/url" "sync" "time" + "zettelstore.de/c/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/mapstore" + "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // ConnectData contains all administration related values. type ConnectData struct { Number int // number of the box, starting with 1. @@ -79,16 +77,18 @@ panic(scheme) } registry[scheme] = create } +// GetSchemes returns all registered scheme, ordered by scheme string. +func GetSchemes() []string { return maps.Keys(registry) } + // Manager is a coordinating box. type Manager struct { mgrLog *logger.Logger - stateMx sync.RWMutex - state box.StartState mgrMx sync.RWMutex + started bool rtConfig config.Config boxes []box.ManagedBox observers []box.UpdateFunc mxObserver sync.RWMutex done chan struct{} @@ -96,33 +96,20 @@ propertyKeys strfun.Set // Set of property key names // Indexer data idxLog *logger.Logger idxStore store.Store - idxAr *anteroomQueue + idxAr *anterooms idxReady chan struct{} // Signal a non-empty anteroom to background task // Indexer stats data idxMx sync.RWMutex idxLastReload time.Time idxDurReload time.Duration idxSinceReload uint64 } -func (mgr *Manager) setState(newState box.StartState) { - mgr.stateMx.Lock() - mgr.state = newState - mgr.stateMx.Unlock() -} - -func (mgr *Manager) State() box.StartState { - mgr.stateMx.RLock() - state := mgr.state - mgr.stateMx.RUnlock() - return state -} - // New creates a new managing box. func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) { descrs := meta.GetSortedKeyDescriptions() propertyKeys := make(strfun.Set, len(descrs)) for _, kd := range descrs { @@ -136,12 +123,12 @@ rtConfig: rtConfig, infos: make(chan box.UpdateInfo, len(boxURIs)*10), propertyKeys: propertyKeys, idxLog: boxLog.Clone().Str("box", "index").Child(), - idxStore: createIdxStore(rtConfig), - idxAr: newAnteroomQueue(1000), + idxStore: memstore.New(), + idxAr: newAnterooms(10), idxReady: make(chan struct{}, 1), } cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos} boxes := make([]box.ManagedBox, 0, len(boxURIs)+2) for _, uri := range boxURIs { @@ -167,14 +154,10 @@ boxes = append(boxes, constbox, compbox) mgr.boxes = boxes return mgr, nil } -func createIdxStore(_ config.Config) store.Store { - return mapstore.New() -} - // RegisterObserver registers an observer that will be notified // if a zettel was found to be changed. func (mgr *Manager) RegisterObserver(f box.UpdateFunc) { if f != nil { mgr.mxObserver.Lock() @@ -184,12 +167,12 @@ } func (mgr *Manager) notifier() { // The call to notify may panic. Ensure a running notifier. defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("Notifier", ri) + if r := recover(); r != nil { + kernel.Main.LogRecover("Notifier", r) go mgr.notifier() } }() tsLastEvent := time.Now() @@ -210,18 +193,15 @@ mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier") if ignoreUpdate(cache, now, reason, zid) { mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored") continue } - mgr.idxEnqueue(reason, zid) if ci.Box == nil { ci.Box = mgr } - if mgr.State() == box.StartStateStarted { - mgr.notifyObserver(&ci) - } + mgr.notifyObserver(&ci) } case <-mgr.done: return } } @@ -246,18 +226,15 @@ return false } func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) { switch reason { - case box.OnReady: - return case box.OnReload: mgr.idxAr.Reset() case box.OnZettel: mgr.idxAr.EnqueueZettel(zid) default: - mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason") return } select { case mgr.idxReady <- struct{}{}: default: @@ -275,113 +252,74 @@ // Start the box. Now all other functions of the box are allowed. // Starting an already started box is not allowed. func (mgr *Manager) Start(ctx context.Context) error { mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() - if mgr.State() != box.StartStateStopped { + if mgr.started { + mgr.mgrMx.Unlock() return box.ErrStarted } - mgr.setState(box.StartStateStarting) for i := len(mgr.boxes) - 1; i >= 0; i-- { ssi, ok := mgr.boxes[i].(box.StartStopper) if !ok { continue } err := ssi.Start(ctx) if err == nil { continue } - mgr.setState(box.StartStateStopping) for j := i + 1; j < len(mgr.boxes); j++ { if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 { ssj.Stop(ctx) } } - mgr.setState(box.StartStateStopped) + mgr.mgrMx.Unlock() return err } mgr.idxAr.Reset() // Ensure an initial index run mgr.done = make(chan struct{}) go mgr.notifier() - - mgr.waitBoxesAreStarted() - mgr.setState(box.StartStateStarted) - mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady}) - go mgr.idxIndexer() - return nil -} - -func (mgr *Manager) waitBoxesAreStarted() { - const waitTime = 10 * time.Millisecond - const waitLoop = int(1 * time.Second / waitTime) - for i := 1; !mgr.allBoxesStarted(); i++ { - if i%waitLoop == 0 { - if time.Duration(i)*waitTime > time.Minute { - mgr.mgrLog.Info().Msg("Waiting for more than one minute to start") - } else { - mgr.mgrLog.Trace().Msg("Wait for boxes to start") - } - } - time.Sleep(waitTime) - } -} - -func (mgr *Manager) allBoxesStarted() bool { - for _, bx := range mgr.boxes { - if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted { - return false - } - } - return true + + mgr.started = true + mgr.mgrMx.Unlock() + return nil } // Stop the started box. Now only the Start() function is allowed. func (mgr *Manager) Stop(ctx context.Context) { mgr.mgrMx.Lock() defer mgr.mgrMx.Unlock() - if mgr.State() != box.StartStateStarted { + if !mgr.started { return } - mgr.setState(box.StartStateStopping) close(mgr.done) for _, p := range mgr.boxes { if ss, ok := p.(box.StartStopper); ok { ss.Stop(ctx) } } - mgr.setState(box.StartStateStopped) + mgr.started = false } // Refresh internal box data. func (mgr *Manager) Refresh(ctx context.Context) error { mgr.mgrLog.Debug().Msg("Refresh") - if mgr.State() != box.StartStateStarted { + mgr.mgrMx.Lock() + defer mgr.mgrMx.Unlock() + if !mgr.started { return box.ErrStopped } mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid} - mgr.mgrMx.Lock() - defer mgr.mgrMx.Unlock() for _, bx := range mgr.boxes { if rb, ok := bx.(box.Refresher); ok { rb.Refresh(ctx) } } return nil } -// ReIndex data of the given zettel. -func (mgr *Manager) ReIndex(_ context.Context, zid id.Zid) error { - mgr.mgrLog.Debug().Msg("ReIndex") - if mgr.State() != box.StartStateStarted { - return box.ErrStopped - } - mgr.infos <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid} - return nil -} - // ReadStats populates st with box statistics. func (mgr *Manager) ReadStats(st *box.Stats) { mgr.mgrLog.Debug().Msg("ReadStats") mgr.mgrMx.RLock() defer mgr.mgrMx.RUnlock() DELETED box/manager/mapstore/mapstore.go Index: box/manager/mapstore/mapstore.go ================================================================== --- box/manager/mapstore/mapstore.go +++ box/manager/mapstore/mapstore.go @@ -1,717 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -// Package mapstore stored the index in main memory via a Go map. -package mapstore - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/maps" - "zettelstore.de/z/box" - "zettelstore.de/z/box/manager/store" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) - -type zettelData struct { - meta *meta.Meta // a local copy of the metadata, without computed keys - dead id.Slice // list of dead references in this zettel - forward id.Slice // list of forward references in this zettel - backward id.Slice // list of zettel that reference with zettel - otherRefs map[string]bidiRefs - words []string // list of words of this zettel - urls []string // list of urls of this zettel -} - -type bidiRefs struct { - forward id.Slice - backward id.Slice -} - -type stringRefs map[string]id.Slice - -type memStore struct { - 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{ - 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 && zi.meta != nil { - // zi.meta is nil, if zettel was referenced, but is not indexed yet. - return zi.meta.Clone(), nil - } - return nil, box.ErrZettelNotFound{Zid: zid} -} - -func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { - if ms.doEnrich(m) { - ms.mxStats.Lock() - ms.updates++ - ms.mxStats.Unlock() - } -} - -func (ms *memStore) doEnrich(m *meta.Meta) bool { - ms.mx.RLock() - defer ms.mx.RUnlock() - zi, ok := ms.idx[m.Zid] - if !ok { - return false - } - var updated bool - if len(zi.dead) > 0 { - m.Set(api.KeyDead, zi.dead.String()) - updated = true - } - back := removeOtherMetaRefs(m, zi.backward.Clone()) - if len(zi.backward) > 0 { - m.Set(api.KeyBackward, zi.backward.String()) - updated = true - } - if len(zi.forward) > 0 { - m.Set(api.KeyForward, zi.forward.String()) - back = remRefs(back, zi.forward) - updated = true - } - for k, refs := range zi.otherRefs { - if len(refs.backward) > 0 { - m.Set(k, refs.backward.String()) - back = remRefs(back, refs.backward) - updated = true - } - } - if len(back) > 0 { - m.Set(api.KeyBack, back.String()) - updated = true - } - return updated -} - -// SearchEqual returns all zettel that contains the given exact word. -// The word must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchEqual(word string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := id.NewSet() - if refs, ok := ms.words[word]; ok { - result.CopySlice(refs) - } - if refs, ok := ms.urls[word]; ok { - result.CopySlice(refs) - } - zid, err := id.Parse(word) - if err != nil { - return result - } - zi, ok := ms.idx[zid] - if !ok { - return result - } - - addBackwardZids(result, zid, zi) - return result -} - -// SearchPrefix returns all zettel that have a word with the given prefix. -// The prefix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchPrefix(prefix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(prefix, strings.HasPrefix) - l := len(prefix) - if l > 14 { - return result - } - maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) - if err != nil { - return result - } - var minZid id.Zid - if l < 14 && prefix == "0000000000000"[:l] { - minZid = id.Zid(1) - } else { - minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) - if err != nil { - return result - } - } - for zid, zi := range ms.idx { - if minZid <= zid && zid <= maxZid { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchSuffix returns all zettel that have a word with the given suffix. -// The suffix must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchSuffix(suffix string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(suffix, strings.HasSuffix) - l := len(suffix) - if l > 14 { - return result - } - val, err := id.ParseUint(suffix) - if err != nil { - return result - } - modulo := uint64(1) - for range l { - modulo *= 10 - } - for zid, zi := range ms.idx { - if uint64(zid)%modulo == val { - addBackwardZids(result, zid, zi) - } - } - return result -} - -// SearchContains returns all zettel that contains the given string. -// The string must be normalized through Unicode NKFD, trimmed and not empty. -func (ms *memStore) SearchContains(s string) id.Set { - ms.mx.RLock() - defer ms.mx.RUnlock() - result := ms.selectWithPred(s, strings.Contains) - if len(s) > 14 { - return result - } - if _, err := id.ParseUint(s); err != nil { - return result - } - for zid, zi := range ms.idx { - if strings.Contains(zid.String(), s) { - addBackwardZids(result, zid, zi) - } - } - return result -} - -func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { - // Must only be called if ms.mx is read-locked! - result := id.NewSet() - for word, refs := range ms.words { - if !pred(word, s) { - continue - } - result.CopySlice(refs) - } - for u, refs := range ms.urls { - if !pred(u, s) { - continue - } - result.CopySlice(refs) - } - return result -} - -func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) { - // Must only be called if ms.mx is read-locked! - result.Add(zid) - result.CopySlice(zi.backward) - for _, mref := range zi.otherRefs { - result.CopySlice(mref.backward) - } -} - -func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { - for _, p := range m.PairsRest() { - switch meta.Type(p.Key) { - case meta.TypeID: - if zid, err := id.Parse(p.Value); err == nil { - back = remRef(back, zid) - } - case meta.TypeIDSet: - for _, val := range meta.ListFromValue(p.Value) { - if zid, err := id.Parse(val); err == nil { - back = remRef(back, zid) - } - } - } - } - return back -} - -func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - m := ms.makeMeta(zidx) - zi, ziExist := ms.idx[zidx.Zid] - if !ziExist || zi == nil { - zi = &zettelData{} - ziExist = false - } - - // Is this zettel an old dead reference mentioned in other zettel? - var toCheck id.Set - if refs, ok := ms.dead[zidx.Zid]; ok { - // These must be checked later again - toCheck = id.NewSet(refs...) - delete(ms.dead, zidx.Zid) - } - - zi.meta = m - ms.updateDeadReferences(zidx, zi) - ids := ms.updateForwardBackwardReferences(zidx, zi) - toCheck = toCheck.Copy(ids) - ids = ms.updateMetadataReferences(zidx, zi) - toCheck = toCheck.Copy(ids) - zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords()) - zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls()) - - // Check if zi must be inserted into ms.idx - if !ziExist { - ms.idx[zidx.Zid] = zi - } - - return toCheck -} - -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, meta.SuffixKeyRole) -} - -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 { - ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) - } - for _, ref := range newRefs { - ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) - } -} - -func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - brefs := zidx.GetBackRefs() - newRefs, remRefs := refsDiff(brefs, zi.forward) - zi.forward = brefs - - var toCheck id.Set - for _, ref := range remRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = remRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - for _, ref := range newRefs { - bzi := ms.getOrCreateEntry(ref) - bzi.backward = addRef(bzi.backward, zidx.Zid) - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - return toCheck -} - -func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - inverseRefs := zidx.GetInverseRefs() - for key, mr := range zi.otherRefs { - if _, ok := inverseRefs[key]; ok { - continue - } - ms.removeInverseMeta(zidx.Zid, key, mr.forward) - } - if zi.otherRefs == nil { - zi.otherRefs = make(map[string]bidiRefs) - } - var toCheck id.Set - for key, mrefs := range inverseRefs { - mr := zi.otherRefs[key] - newRefs, remRefs := refsDiff(mrefs, mr.forward) - mr.forward = mrefs - zi.otherRefs[key] = mr - - for _, ref := range newRefs { - bzi := ms.getOrCreateEntry(ref) - if bzi.otherRefs == nil { - bzi.otherRefs = make(map[string]bidiRefs) - } - bmr := bzi.otherRefs[key] - bmr.backward = addRef(bmr.backward, zidx.Zid) - bzi.otherRefs[key] = bmr - if bzi.meta == nil { - toCheck = toCheck.Add(ref) - } - } - ms.removeInverseMeta(zidx.Zid, key, remRefs) - } - return toCheck -} - -func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string { - newWords, removeWords := next.Diff(prev) - for _, word := range newWords { - if refs, ok := srefs[word]; ok { - srefs[word] = addRef(refs, zid) - continue - } - srefs[word] = id.Slice{zid} - } - for _, word := range removeWords { - refs, ok := srefs[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(srefs, word) - continue - } - srefs[word] = refs2 - } - return next.Words() -} - -func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData { - // Must only be called if ms.mx is write-locked! - if zi, ok := ms.idx[zid]; ok { - return zi - } - zi := &zettelData{} - ms.idx[zid] = zi - return zi -} - -func (ms *memStore) RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - - curZi, curFound := ms.idx[curZid] - _, newFound := ms.idx[newZid] - if !curFound || newFound { - return nil - } - newZi := &zettelData{ - meta: copyMeta(curZi.meta, newZid), - dead: ms.copyDeadReferences(curZi.dead), - forward: ms.copyForward(curZi.forward, newZid), - backward: nil, // will be done through tocheck - otherRefs: nil, // TODO: check if this will be done through toCheck - words: copyStrings(ms.words, curZi.words, newZid), - urls: copyStrings(ms.urls, curZi.urls, newZid), - } - - ms.idx[newZid] = newZi - toCheck := ms.doDeleteZettel(curZid) - toCheck = toCheck.CopySlice(ms.dead[newZid]) - delete(ms.dead, newZid) - toCheck = toCheck.Add(newZid) // should update otherRefs - return toCheck -} -func copyMeta(m *meta.Meta, newZid id.Zid) *meta.Meta { - result := m.Clone() - result.Zid = newZid - return result -} -func (ms *memStore) copyDeadReferences(curDead id.Slice) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curDead); l > 0 { - result := make(id.Slice, l) - for i, ref := range curDead { - result[i] = ref - ms.dead[ref] = addRef(ms.dead[ref], ref) - } - return result - } - return nil -} -func (ms *memStore) copyForward(curForward id.Slice, newZid id.Zid) id.Slice { - // Must only be called if ms.mx is write-locked! - if l := len(curForward); l > 0 { - result := make(id.Slice, l) - for i, ref := range curForward { - result[i] = ref - if fzi, found := ms.idx[ref]; found { - fzi.backward = addRef(fzi.backward, newZid) - } - } - return result - } - return nil -} -func copyStrings(msStringMap stringRefs, curStrings []string, newZid id.Zid) []string { - // Must only be called if ms.mx is write-locked! - if l := len(curStrings); l > 0 { - result := make([]string, l) - for i, s := range curStrings { - result[i] = s - msStringMap[s] = addRef(msStringMap[s], newZid) - } - return result - } - return nil -} - -func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { - ms.mx.Lock() - defer ms.mx.Unlock() - return ms.doDeleteZettel(zid) -} - -func (ms *memStore) doDeleteZettel(zid id.Zid) id.Set { - // Must only be called if ms.mx is write-locked! - zi, ok := ms.idx[zid] - if !ok { - return nil - } - - ms.deleteDeadSources(zid, zi) - toCheck := ms.deleteForwardBackward(zid, zi) - for key, mrefs := range zi.otherRefs { - ms.removeInverseMeta(zid, key, mrefs.forward) - } - deleteStrings(ms.words, zi.words, zid) - deleteStrings(ms.urls, zi.urls, zid) - delete(ms.idx, zid) - return toCheck -} - -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 { - ms.dead[ref] = drefs - } else { - delete(ms.dead, ref) - } - } - } -} - -func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelData) id.Set { - // Must only be called if ms.mx is write-locked! - for _, ref := range zi.forward { - if fzi, ok := ms.idx[ref]; ok { - fzi.backward = remRef(fzi.backward, zid) - } - } - var toCheck id.Set - for _, ref := range zi.backward { - if bzi, ok := ms.idx[ref]; ok { - bzi.forward = remRef(bzi.forward, zid) - toCheck = toCheck.Add(ref) - } - } - return toCheck -} - -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.otherRefs == nil { - continue - } - bmr, ok := bzi.otherRefs[key] - if !ok { - continue - } - bmr.backward = remRef(bmr.backward, zid) - if len(bmr.backward) > 0 || len(bmr.forward) > 0 { - bzi.otherRefs[key] = bmr - } else { - delete(bzi.otherRefs, key) - if len(bzi.otherRefs) == 0 { - bzi.otherRefs = nil - } - } - } -} - -func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) { - // Must only be called if ms.mx is write-locked! - for _, word := range curStrings { - refs, ok := msStringMap[word] - if !ok { - continue - } - refs2 := remRef(refs, zid) - if len(refs2) == 0 { - delete(msStringMap, word) - continue - } - msStringMap[word] = refs2 - } -} - -func (ms *memStore) ReadStats(st *store.Stats) { - ms.mx.RLock() - st.Zettel = len(ms.idx) - 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() - - io.WriteString(w, "=== Dump\n") - ms.dumpIndex(w) - ms.dumpDead(w) - dumpStringRefs(w, "Words", "", "", ms.words) - dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) -} - -func (ms *memStore) dumpIndex(w io.Writer) { - if len(ms.idx) == 0 { - return - } - io.WriteString(w, "==== Zettel Index\n") - zids := make(id.Slice, 0, len(ms.idx)) - for id := range ms.idx { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, "=====", id) - zi := ms.idx[id] - 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.otherRefs { - fmt.Fprintln(w, "* Meta", k) - dumpZids(w, "** Forward:", fb.forward) - dumpZids(w, "** Backward:", fb.backward) - } - dumpStrings(w, "* Words", "", "", zi.words) - dumpStrings(w, "* URLs", "[[", "]]", zi.urls) - } -} - -func (ms *memStore) dumpDead(w io.Writer) { - if len(ms.dead) == 0 { - return - } - fmt.Fprintf(w, "==== Dead References\n") - zids := make(id.Slice, 0, len(ms.dead)) - for id := range ms.dead { - zids = append(zids, id) - } - zids.Sort() - for _, id := range zids { - fmt.Fprintln(w, ";", id) - fmt.Fprintln(w, ":", ms.dead[id]) - } -} - -func dumpZids(w io.Writer, prefix string, zids id.Slice) { - if len(zids) > 0 { - io.WriteString(w, prefix) - for _, zid := range zids { - io.WriteString(w, " ") - w.Write(zid.Bytes()) - } - fmt.Fprintln(w) - } -} - -func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { - if len(slice) > 0 { - sl := make([]string, len(slice)) - copy(sl, slice) - sort.Strings(sl) - fmt.Fprintln(w, title) - for _, s := range sl { - fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) - } - } - -} - -func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { - if len(srefs) == 0 { - return - } - fmt.Fprintln(w, "====", title) - for _, s := range maps.Keys(srefs) { - fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) - fmt.Fprintln(w, ":", srefs[s]) - } -} DELETED box/manager/mapstore/refs.go Index: box/manager/mapstore/refs.go ================================================================== --- box/manager/mapstore/refs.go +++ box/manager/mapstore/refs.go @@ -1,105 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package mapstore - -import ( - "slices" - - "zettelstore.de/z/zettel/id" -) - -func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { - npos, opos := 0, 0 - for npos < len(refsN) && opos < len(refsO) { - rn, ro := refsN[npos], refsO[opos] - if rn == ro { - npos++ - opos++ - continue - } - if rn < ro { - newRefs = append(newRefs, rn) - npos++ - continue - } - remRefs = append(remRefs, ro) - opos++ - } - if npos < len(refsN) { - newRefs = append(newRefs, refsN[npos:]...) - } - if opos < len(refsO) { - remRefs = append(remRefs, refsO[opos:]...) - } - return newRefs, remRefs -} - -func addRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - refs = slices.Insert(refs, hi, ref) - return refs -} - -func remRefs(refs, rem id.Slice) id.Slice { - if len(refs) == 0 || len(rem) == 0 { - return refs - } - result := make(id.Slice, 0, len(refs)) - rpos, dpos := 0, 0 - for rpos < len(refs) && dpos < len(rem) { - rr, dr := refs[rpos], rem[dpos] - if rr < dr { - result = append(result, rr) - rpos++ - continue - } - if dr < rr { - dpos++ - continue - } - rpos++ - dpos++ - } - if rpos < len(refs) { - result = append(result, refs[rpos:]...) - } - return result -} - -func remRef(refs id.Slice, ref id.Zid) id.Slice { - hi := len(refs) - for lo := 0; lo < hi; { - m := lo + (hi-lo)/2 - if r := refs[m]; r == ref { - copy(refs[m:], refs[m+1:]) - refs = refs[:len(refs)-1] - return refs - } else if r < ref { - lo = m + 1 - } else { - hi = m - } - } - return refs -} DELETED box/manager/mapstore/refs_test.go Index: box/manager/mapstore/refs_test.go ================================================================== --- box/manager/mapstore/refs_test.go +++ box/manager/mapstore/refs_test.go @@ -1,140 +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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern -//----------------------------------------------------------------------------- - -package mapstore - -import ( - "testing" - - "zettelstore.de/z/zettel/id" -) - -func assertRefs(t *testing.T, i int, got, exp id.Slice) { - t.Helper() - if got == nil && exp != nil { - t.Errorf("%d: got nil, but expected %v", i, exp) - return - } - if got != nil && exp == nil { - t.Errorf("%d: expected nil, but got %v", i, got) - return - } - if len(got) != len(exp) { - t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) - return - } - for p, n := range exp { - if got := got[p]; got != id.Zid(n) { - t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) - } - } -} - -func TestRefsDiff(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp1, exp2 id.Slice - }{ - {nil, nil, nil, nil}, - {id.Slice{1}, nil, id.Slice{1}, nil}, - {nil, id.Slice{1}, nil, id.Slice{1}}, - {id.Slice{1}, id.Slice{1}, nil, nil}, - {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, - {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, - {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, - } - for i, tc := range testcases { - got1, got2 := refsDiff(tc.in1, tc.in2) - assertRefs(t, i, got1, tc.exp1) - assertRefs(t, i, got2, tc.exp2) - } -} - -func TestAddRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, id.Slice{5}}, - {id.Slice{1}, 5, id.Slice{1, 5}}, - {id.Slice{10}, 5, id.Slice{5, 10}}, - {id.Slice{5}, 5, id.Slice{5}}, - {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, - } - for i, tc := range testcases { - got := addRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRefs(t *testing.T) { - t.Parallel() - testcases := []struct { - in1, in2 id.Slice - exp id.Slice - }{ - {nil, nil, nil}, - {nil, id.Slice{}, nil}, - {id.Slice{}, nil, id.Slice{}}, - {id.Slice{}, id.Slice{}, id.Slice{}}, - {id.Slice{1}, id.Slice{5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, - {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, - {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, - {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, - {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRefs(tc.in1, tc.in2) - assertRefs(t, i, got, tc.exp) - } -} - -func TestRemRef(t *testing.T) { - t.Parallel() - testcases := []struct { - ref id.Slice - zid uint - exp id.Slice - }{ - {nil, 5, nil}, - {id.Slice{}, 5, id.Slice{}}, - {id.Slice{5}, 5, id.Slice{}}, - {id.Slice{1}, 5, id.Slice{1}}, - {id.Slice{10}, 5, id.Slice{10}}, - {id.Slice{1, 5}, 5, id.Slice{1}}, - {id.Slice{5, 10}, 5, id.Slice{10}}, - {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, - } - for i, tc := range testcases { - got := remRef(tc.ref, id.Zid(tc.zid)) - assertRefs(t, i, got, tc.exp) - } -} ADDED box/manager/memstore/memstore.go Index: box/manager/memstore/memstore.go ================================================================== --- box/manager/memstore/memstore.go +++ box/manager/memstore/memstore.go @@ -0,0 +1,580 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-2022 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 memstore stored the index in main memory. +package memstore + +import ( + "context" + "fmt" + "io" + "sort" + "strings" + "sync" + + "zettelstore.de/c/api" + "zettelstore.de/c/maps" + "zettelstore.de/z/box/manager/store" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +type metaRefs 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 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 + + // Stats + 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), + } +} + +func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) { + if ms.doEnrich(m) { + ms.mx.Lock() + ms.updates++ + ms.mx.Unlock() + } +} + +func (ms *memStore) doEnrich(m *meta.Meta) bool { + ms.mx.RLock() + defer ms.mx.RUnlock() + zi, ok := ms.idx[m.Zid] + if !ok { + return false + } + var updated bool + if len(zi.dead) > 0 { + m.Set(api.KeyDead, zi.dead.String()) + updated = true + } + back := removeOtherMetaRefs(m, zi.backward.Copy()) + if len(zi.backward) > 0 { + m.Set(api.KeyBackward, zi.backward.String()) + updated = true + } + 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 { + if len(refs.backward) > 0 { + m.Set(k, refs.backward.String()) + back = remRefs(back, refs.backward) + updated = true + } + } + if len(back) > 0 { + m.Set(api.KeyBack, back.String()) + updated = true + } + return updated +} + +// SearchEqual returns all zettel that contains the given exact word. +// The word must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchEqual(word string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := id.NewSet() + if refs, ok := ms.words[word]; ok { + result.AddSlice(refs) + } + if refs, ok := ms.urls[word]; ok { + result.AddSlice(refs) + } + zid, err := id.Parse(word) + if err != nil { + return result + } + zi, ok := ms.idx[zid] + if !ok { + return result + } + + addBackwardZids(result, zid, zi) + return result +} + +// SearchPrefix returns all zettel that have a word with the given prefix. +// The prefix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchPrefix(prefix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(prefix, strings.HasPrefix) + l := len(prefix) + if l > 14 { + return result + } + maxZid, err := id.Parse(prefix + "99999999999999"[:14-l]) + if err != nil { + return result + } + var minZid id.Zid + if l < 14 && prefix == "0000000000000"[:l] { + minZid = id.Zid(1) + } else { + minZid, err = id.Parse(prefix + "00000000000000"[:14-l]) + if err != nil { + return result + } + } + for zid, zi := range ms.idx { + if minZid <= zid && zid <= maxZid { + addBackwardZids(result, zid, zi) + } + } + return result +} + +// SearchSuffix returns all zettel that have a word with the given suffix. +// The suffix must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchSuffix(suffix string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(suffix, strings.HasSuffix) + l := len(suffix) + if l > 14 { + return result + } + val, err := id.ParseUint(suffix) + if err != nil { + return result + } + modulo := uint64(1) + for i := 0; i < l; i++ { + modulo *= 10 + } + for zid, zi := range ms.idx { + if uint64(zid)%modulo == val { + addBackwardZids(result, zid, zi) + } + } + return result +} + +// SearchContains returns all zettel that contains the given string. +// The string must be normalized through Unicode NKFD, trimmed and not empty. +func (ms *memStore) SearchContains(s string) id.Set { + ms.mx.RLock() + defer ms.mx.RUnlock() + result := ms.selectWithPred(s, strings.Contains) + if len(s) > 14 { + return result + } + if _, err := id.ParseUint(s); err != nil { + return result + } + for zid, zi := range ms.idx { + if strings.Contains(zid.String(), s) { + addBackwardZids(result, zid, zi) + } + } + return result +} + +func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set { + // Must only be called if ms.mx is read-locked! + result := id.NewSet() + for word, refs := range ms.words { + if !pred(word, s) { + continue + } + result.AddSlice(refs) + } + for u, refs := range ms.urls { + if !pred(u, s) { + continue + } + result.AddSlice(refs) + } + return result +} + +func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) { + // Must only be called if ms.mx is read-locked! + result.Zid(zid) + result.AddSlice(zi.backward) + for _, mref := range zi.meta { + result.AddSlice(mref.backward) + } +} + +func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice { + for _, p := range m.PairsRest() { + switch meta.Type(p.Key) { + case meta.TypeID: + if zid, err := id.Parse(p.Value); err == nil { + back = remRef(back, zid) + } + case meta.TypeIDSet: + for _, val := range meta.ListFromValue(p.Value) { + if zid, err := id.Parse(val); err == nil { + back = remRef(back, zid) + } + } + } + } + return back +} + +func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + zi, ziExist := ms.idx[zidx.Zid] + if !ziExist || zi == nil { + zi = &zettelIndex{} + ziExist = false + } + + // Is this zettel an old dead reference mentioned in other zettel? + var toCheck id.Set + if refs, ok := ms.dead[zidx.Zid]; ok { + // These must be checked later again + toCheck = id.NewSet(refs...) + delete(ms.dead, zidx.Zid) + } + + 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() { + ms.idx[zidx.Zid] = zi + } + + return toCheck +} + +func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) { + // 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 { + ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid) + } + for _, ref := range newRefs { + ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid) + } +} + +func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) { + // 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 { + bzi := ms.getEntry(ref) + bzi.backward = remRef(bzi.backward, zidx.Zid) + } + for _, ref := range newRefs { + bzi := ms.getEntry(ref) + bzi.backward = addRef(bzi.backward, zidx.Zid) + } +} + +func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) { + // Must only be called if ms.mx is write-locked! + metarefs := zidx.GetMetaRefs() + for key, mr := range zi.meta { + if _, ok := metarefs[key]; ok { + continue + } + ms.removeInverseMeta(zidx.Zid, key, mr.forward) + } + if zi.meta == nil { + zi.meta = make(map[string]metaRefs) + } + for key, mrefs := range metarefs { + mr := zi.meta[key] + newRefs, remRefs := refsDiff(mrefs, mr.forward) + mr.forward = mrefs + zi.meta[key] = mr + + for _, ref := range newRefs { + bzi := ms.getEntry(ref) + if bzi.meta == nil { + bzi.meta = make(map[string]metaRefs) + } + bmr := bzi.meta[key] + bmr.backward = addRef(bmr.backward, zidx.Zid) + bzi.meta[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 + } + srefs[word] = id.Slice{zid} + } + for _, word := range removeWords { + refs, ok := srefs[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(srefs, word) + continue + } + srefs[word] = refs2 + } + return next.Words() +} + +func (ms *memStore) getEntry(zid id.Zid) *zettelIndex { + // Must only be called if ms.mx is write-locked! + if zi, ok := ms.idx[zid]; ok { + return zi + } + zi := &zettelIndex{} + ms.idx[zid] = zi + return zi +} + +func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set { + ms.mx.Lock() + defer ms.mx.Unlock() + + zi, ok := ms.idx[zid] + if !ok { + return nil + } + + ms.deleteDeadSources(zid, zi) + toCheck := ms.deleteForwardBackward(zid, zi) + if len(zi.meta) > 0 { + for key, mrefs := range zi.meta { + 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) { + // 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 { + ms.dead[ref] = drefs + } else { + delete(ms.dead, ref) + } + } + } +} + +func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) 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) + } + } + for _, ref := range zi.backward { + if bzi, ok := ms.idx[ref]; ok { + bzi.forward = remRef(bzi.forward, zid) + if toCheck == nil { + toCheck = id.NewSet() + } + toCheck.Zid(ref) + } + } + return toCheck +} + +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 { + continue + } + bmr, ok := bzi.meta[key] + if !ok { + continue + } + bmr.backward = remRef(bmr.backward, zid) + if len(bmr.backward) > 0 || len(bmr.forward) > 0 { + bzi.meta[key] = bmr + } else { + delete(bzi.meta, key) + if len(bzi.meta) == 0 { + bzi.meta = nil + } + } + } +} + +func (ms *memStore) deleteWords(zid id.Zid, words []string) { + // Must only be called if ms.mx is write-locked! + for _, word := range words { + refs, ok := ms.words[word] + if !ok { + continue + } + refs2 := remRef(refs, zid) + if len(refs2) == 0 { + delete(ms.words, word) + continue + } + ms.words[word] = refs2 + } +} + +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() +} + +func (ms *memStore) Dump(w io.Writer) { + ms.mx.RLock() + defer ms.mx.RUnlock() + + io.WriteString(w, "=== Dump\n") + ms.dumpIndex(w) + ms.dumpDead(w) + dumpStringRefs(w, "Words", "", "", ms.words) + dumpStringRefs(w, "URLs", "[[", "]]", ms.urls) +} + +func (ms *memStore) dumpIndex(w io.Writer) { + if len(ms.idx) == 0 { + return + } + io.WriteString(w, "==== Zettel Index\n") + zids := make(id.Slice, 0, len(ms.idx)) + for id := range ms.idx { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, "=====", id) + zi := ms.idx[id] + 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 { + fmt.Fprintln(w, "* Meta", k) + dumpZids(w, "** Forward:", fb.forward) + dumpZids(w, "** Backward:", fb.backward) + } + dumpStrings(w, "* Words", "", "", zi.words) + dumpStrings(w, "* URLs", "[[", "]]", zi.urls) + } +} + +func (ms *memStore) dumpDead(w io.Writer) { + if len(ms.dead) == 0 { + return + } + fmt.Fprintf(w, "==== Dead References\n") + zids := make(id.Slice, 0, len(ms.dead)) + for id := range ms.dead { + zids = append(zids, id) + } + zids.Sort() + for _, id := range zids { + fmt.Fprintln(w, ";", id) + fmt.Fprintln(w, ":", ms.dead[id]) + } +} + +func dumpZids(w io.Writer, prefix string, zids id.Slice) { + if len(zids) > 0 { + io.WriteString(w, prefix) + for _, zid := range zids { + io.WriteString(w, " ") + w.Write(zid.Bytes()) + } + fmt.Fprintln(w) + } +} + +func dumpStrings(w io.Writer, title, preString, postString string, slice []string) { + if len(slice) > 0 { + sl := make([]string, len(slice)) + copy(sl, slice) + sort.Strings(sl) + fmt.Fprintln(w, title) + for _, s := range sl { + fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString) + } + } + +} + +func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) { + if len(srefs) == 0 { + return + } + fmt.Fprintln(w, "====", title) + for _, s := range maps.Keys(srefs) { + fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString) + fmt.Fprintln(w, ":", srefs[s]) + } +} ADDED box/manager/memstore/refs.go Index: box/manager/memstore/refs.go ================================================================== --- box/manager/memstore/refs.go +++ box/manager/memstore/refs.go @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 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 memstore + +import "zettelstore.de/z/domain/id" + +func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) { + npos, opos := 0, 0 + for npos < len(refsN) && opos < len(refsO) { + rn, ro := refsN[npos], refsO[opos] + if rn == ro { + npos++ + opos++ + continue + } + if rn < ro { + newRefs = append(newRefs, rn) + npos++ + continue + } + remRefs = append(remRefs, ro) + opos++ + } + if npos < len(refsN) { + newRefs = append(newRefs, refsN[npos:]...) + } + if opos < len(refsO) { + remRefs = append(remRefs, refsO[opos:]...) + } + return newRefs, remRefs +} + +func addRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + refs = append(refs, id.Invalid) + copy(refs[hi+1:], refs[hi:]) + refs[hi] = ref + return refs +} + +func remRefs(refs, rem id.Slice) id.Slice { + if len(refs) == 0 || len(rem) == 0 { + return refs + } + result := make(id.Slice, 0, len(refs)) + rpos, dpos := 0, 0 + for rpos < len(refs) && dpos < len(rem) { + rr, dr := refs[rpos], rem[dpos] + if rr < dr { + result = append(result, rr) + rpos++ + continue + } + if dr < rr { + dpos++ + continue + } + rpos++ + dpos++ + } + if rpos < len(refs) { + result = append(result, refs[rpos:]...) + } + return result +} + +func remRef(refs id.Slice, ref id.Zid) id.Slice { + hi := len(refs) + for lo := 0; lo < hi; { + m := lo + (hi-lo)/2 + if r := refs[m]; r == ref { + copy(refs[m:], refs[m+1:]) + refs = refs[:len(refs)-1] + return refs + } else if r < ref { + lo = m + 1 + } else { + hi = m + } + } + return refs +} ADDED box/manager/memstore/refs_test.go Index: box/manager/memstore/refs_test.go ================================================================== --- box/manager/memstore/refs_test.go +++ box/manager/memstore/refs_test.go @@ -0,0 +1,137 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 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 memstore + +import ( + "testing" + + "zettelstore.de/z/domain/id" +) + +func assertRefs(t *testing.T, i int, got, exp id.Slice) { + t.Helper() + if got == nil && exp != nil { + t.Errorf("%d: got nil, but expected %v", i, exp) + return + } + if got != nil && exp == nil { + t.Errorf("%d: expected nil, but got %v", i, got) + return + } + if len(got) != len(exp) { + t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got)) + return + } + for p, n := range exp { + if got := got[p]; got != id.Zid(n) { + t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got) + } + } +} + +func TestRefsDiff(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp1, exp2 id.Slice + }{ + {nil, nil, nil, nil}, + {id.Slice{1}, nil, id.Slice{1}, nil}, + {nil, id.Slice{1}, nil, id.Slice{1}}, + {id.Slice{1}, id.Slice{1}, nil, nil}, + {id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil}, + {id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}}, + {id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}}, + } + for i, tc := range testcases { + got1, got2 := refsDiff(tc.in1, tc.in2) + assertRefs(t, i, got1, tc.exp1) + assertRefs(t, i, got2, tc.exp2) + } +} + +func TestAddRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, id.Slice{5}}, + {id.Slice{1}, 5, id.Slice{1, 5}}, + {id.Slice{10}, 5, id.Slice{5, 10}}, + {id.Slice{5}, 5, id.Slice{5}}, + {id.Slice{1, 10}, 5, id.Slice{1, 5, 10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}}, + } + for i, tc := range testcases { + got := addRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRefs(t *testing.T) { + t.Parallel() + testcases := []struct { + in1, in2 id.Slice + exp id.Slice + }{ + {nil, nil, nil}, + {nil, id.Slice{}, nil}, + {id.Slice{}, nil, id.Slice{}}, + {id.Slice{}, id.Slice{}, id.Slice{}}, + {id.Slice{1}, id.Slice{5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}}, + {id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}}, + {id.Slice{1}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}}, + {id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}}, + {id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRefs(tc.in1, tc.in2) + assertRefs(t, i, got, tc.exp) + } +} + +func TestRemRef(t *testing.T) { + t.Parallel() + testcases := []struct { + ref id.Slice + zid uint + exp id.Slice + }{ + {nil, 5, nil}, + {id.Slice{}, 5, id.Slice{}}, + {id.Slice{5}, 5, id.Slice{}}, + {id.Slice{1}, 5, id.Slice{1}}, + {id.Slice{10}, 5, id.Slice{10}}, + {id.Slice{1, 5}, 5, id.Slice{1}}, + {id.Slice{5, 10}, 5, id.Slice{10}}, + {id.Slice{1, 5, 10}, 5, id.Slice{1, 10}}, + } + for i, tc := range testcases { + got := remRef(tc.ref, id.Zid(tc.zid)) + assertRefs(t, i, got, tc.exp) + } +} Index: box/manager/store/store.go ================================================================== --- box/manager/store/store.go +++ box/manager/store/store.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package store contains general index data for storing a zettel index. package store import ( "context" "io" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/query" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // Stats records statistics about the store. type Stats struct { // Zettel is the number of zettel managed by the indexer. @@ -41,24 +38,17 @@ // 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. // Returns set of zettel identifier that must also be checked for changes. UpdateReferences(context.Context, *ZettelIndex) id.Set - // RenameZettel changes all references of current zettel identifier to new - // zettel identifier. - RenameZettel(_ context.Context, curZid, newZid id.Zid) id.Set - // DeleteZettel removes index data for given zettel. // Returns set of zettel identifier that must also be checked for changes. DeleteZettel(context.Context, id.Zid) id.Set // ReadStats populates st with store statistics. Index: box/manager/store/wordset.go ================================================================== --- box/manager/store/wordset.go +++ box/manager/store/wordset.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store // WordSet contains the set of all words, with the count of their occurrences. Index: box/manager/store/wordset_test.go ================================================================== --- box/manager/store/wordset_test.go +++ box/manager/store/wordset_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store_test import ( Index: box/manager/store/zettel.go ================================================================== --- box/manager/store/zettel.go +++ box/manager/store/zettel.go @@ -1,90 +1,83 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package store -import ( - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" -) +import "zettelstore.de/z/domain/id" // ZettelIndex contains all index data of a zettel. type ZettelIndex struct { - 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 + 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 } // NewZettelIndex creates a new zettel index. -func NewZettelIndex(m *meta.Meta) *ZettelIndex { +func NewZettelIndex(zid id.Zid) *ZettelIndex { return &ZettelIndex{ - Zid: m.Zid, - meta: m, - backrefs: id.NewSet(), - inverseRefs: make(map[string]id.Set), - deadrefs: id.NewSet(), + Zid: zid, + backrefs: id.NewSet(), + metarefs: 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.Add(zid) + zi.backrefs.Zid(zid) } -// AddInverseRef adds a named reference to a zettel. On that zettel, the given +// AddMetaRef adds a named reference to a zettel. On that zettel, the given // metadata key should point back to the current zettel. -func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) { - if zids, ok := zi.inverseRefs[key]; ok { - zids.Add(zid) +func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) { + if zids, ok := zi.metarefs[key]; ok { + zids.Zid(zid) return } - zi.inverseRefs[key] = id.NewSet(zid) + zi.metarefs[key] = id.NewSet(zid) } // AddDeadRef adds a dead reference to a zettel. func (zi *ZettelIndex) AddDeadRef(zid id.Zid) { - zi.deadrefs.Add(zid) + zi.deadrefs.Zid(zid) } // SetWords sets the words to the given value. func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words } // 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() } - -// GetMeta return just the raw metadata. -func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta } +func (zi *ZettelIndex) GetDeadRefs() id.Slice { + return zi.deadrefs.Sorted() +} // 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() +} -// 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 { +// 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 { return nil } - result := make(map[string]id.Slice, len(zi.inverseRefs)) - for key, refs := range zi.inverseRefs { + result := make(map[string]id.Slice, len(zi.metarefs)) + for key, refs := range zi.metarefs { result[key] = refs.Sorted() } return result } Index: box/membox/membox.go ================================================================== --- box/membox/membox.go +++ box/membox/membox.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package membox stores zettel volatile in main memory. package membox @@ -19,15 +16,16 @@ "net/url" "sync" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" ) func init() { manager.Register( "mem", @@ -48,36 +46,27 @@ u *url.URL cdata manager.ConnectData maxZettel int maxBytes int mx sync.RWMutex // Protects the following fields - zettel map[id.Zid]zettel.Zettel + zettel map[id.Zid]domain.Zettel curBytes int } -func (mb *memBox) notifyChanged(zid id.Zid) { +func (mb *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) { if chci := mb.cdata.Notify; chci != nil { - chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid} + chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } func (mb *memBox) Location() string { return mb.u.String() } -func (mb *memBox) State() box.StartState { - mb.mx.RLock() - defer mb.mx.RUnlock() - if mb.zettel == nil { - return box.StartStateStopped - } - return box.StartStateStarted -} - func (mb *memBox) Start(context.Context) error { mb.mx.Lock() - mb.zettel = make(map[id.Zid]zettel.Zettel) + mb.zettel = make(map[id.Zid]domain.Zettel) mb.curBytes = 0 mb.mx.Unlock() mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box") return nil } @@ -92,11 +81,11 @@ mb.mx.RLock() defer mb.mx.RUnlock() return len(mb.zettel) < mb.maxZettel } -func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) { +func (mb *memBox) CreateZettel(_ context.Context, zettel domain.Zettel) (id.Zid, error) { mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes { mb.mx.Unlock() return id.Invalid, box.ErrCapacity @@ -113,32 +102,36 @@ meta.Zid = zid zettel.Meta = meta mb.zettel[zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(zid) + mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Zid(zid).Msg("CreateZettel") return zid, nil } -func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) { +func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) { + mb.mx.RLock() + zettel, ok := mb.zettel[zid] + mb.mx.RUnlock() + if !ok { + return domain.Zettel{}, box.ErrNotFound + } + zettel.Meta = zettel.Meta.Clone() + mb.log.Trace().Msg("GetZettel") + return zettel, nil +} + +func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) { mb.mx.RLock() - z, ok := mb.zettel[zid] + zettel, ok := mb.zettel[zid] mb.mx.RUnlock() if !ok { - return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid} - } - z.Meta = z.Meta.Clone() - mb.log.Trace().Msg("GetZettel") - return z, nil -} - -func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool { - mb.mx.RLock() - _, found := mb.zettel[zid] - mb.mx.RUnlock() - return found + return nil, box.ErrNotFound + } + mb.log.Trace().Msg("GetMeta") + return zettel.Meta.Clone(), nil } func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error { mb.mx.RLock() defer mb.mx.RUnlock() @@ -163,11 +156,11 @@ } } return nil } -func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool { +func (mb *memBox) CanUpdateZettel(_ context.Context, zettel domain.Zettel) bool { mb.mx.RLock() defer mb.mx.RUnlock() zid := zettel.Meta.Zid if !zid.IsValid() { return false @@ -178,14 +171,14 @@ newBytes -= prevZettel.Length() } return newBytes < mb.maxBytes } -func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error { +func (mb *memBox) UpdateZettel(_ context.Context, zettel domain.Zettel) error { m := zettel.Meta.Clone() if !m.Zid.IsValid() { - return box.ErrInvalidZid{Zid: m.Zid.String()} + return &box.ErrInvalidID{Zid: m.Zid} } mb.mx.Lock() newBytes := mb.curBytes + zettel.Length() if prevZettel, found := mb.zettel[m.Zid]; found { @@ -198,11 +191,11 @@ zettel.Meta = m mb.zettel[m.Zid] = zettel mb.curBytes = newBytes mb.mx.Unlock() - mb.notifyChanged(m.Zid) + mb.notifyChanged(box.OnZettel, m.Zid) mb.log.Trace().Msg("UpdateZettel") return nil } func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true } @@ -210,27 +203,27 @@ func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error { mb.mx.Lock() zettel, ok := mb.zettel[curZid] if !ok { mb.mx.Unlock() - return box.ErrZettelNotFound{Zid: curZid} + return box.ErrNotFound } // Check that there is no zettel with newZid if _, ok = mb.zettel[newZid]; ok { mb.mx.Unlock() - return box.ErrInvalidZid{Zid: newZid.String()} + return &box.ErrInvalidID{Zid: newZid} } meta := zettel.Meta.Clone() meta.Zid = newZid zettel.Meta = meta mb.zettel[newZid] = zettel delete(mb.zettel, curZid) mb.mx.Unlock() - mb.notifyChanged(curZid) - mb.notifyChanged(newZid) + mb.notifyChanged(box.OnZettel, curZid) + mb.notifyChanged(box.OnZettel, newZid) mb.log.Trace().Msg("RenameZettel") return nil } func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool { @@ -243,16 +236,16 @@ func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error { mb.mx.Lock() oldZettel, found := mb.zettel[zid] if !found { mb.mx.Unlock() - return box.ErrZettelNotFound{Zid: zid} + return box.ErrNotFound } delete(mb.zettel, zid) mb.curBytes -= oldZettel.Length() mb.mx.Unlock() - mb.notifyChanged(zid) + mb.notifyChanged(box.OnZettel, zid) mb.log.Trace().Msg("DeleteZettel") return nil } func (mb *memBox) ReadStats(st *box.ManagedBoxStats) { Index: box/notify/directory.go ================================================================== --- box/notify/directory.go +++ box/notify/directory.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -20,80 +17,68 @@ "regexp" "strings" "sync" "zettelstore.de/z/box" - "zettelstore.de/z/kernel" + "zettelstore.de/z/domain/id" "zettelstore.de/z/logger" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/strfun" - "zettelstore.de/z/zettel/id" ) type entrySet map[id.Zid]*DirEntry -// DirServiceState signal the internal state of the service. +// directoryState signal the internal state of the service. // // The following state transitions are possible: // --newDirService--> dsCreated // dsCreated --Start--> dsStarting // dsStarting --last list notification--> dsWorking // dsWorking --directory missing--> dsMissing // dsMissing --last list notification--> dsWorking // --Stop--> dsStopping -type DirServiceState uint8 +type directoryState uint8 const ( - DsCreated DirServiceState = iota - DsStarting // Reading inital scan - DsWorking // Initial scan complete, fully operational - DsMissing // Directory is missing - DsStopping // Service is shut down + dsCreated directoryState = iota + dsStarting // Reading inital scan + dsWorking // Initial scan complete, fully operational + dsMissing // Directory is missing + dsStopping // Service is shut down ) // DirService specifies a directory service for file based zettel. type DirService struct { - box box.ManagedBox log *logger.Logger dirPath string notifier Notifier infos chan<- box.UpdateInfo mx sync.RWMutex // protects status, entries - state DirServiceState + state directoryState entries entrySet } // ErrNoDirectory signals missing directory data. var ErrNoDirectory = errors.New("unable to retrieve zettel directory information") // NewDirService creates a new directory service. -func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { +func NewDirService(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService { return &DirService{ - box: box, log: log, notifier: notifier, infos: chci, - state: DsCreated, - } -} - -// State the current service state. -func (ds *DirService) State() DirServiceState { - ds.mx.RLock() - state := ds.state - ds.mx.RUnlock() - return state + state: dsCreated, + } } // Start the directory service. func (ds *DirService) Start() { ds.mx.Lock() - ds.state = DsStarting + ds.state = dsStarting ds.mx.Unlock() - var newEntries entrySet - go ds.updateEvents(newEntries) + go ds.updateEvents() } // Refresh the directory entries. func (ds *DirService) Refresh() { ds.notifier.Refresh() @@ -100,11 +85,11 @@ } // Stop the directory service. func (ds *DirService) Stop() { ds.mx.Lock() - ds.state = DsStopping + ds.state = dsStopping ds.mx.Unlock() ds.notifier.Close() } func (ds *DirService) logMissingEntry(action string) error { @@ -192,11 +177,11 @@ defer ds.mx.Unlock() if ds.entries == nil { return DirEntry{}, ds.logMissingEntry("rename") } if _, found := ds.entries[newZid]; found { - return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()} + return DirEntry{}, &box.ErrInvalidID{Zid: newZid} } oldZid := oldEntry.Zid newEntry := DirEntry{ Zid: newZid, MetaName: renameFilename(oldEntry.MetaName, oldZid, newZid), @@ -225,87 +210,71 @@ } delete(ds.entries, zid) return nil } -func (ds *DirService) updateEvents(newEntries entrySet) { - // Something may panic. Ensure a running service. - defer func() { - if ri := recover(); ri != nil { - kernel.Main.LogRecover("DirectoryService", ri) - go ds.updateEvents(newEntries) - } - }() - +func (ds *DirService) updateEvents() { + var newEntries entrySet for ev := range ds.notifier.Events() { - e, ok := ds.handleEvent(ev, newEntries) - if !ok { + ds.mx.RLock() + state := ds.state + ds.mx.RUnlock() + + if msg := ds.log.Trace(); msg.Enabled() { + msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") + } + if state == dsStopping { break } - newEntries = e - } -} -func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) { - ds.mx.RLock() - state := ds.state - ds.mx.RUnlock() - - if msg := ds.log.Trace(); msg.Enabled() { - msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent") - } - if state == DsStopping { - return nil, false - } - - switch ev.Op { - case Error: - newEntries = nil - if state != DsMissing { - ds.log.Error().Err(ev.Err).Msg("Notifier confused") - } - case Make: - newEntries = make(entrySet) - case List: - if ev.Name == "" { - zids := getNewZids(newEntries) - ds.mx.Lock() - fromMissing := ds.state == DsMissing - prevEntries := ds.entries - ds.entries = newEntries - ds.state = DsWorking - ds.mx.Unlock() - ds.onCreateDirectory(zids, prevEntries) - if fromMissing { - ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") - } - return nil, true - } - if newEntries != nil { - ds.onUpdateFileEvent(newEntries, ev.Name) - } - case Destroy: - ds.onDestroyDirectory() - ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") - return nil, true - case Update: - ds.mx.Lock() - zid := ds.onUpdateFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(zid) - } - case Delete: - ds.mx.Lock() - zid := ds.onDeleteFileEvent(ds.entries, ev.Name) - ds.mx.Unlock() - if zid != id.Invalid { - ds.notifyChange(zid) - } - default: - ds.log.Error().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") - } - return newEntries, true + + switch ev.Op { + case Error: + newEntries = nil + if state != dsMissing { + ds.log.Warn().Err(ev.Err).Msg("Notifier confused") + } + case Make: + newEntries = make(entrySet) + case List: + if ev.Name == "" { + zids := getNewZids(newEntries) + ds.mx.Lock() + fromMissing := ds.state == dsMissing + prevEntries := ds.entries + ds.entries = newEntries + ds.state = dsWorking + ds.mx.Unlock() + newEntries = nil + ds.onCreateDirectory(zids, prevEntries) + if fromMissing { + ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found") + } + } else if newEntries != nil { + ds.onUpdateFileEvent(newEntries, ev.Name) + } + case Destroy: + newEntries = nil + ds.onDestroyDirectory() + ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing") + case Update: + ds.mx.Lock() + zid := ds.onUpdateFileEvent(ds.entries, ev.Name) + ds.mx.Unlock() + if zid != id.Invalid { + ds.notifyChange(box.OnZettel, zid) + } + case Delete: + ds.mx.Lock() + zid := ds.onDeleteFileEvent(ds.entries, ev.Name) + ds.mx.Unlock() + if zid != id.Invalid { + ds.notifyChange(box.OnZettel, zid) + } + default: + ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event") + } + } } func getNewZids(entries entrySet) id.Slice { zids := make(id.Slice, 0, len(entries)) for zid := range entries { @@ -314,29 +283,29 @@ return zids } func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) { for _, zid := range zids { - ds.notifyChange(zid) + ds.notifyChange(box.OnZettel, zid) delete(prevEntries, zid) } // These were previously stored, by are not found now. // Notify system that these were deleted, e.g. for updating the index. for zid := range prevEntries { - ds.notifyChange(zid) + ds.notifyChange(box.OnZettel, zid) } } func (ds *DirService) onDestroyDirectory() { ds.mx.Lock() entries := ds.entries ds.entries = nil - ds.state = DsMissing + ds.state = dsMissing ds.mx.Unlock() for zid := range entries { - ds.notifyChange(zid) + ds.notifyChange(box.OnZettel, zid) } } var validFileName = regexp.MustCompile(`^(\d{14})`) @@ -374,13 +343,13 @@ return id.Invalid } entry := fetchdirEntry(entries, zid) dupName1, dupName2 := ds.updateEntry(entry, name) if dupName1 != "" { - ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)") + ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)") if dupName2 != "" { - ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)") + ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)") } return id.Invalid } return zid } @@ -581,15 +550,12 @@ if newExt == "zmk" { return true } oldInfo := parser.Get(oldExt) newInfo := parser.Get(newExt) - if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser { - return !oldASTParser - } - if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat { - return !oldTextFormat + if oldTextParser := oldInfo.IsTextParser; oldTextParser != newInfo.IsTextParser { + return !oldTextParser } if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat { return oldImageFormat } if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) { @@ -603,11 +569,11 @@ return newLen < oldLen } return newExt < oldExt } -func (ds *DirService) notifyChange(zid id.Zid) { +func (ds *DirService) notifyChange(reason box.UpdateReason, zid id.Zid) { if chci := ds.infos; chci != nil { - ds.log.Trace().Zid(zid).Msg("notifyChange") - chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid} + ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange") + chci <- box.UpdateInfo{Reason: reason, Zid: zid} } } Index: box/notify/directory_test.go ================================================================== --- box/notify/directory_test.go +++ box/notify/directory_test.go @@ -1,31 +1,28 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2022-present Detlef Stern +// Copyright (c) 2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2022-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "testing" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. - _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. + _ "zettelstore.de/z/parser/pikchr" // Allow to use pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) func TestSeekZid(t *testing.T) { testcases := []struct { name string @@ -50,20 +47,15 @@ } func TestNewExtIsBetter(t *testing.T) { extVals := []string{ // Main Formats - meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD, + api.ValueSyntaxZmk, "pikchr", "markdown", "md", // Other supported text formats - meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML, - meta.SyntaxText, meta.SyntaxPlain, - // Supported text graphics formats - meta.SyntaxSVG, - meta.SyntaxNone, - // Supported binary graphic formats - meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG, - + "css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain", + // Supported graphics formats + api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg", // Unsupported syntax values "gz", "cpp", "tar", "cppc", } for oldI, oldExt := range extVals { for newI, newExt := range extVals { Index: box/notify/entry.go ================================================================== --- box/notify/entry.go +++ box/notify/entry.go @@ -1,28 +1,25 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( "path/filepath" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) const ( extZettel = "zettel" // file contains metadata and content extBin = "bin" // file contains binary content @@ -49,11 +46,11 @@ func (e *DirEntry) HasMetaInContent() bool { return e.IsValid() && extIsMetaAndContent(e.ContentExt) } // SetupFromMetaContent fills entry data based on metadata and zettel content. -func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) { +func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content domain.Content, getZettelFileSyntax func() []string) { if e.Zid != m.Zid { panic("Zid differ") } if contentName := e.ContentName; contentName != "" { if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" { @@ -60,11 +57,11 @@ e.MetaName = e.calcBaseName(contentName) } return } - syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax) + syntax := m.GetDefault(api.KeySyntax, "") ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax) metaName := e.MetaName eimc := extIsMetaAndContent(ext) if eimc { if metaName != "" { @@ -81,11 +78,11 @@ e.MetaName = e.calcBaseName(e.ContentName) } } } -func contentExtWithMeta(syntax string, content zettel.Content) string { +func contentExtWithMeta(syntax string, content domain.Content) string { p := parser.Get(syntax) if content.IsBinary() { if p.IsImageFormat { return syntax } @@ -100,11 +97,11 @@ func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string { if yamlSep { return extZettel } switch syntax { - case meta.SyntaxNone, meta.SyntaxZmk: + case api.ValueSyntaxNone, api.ValueSyntaxZmk: return extZettel } for _, s := range getZettelFileSyntax() { if s == syntax { return extZettel Index: box/notify/fsdir.go ================================================================== --- box/notify/fsdir.go +++ box/notify/fsdir.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -56,17 +53,18 @@ Str("path", absPath).Err(err). Msg("Unable to access Zettel directory and its parent directory") watcher.Close() return nil, err } - log.Info().Str("parentDir", absParentDir).Err(errParent). + log.Warn(). + Str("parentDir", absParentDir).Err(errParent). Msg("Parent of Zettel directory cannot be supervised") - log.Info().Str("path", absPath). + log.Warn().Str("path", absPath). Msg("Zettelstore might not detect a deletion or movement of the Zettel directory") } else if err != nil { // Not a problem, if container is not available. It might become available later. - log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available") + log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available") } fsdn := &fsdirNotifier{ log: log, events: make(chan Event), @@ -94,25 +92,24 @@ defer close(fsdn.events) defer close(fsdn.refresh) if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) { return } - for fsdn.readAndProcessEvent() { } } func (fsdn *fsdirNotifier) readAndProcessEvent() bool { select { case <-fsdn.done: - fsdn.traceDone(1) + fsdn.log.Trace().Int("i", 1).Msg("done with read and process events") return false default: } select { case <-fsdn.done: - fsdn.traceDone(2) + fsdn.log.Trace().Int("i", 2).Msg("done with read and process events") return false case <-fsdn.refresh: fsdn.log.Trace().Msg("refresh") listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) case err, ok := <-fsdn.base.Errors: @@ -121,11 +118,11 @@ return false } select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: - fsdn.traceDone(3) + fsdn.log.Trace().Int("i", 3).Msg("done with read and process events") return false } case ev, ok := <-fsdn.base.Events: fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event") if !ok { @@ -136,14 +133,10 @@ } } return true } -func (fsdn *fsdirNotifier) traceDone(pos int64) { - fsdn.log.Trace().Int("i", pos).Msg("done with read and process events") -} - func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool { if strings.HasPrefix(ev.Name, fsdn.path) { if len(ev.Name) == len(fsdn.path) { return fsdn.processDirEvent(ev) } @@ -167,11 +160,11 @@ } if ev.Has(fsnotify.Create) { err := fsdn.base.Add(fsdn.path) if err != nil { - fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory") + fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory") select { case fsdn.events <- Event{Op: Error, Err: err}: case <-fsdn.done: fsdn.log.Trace().Int("i", 2).Msg("done dir event processing") return false Index: box/notify/helper.go ================================================================== --- box/notify/helper.go +++ box/notify/helper.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -17,10 +14,15 @@ "archive/zip" "os" "zettelstore.de/z/logger" ) + +// MakeMetaFilename builds the name of the file containing metadata. +func MakeMetaFilename(basename string) string { + return basename //+ ".meta" +} // EntryFetcher return a list of (file) names of an directory. type EntryFetcher interface { Fetch() ([]string, error) } Index: box/notify/notify.go ================================================================== --- box/notify/notify.go +++ box/notify/notify.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package notify provides some notification services to be used by box services. package notify @@ -36,19 +33,19 @@ // Error signals a detected error. Details are in Event.Err. // // Make signals that the container is detected. List events will follow. // // List signals a found file, if Event.Name is not empty. Otherwise it signals -// the end of files within the container. +// the end of files within the container. // // Destroy signals that the container is not there any more. It might me Make later again. // -// Update signals that file Event.Name was created/updated. -// File name is relative to the container. +// Update signals that file Event.Name was created/updated. File name is relative +// to the container. // -// Delete signals that file Event.Name was removed. -// File name is relative to the container's name. +// Delete signals that file Event.Name was removed. File name is relative to +// the container's name. const ( _ EventOp = iota Error // Error while operating Make // Make container List // List container Index: box/notify/simpledir.go ================================================================== --- box/notify/simpledir.go +++ box/notify/simpledir.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package notify import ( @@ -45,20 +42,20 @@ return sdn, nil } // NewSimpleZipNotifier creates a zip-file based notifier that will not receive // any notifications from the operating system. -func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier { +func NewSimpleZipNotifier(log *logger.Logger, zipPath string) (Notifier, error) { sdn := &simpleDirNotifier{ log: log, events: make(chan Event), done: make(chan struct{}), refresh: make(chan struct{}), fetcher: newZipPathFetcher(zipPath), } go sdn.eventLoop() - return sdn + return sdn, nil } func (sdn *simpleDirNotifier) Events() <-chan Event { return sdn.events } Index: cmd/cmd_file.go ================================================================== --- cmd/cmd_file.go +++ cmd/cmd_file.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -18,17 +15,17 @@ "flag" "fmt" "io" "os" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" + "zettelstore.de/c/api" + "zettelstore.de/z/domain" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/encoder" + "zettelstore.de/z/input" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: file ----------------------------------------------- func cmdFile(fs *flag.FlagSet) (int, error) { @@ -37,18 +34,18 @@ if m == nil { return 2, err } z := parser.ParseZettel( context.Background(), - zettel.Zettel{ + domain.Zettel{ Meta: m, - Content: zettel.NewContent(inp.Src[inp.Pos:]), + Content: domain.NewContent(inp.Src[inp.Pos:]), }, - m.GetDefault(api.KeySyntax, meta.DefaultSyntax), + m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk), nil, ) - encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)}) + encdr := encoder.Create(api.Encoder(enc)) if encdr == nil { fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc) return 2, nil } _, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata) Index: cmd/cmd_password.go ================================================================== --- cmd/cmd_password.go +++ cmd/cmd_password.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -18,13 +15,13 @@ "fmt" "os" "golang.org/x/term" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/auth/cred" - "zettelstore.de/z/zettel/id" + "zettelstore.de/z/domain/id" ) // ---------- Subcommand: password ------------------------------------------- func cmdPassword(fs *flag.FlagSet) (int, error) { Index: cmd/cmd_run.go ================================================================== --- cmd/cmd_run.go +++ cmd/cmd_run.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -19,16 +16,16 @@ "net/http" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" + "zettelstore.de/z/domain/meta" "zettelstore.de/z/kernel" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter/api" "zettelstore.de/z/web/adapter/webui" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/meta" ) // ---------- Subcommand: run ------------------------------------------------ func flgRun(fs *flag.FlagSet) { @@ -53,81 +50,95 @@ func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) { protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig) kern := kernel.Main webLog := kern.GetLogger(kernel.WebService) - - var getUser getUserImpl - logAuth := kern.GetLogger(kernel.AuthService) - logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) - ucGetUser := usecase.NewGetUser(authManager, boxManager) - ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser) - ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) - ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) - ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager) - ucGetZettel := usecase.NewGetZettel(protectedBoxManager) - ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) - ucQuery := usecase.NewQuery(protectedBoxManager) - ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery) - ucQuery.SetEvaluate(&ucEvaluate) - ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery) - ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery) - ucListSyntax := usecase.NewListSyntax(protectedBoxManager) - ucListRoles := usecase.NewListRoles(protectedBoxManager) - ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager) - ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager) - ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager) - ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager) - ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager) - ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string)) - a := api.New( webLog.Clone().Str("adapter", "api").Child(), webSrv, authManager, authManager, rtConfig, authPolicy) wui := webui.New( webLog.Clone().Str("adapter", "wui").Child(), - webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate) + webSrv, authManager, rtConfig, authManager, boxManager, authPolicy) + + var getUser getUserImpl + logAuth := kern.GetLogger(kernel.AuthService) + logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser) + ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager) + ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager) + ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager) + ucGetMeta := usecase.NewGetMeta(protectedBoxManager) + ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager) + ucGetZettel := usecase.NewGetZettel(protectedBoxManager) + ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel) + ucListMeta := usecase.NewListMeta(protectedBoxManager) + ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta) + ucListSyntax := usecase.NewListSyntax(protectedBoxManager) + ucListRoles := usecase.NewListRoles(protectedBoxManager) + ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig) + 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)) webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager)) if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" { const assetPrefix = "/assets/" webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir)))) - webSrv.Handle("/favicon.ico", wui.MakeFaviconHandler(assetDir)) } // Web user interface if !authManager.IsReadonly() { - webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler( + ucGetMeta, &ucEvaluate)) webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename)) - 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(ucGetZettel, ucGetAllZettel)) + webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler( + ucGetMeta, ucGetAllMeta, &ucEvaluate)) 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(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) - webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel)) + webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate)) + webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler( + &ucEvaluate, ucGetMeta)) webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler()) webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler( - ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery)) + ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs)) + webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler( + ucZettelContext, &ucEvaluate)) // API webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate)) webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler()) + webSrv.AddListRoute('j', server.MethodGet, a.MakeQueryHandler(ucListMeta)) + webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel)) + webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta)) + webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler( + usecase.NewZettelOrder(protectedBoxManager, ucEvaluate))) + webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel)) + webSrv.AddListRoute('q', server.MethodGet, a.MakeQueryHandler(ucListMeta)) + webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler( + ucGetMeta, ucUnlinkedRefs, &ucEvaluate)) + webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate)) webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion)) webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh)) - webSrv.AddListRoute('z', server.MethodGet, a.MakeQueryHandler(&ucQuery, &ucTagZettel, &ucRoleZettel, &ucReIndex)) - webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate)) + webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext)) + webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta)) + webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel)) if !authManager.IsReadonly() { - webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) - webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) + webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel)) + webSrv.AddZettelRoute('j', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate)) + webSrv.AddZettelRoute('j', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) + webSrv.AddZettelRoute('j', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) + webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreatePlainZettelHandler(&ucCreateZettel)) + webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdatePlainZettelHandler(&ucUpdate)) webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete)) webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename)) } if authManager.WithAuth() { Index: cmd/command.go ================================================================== --- cmd/command.go +++ cmd/command.go @@ -1,24 +1,21 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( "flag" - "t73f.de/r/zsc/maps" + "zettelstore.de/c/maps" "zettelstore.de/z/logger" ) // Command stores information about commands / sub-commands. type Command struct { @@ -49,11 +46,11 @@ } if _, ok := commands[cmd.Name]; ok { panic("Command already registered: " + cmd.Name) } cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError) - cmd.flags.String("l", logger.InfoLevel.String(), "log level specification") + cmd.flags.String("l", logger.InfoLevel.String(), "global log level") if cmd.SetFlags != nil { cmd.SetFlags(cmd.flags) } commands[cmd.Name] = cmd Index: cmd/main.go ================================================================== --- cmd/main.go +++ cmd/main.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- package cmd import ( @@ -23,23 +20,23 @@ "runtime/debug" "strconv" "strings" "time" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" + "zettelstore.de/c/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" ) const strRunSimple = "run-simple" func init() { @@ -89,19 +86,19 @@ Name: "password", Func: cmdPassword, }) } -func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) { +func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) { if configFlag := fs.Lookup("c"); configFlag != nil { if filename := configFlag.Value.String(); filename != "" { content, err := readConfiguration(filename) - return filename, createConfiguration(content, err) + return createConfiguration(content, err) } } - filename, content, err := searchAndReadConfiguration() - return filename, createConfiguration(content, err) + content, err := searchAndReadConfiguration() + return createConfiguration(content, err) } func createConfiguration(content []byte, err error) *meta.Meta { if err != nil { return meta.New(id.Invalid) @@ -109,21 +106,21 @@ return meta.NewFromInput(id.Invalid, input.NewInput(content)) } func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) } -func searchAndReadConfiguration() (string, []byte, error) { - for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} { +func searchAndReadConfiguration() ([]byte, error) { + for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} { if content, err := readConfiguration(filename); err == nil { - return filename, content, nil + return content, nil } } - return "", nil, os.ErrNotExist + return readConfiguration(".zscfg") } -func getConfig(fs *flag.FlagSet) (string, *meta.Meta) { - filename, cfg := fetchStartupConfiguration(fs) +func getConfig(fs *flag.FlagSet) *meta.Meta { + 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": @@ -145,11 +142,11 @@ cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) - return filename, cfg + return cfg } func deleteConfiguredBoxes(cfg *meta.Meta) { for _, p := range cfg.PairsRest() { if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) { @@ -180,14 +177,16 @@ ) func setServiceConfig(cfg *meta.Meta) bool { debugMode := cfg.GetBool(keyDebug) if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel { - kernel.Main.SetLogLevel(logger.DebugLevel.String()) + kernel.Main.SetGlobalLogLevel(logger.DebugLevel) } - if logLevel, found := cfg.Get(keyLogLevel); found { - kernel.Main.SetLogLevel(logLevel) + if strLevel, found := cfg.Get(keyLogLevel); found { + if level := logger.ParseLevel(strLevel); level.IsValid() { + kernel.Main.SetGlobalLogLevel(level) + } } err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode) err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose)) if val, found := cfg.Get(keyAdminPort); found { err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val) @@ -235,11 +234,11 @@ func setConfigValue(err error, subsys kernel.Service, key string, val any) error { if err == nil { err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val)) if err != nil { - kernel.Main.GetKernelLogger().Error().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") + kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration") } } return err } @@ -252,11 +251,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 } - filename, cfg := getConfig(fs) + cfg := getConfig(fs) if !setServiceConfig(cfg) { fs.Usage() return 2 } @@ -291,11 +290,11 @@ ) if command.Simple { kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true") } - kern.Start(command.Header, command.LineServer, filename) + kern.Start(command.Header, command.LineServer) exitCode, err := command.Func(fs) if err != nil { fmt.Fprintf(os.Stderr, "%s: %v\n", name, err) } kern.Shutdown(true) @@ -303,11 +302,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: cmd/register.go ================================================================== --- cmd/register.go +++ cmd/register.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package cmd provides command generic functions. package cmd @@ -20,18 +17,17 @@ _ "zettelstore.de/z/box/constbox" // Allow to use global internal box. _ "zettelstore.de/z/box/dirbox" // Allow to use directory box. _ "zettelstore.de/z/box/filebox" // Allow to use file box. _ "zettelstore.de/z/box/membox" // Allow to use in-memory box. _ "zettelstore.de/z/encoder/htmlenc" // Allow to use HTML encoder. - _ "zettelstore.de/z/encoder/mdenc" // Allow to use markdown encoder. - _ "zettelstore.de/z/encoder/shtmlenc" // Allow to use SHTML encoder. - _ "zettelstore.de/z/encoder/szenc" // Allow to use Sz encoder. + _ "zettelstore.de/z/encoder/sexprenc" // Allow to use sexpr encoder. _ "zettelstore.de/z/encoder/textenc" // Allow to use text encoder. + _ "zettelstore.de/z/encoder/zjsonenc" // Allow to use ZJSON encoder. _ "zettelstore.de/z/encoder/zmkenc" // Allow to use zmk encoder. _ "zettelstore.de/z/kernel/impl" // Allow kernel implementation to create itself _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. - _ "zettelstore.de/z/parser/draw" // Allow to use draw parser. _ "zettelstore.de/z/parser/markdown" // Allow to use markdown parser. _ "zettelstore.de/z/parser/none" // Allow to use none parser. + _ "zettelstore.de/z/parser/pikchr" // Allow to use PIC/Pikchr parser. _ "zettelstore.de/z/parser/plain" // Allow to use plain parser. _ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser. ) Index: cmd/zettelstore/main.go ================================================================== --- cmd/zettelstore/main.go +++ cmd/zettelstore/main.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2021 Detlef Stern // -// This file is part of Zettelstore. +// 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package main is the starting point for the zettelstore command. package main Index: collect/collect.go ================================================================== --- collect/collect.go +++ collect/collect.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect Index: collect/collect_test.go ================================================================== --- collect/collect_test.go +++ collect/collect_test.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package collect_test provides some unit test for collectors. package collect_test Index: collect/order.go ================================================================== --- collect/order.go +++ collect/order.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- // Package collect provides functions to collect items from a syntax tree. package collect ADDED collect/split.go Index: collect/split.go ================================================================== --- collect/split.go +++ collect/split.go @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 collect provides functions to collect items from a syntax tree. +package collect + +import ( + "zettelstore.de/z/ast" + "zettelstore.de/z/strfun" +) + +// DivideReferences divides the given list of rederences into zettel, local, and external References. +func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) { + if len(all) == 0 { + return nil, nil, nil + } + + mapZettel := make(strfun.Set) + mapLocal := make(strfun.Set) + mapExternal := make(strfun.Set) + for _, ref := range all { + if ref.State == ast.RefStateSelf { + continue + } + if ref.IsZettel() { + zettel = appendRefToList(zettel, mapZettel, ref) + } else if ref.IsExternal() { + external = appendRefToList(external, mapExternal, ref) + } else { + local = appendRefToList(local, mapLocal, ref) + } + } + return zettel, local, external +} + +func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference { + s := ref.String() + if !refSet.Has(s) { + reflist = append(reflist, ref) + refSet.Set(s) + } + return reflist +} Index: config/config.go ================================================================== --- config/config.go +++ config/config.go @@ -1,36 +1,31 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package config provides functions to retrieve runtime configuration data. package config import ( "context" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" ) // Key values that are supported by Config.Get const ( - KeyFooterZettel = "footer-zettel" - KeyHomeZettel = "home-zettel" - KeyShowBackLinks = "show-back-links" - KeyShowFolgeLinks = "show-folge-links" - KeyShowSubordinateLinks = "show-subordinate-links" - KeyShowSuccessorLinks = "show-successor-links" + KeyFooterHTML = "footer-html" // api.KeyLang + KeyMarkerExternal = "marker-external" ) // Config allows to retrieve all defined configuration values that can be changed during runtime. type Config interface { AuthConfig @@ -42,10 +37,13 @@ // AddDefaultValues enriches the given meta data with its default values. AddDefaultValues(context.Context, *meta.Meta) *meta.Meta // GetSiteName returns the current value of the "site-name" key. GetSiteName() string + + // GetHomeZettel returns the value of the "home-zettel" key. + GetHomeZettel() id.Zid // GetHTMLInsecurity returns the current GetHTMLInsecurity() HTMLInsecurity // GetMaxTransclusions returns the maximum number of indirect transclusions. @@ -96,14 +94,13 @@ // AllowHTML returns true, if the given HTML insecurity level matches the given syntax value. func (hi HTMLInsecurity) AllowHTML(syntax string) bool { switch hi { case SyntaxHTML: - return syntax == meta.SyntaxHTML + return syntax == api.ValueSyntaxHTML case MarkdownHTML: - return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD + return syntax == api.ValueSyntaxHTML || syntax == "markdown" || syntax == "md" case ZettelmarkupHTML: - return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML || - syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD + return syntax == api.ValueSyntaxZmk || syntax == api.ValueSyntaxHTML || syntax == "markdown" || syntax == "md" } return false } Index: docs/development/00010000000000.zettel ================================================================== --- docs/development/00010000000000.zettel +++ docs/development/00010000000000.zettel @@ -1,11 +1,8 @@ id: 00010000000000 title: Developments Notes role: zettel syntax: zmk -created: 00010101000000 -modified: 20231218182020 +modified: 20210916194954 * [[Required Software|20210916193200]] -* [[Fuzzing tests|20221026184300]] * [[Checklist for Release|20210916194900]] -* [[Development tools|20231218181900]] Index: docs/development/20210916193200.zettel ================================================================== --- docs/development/20210916193200.zettel +++ docs/development/20210916193200.zettel @@ -1,28 +1,19 @@ id: 20210916193200 title: Required Software role: zettel syntax: zmk -created: 20210916193200 -modified: 20231213194509 +modified: 20211213190428 The following software must be installed: -* A current, supported [[release of Go|https://go.dev/doc/devel/release]], -* [[Fossil|https://fossil-scm.org/]], -* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only). +* A current, supported [[release of Go|https://golang.org/doc/devel/release.html]], +* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, +* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, +* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest`` Make sure that the software is in your path, e.g. via: + ```sh export PATH=$PATH:/usr/local/go/bin export PATH=$PATH:$(go env GOPATH)/bin ``` - -The internal build tool need the following software. -It can be installed / updated via the build tool itself: ``go run tools/devtools/devtools.go``. - -Otherwise you can install the software by hand: - -* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``, -* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``, -* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``, -* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``, Index: docs/development/20210916194900.zettel ================================================================== --- docs/development/20210916194900.zettel +++ docs/development/20210916194900.zettel @@ -1,24 +1,23 @@ id: 20210916194900 title: Checklist for Release role: zettel syntax: zmk -created: 20210916194900 -modified: 20231213194631 +modified: 20220309105459 # Sync with the official repository #* ``fossil sync -u`` # Make sure that there is no workspace defined. #* ``ls ..`` must not have a file ''go.work'', in no parent folder. # Make sure that all dependencies are up-to-date. #* ``cat go.mod`` # Clean up your Go workspace: -#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). +#* ``go run tools/build.go clean`` (alternatively: ``make clean``). # All internal tests must succeed: -#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``). +#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``). # The API tests must succeed on every development platform: -#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``). +#* ``go run tools/build.go testapi`` (alternatively: ``make api``). # Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual: #* ``go run -race cmd/zettelstore/main.go run -d docs/manual`` #* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt`` #* Check all ""Error: 404 Not Found"" #* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''. @@ -42,18 +41,18 @@ # Commit the new release version: #* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"`` #* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''. Otherwise client will not be able to import ''zettelkasten.de/z''. # Clean up your Go workspace: -#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``). +#* ``go run tools/build.go clean`` (alternatively: ``make clean``). # Create the release: -#* ``go run tools/build/build.go release`` (alternatively: ``make release``). +#* ``go run tools/build.go release`` (alternatively: ``make release``). # Remove previous executables: #* ``fossil uv remove --glob '*-PREVVERSION*'`` # Add executables for release: -#* ``cd releases`` +#* ``cd release`` #* ``fossil uv add *.zip`` #* ``cd ..`` #* Synchronize with main repository: #* ``fossil sync -u`` # Enable autosync: #* ``fossil setting autosync on`` DELETED docs/development/20221026184300.zettel Index: docs/development/20221026184300.zettel ================================================================== --- docs/development/20221026184300.zettel +++ docs/development/20221026184300.zettel @@ -1,14 +0,0 @@ -id: 20221026184300 -title: Fuzzing Tests -role: zettel -syntax: zmk -created: 20221026184320 -modified: 20221102140156 - -The source code contains some simple [[fuzzing tests|https://go.dev/security/fuzz/]]. -You should call them regularly to make sure that the software will cope with unusual input. - -```sh -go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/draw -go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/zettelmark -``` DELETED docs/development/20231218181900.zettel Index: docs/development/20231218181900.zettel ================================================================== --- docs/development/20231218181900.zettel +++ docs/development/20231218181900.zettel @@ -1,117 +0,0 @@ -id: 20231218181900 -title: Development tools -role: zettel -syntax: zmk -created: 20231218181956 -modified: 20231218184500 - -The source code contains some tools to assist the development of Zettelstore. -These are located in the ''tools'' directory. - -Most tool support the generic option ``-v``, which log internal activities. - -Some of the tools can be called easier by using ``make``, that reads in a provided ''Makefile''. - -=== Check -The ""check"" tool automates some testing activities. -It is called via the command line: -``` -# go run tools/check/check.go -``` -There is an additional option ``-r`` to check in advance of a release. - -The following checks are executed: -* Execution of unit tests, like ``go test ./...`` -* Analyze the source code for general problems, as in ``go vet ./...`` -* Tries to find shadowed variable, via ``shadow ./...`` -* Performs some additional checks on the source code, via ``staticcheck ./...`` -* Checks the usage of function parameters and usage of return values, via ``unparam ./...``. - In case the option ''-r'' is set, the check includes exported functions and internal tests. -* In case option ''-r'' is set, the source code is checked against the vulnerability database, via ``govulncheck ./...`` - -Please note, that most of the tools above are not automatically installed in a standard Go distribution. -Use the command ""devtools"" to install them. - -=== Devtools -The following command installs all needed tools: -``` -# go run tooles/devtools/devtools.go -``` -It will also automatically update these tools. - -=== TestAPI -The following command will perform some high-level tests: -```sh -# go run tools/testapi/testapi.go -``` -Basically, a Zettelstore will be started and then API calls will be made to simulate some typical activities with the Zettelstore. - -If a Zettelstore is already running on port 23123, this Zettelstore will be used instead. -Even if the API test should clean up later, some zettel might stay created if a test fails. -This feature is used, if you want to have more control on the running Zettelstore. -You should start it with the following command: -```sh -# go run -race cmd/zettelstore/main.go run -c testdata/testbox/19700101000000.zettel -``` -This allows you to debug failing API tests. - -=== HTMLlint -The following command will check the generated HTML code for validity: -```sh -# go run tools/htmllint/htmllint.go -``` -In addition, you might specify the URL od a running Zettelstore. -Otherwise ''http://localhost:23123'' is used. - -This command fetches first the list of all zettel. -This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification): - -* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel'' -* Check all zettel web views, via the path ''/h/ZID'' -* The info page of all zettel is checked, via path ''/i/ZID'' -* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID'' -* 10 random zettel are checked for a valid create form, via ''/c/ZID'' -* The zettel rename form will be checked for 100 zettel, via ''/b/ZID'' -* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID'' - -Depending on the selected Zettelstore, the command might take a long time. - -You can shorten the time, if you disable any zettel query in the footer. - -=== Build -The ""build"" tool allows to build the software, either for tests or for a release. - -The following command will create a Zettelstore executable for the architecture of the current computer: -```sh -# go tools/build/build.go build -``` -You will find the executable in the ''bin'' directory. - -A full release will be build in the directory ''releases'', containing ZIP files for the computer architectures ""Linux/amd64"", ""Linux/arm"", ""MacOS/arm64"", ""MacOS/amd64"", and ""Windows/amd64"". -In addition, the manual is also build as a ZIP file: -```sh -# go run tools/build/build.go release -``` - -If you just want the ZIP file with the manual, please use: -```sh -# go run tools/build/build.go manual -``` - -In case you want to check the version of the Zettelstore to be build, use: -```sh -# go run tools/build/build.go version -``` - -=== Clean -To remove the directories ''bin'' and ''releases'', as well as all cached Go libraries used by Zettelstore, execute: -```sh -# go run tools/clean/clean.go -``` - -Internally, the following commands are executed -```sh -# rm -rf bin releases -# go clean ./... -# go clean -cache -modcache -testcache -``` Index: docs/manual/00000000000100.zettel ================================================================== --- docs/manual/00000000000100.zettel +++ docs/manual/00000000000100.zettel @@ -1,14 +1,13 @@ id: 00000000000100 title: Zettelstore Runtime Configuration role: configuration syntax: none -created: 00010101000000 -default-copyright: (c) 2020-present by Detlef Stern +default-copyright: (c) 2020-2022 by Detlef Stern default-license: EUPL-1.2-or-later default-visibility: public -footer-zettel: 00001000000100 +footer-html:

Imprint / Privacy

home-zettel: 00001000000000 -modified: 20221205173642 +modified: 20220215171041 site-name: Zettelstore Manual visibility: owner Index: docs/manual/00001000000000.zettel ================================================================== --- docs/manual/00001000000000.zettel +++ docs/manual/00001000000000.zettel @@ -1,13 +1,11 @@ id: 00001000000000 title: Zettelstore Manual role: manual tags: #manual #zettelstore syntax: zmk -created: 20210301190630 -modified: 20231125185455 -show-back-links: false +modified: 20220803183647 * [[Introduction|00001001000000]] * [[Design goals|00001002000000]] * [[Installation|00001003000000]] * [[Configuration|00001004000000]] @@ -20,8 +18,6 @@ * [[Web user interface|00001014000000]] * [[Tips and Tricks|00001017000000]] * [[Troubleshooting|00001018000000]] * Frequently asked questions -Version: {{00001000000001}}. - Licensed under the EUPL-1.2-or-later. DELETED docs/manual/00001000000001.zettel Index: docs/manual/00001000000001.zettel ================================================================== --- docs/manual/00001000000001.zettel +++ docs/manual/00001000000001.zettel @@ -1,8 +0,0 @@ -id: 00001000000001 -title: Manual Version -role: configuration -syntax: zmk -created: 20231002142915 -modified: 20231002142948 - -To be set by build tool. DELETED docs/manual/00001000000100.zettel Index: docs/manual/00001000000100.zettel ================================================================== --- docs/manual/00001000000100.zettel +++ docs/manual/00001000000100.zettel @@ -1,8 +0,0 @@ -id: 00001000000100 -title: Footer Zettel -role: configuration -syntax: zmk -created: 20221205173520 -modified: 20221207175927 - -[[Imprint / Privacy|/home/doc/trunk/www/impri.wiki]] Index: docs/manual/00001002000000.zettel ================================================================== --- docs/manual/00001002000000.zettel +++ docs/manual/00001002000000.zettel @@ -2,21 +2,17 @@ title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230624171152 +modified: 20221018105415 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. @@ -37,7 +33,5 @@ : 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/00001004010000.zettel ================================================================== --- docs/manual/00001004010000.zettel +++ docs/manual/00001004010000.zettel @@ -2,11 +2,11 @@ title: Zettelstore startup configuration role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20240220190138 +modified: 20221018184208 The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options. These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons. For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored. An attacker that is able to change the owner can do anything. @@ -82,21 +82,16 @@ : Configures the network address, where the Zettelstore service is listening for requests. Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port. Default value: ""127.0.0.1:23123"" ; [!log-level|''log-level''] -: Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]]. +: Specify the global [[logging level|00001004059700]] for the whole application, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]]. Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]]. - - Several specifications are separated by the semicolon character (""'';''"", U+003B). - Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level. - + Default: ""info"". - Examples: ""error"" will produce just error messages (e.g. no ""info"" messages); ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components. - - When you are familiar to operate the Zettelstore, you might set the level to ""error"" to receive less noisy messages from the Zettelstore. + When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore. ; [!max-request-size|''max-request-size''] : Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources. The minimum value is 1024. Default: 16777216 (16 MiB). Index: docs/manual/00001004020000.zettel ================================================================== --- docs/manual/00001004020000.zettel +++ docs/manual/00001004020000.zettel @@ -2,52 +2,43 @@ title: Configure the running Zettelstore role: manual tags: #configuration #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20231126180829 -show-back-links: false +modified: 20220827180953 You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]]. This zettel is called __configuration zettel__. -The following metadata keys change the appearance / behavior of Zettelstore. -Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used. -See the full list of [[metadata that may be overwritten|00001004020200]]. +The following metadata keys change the appearance / behavior of Zettelstore: ; [!default-copyright|''default-copyright''] : Copyright value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''. Default: (the empty string). ; [!default-license|''default-license''] : License value to be used when rendering content. Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''. - Default: (the empty string). ; [!default-visibility|''default-visibility''] : Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key. - Default: ""login"". ; [!expert-mode|''expert-mode''] : If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise). This affects most computed zettel. - Default: ""False"". -; [!footer-zettel|''footer-zettel''] -: Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]]. - Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected. - If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer. - - May be [[overwritten|00001004020200]] in a user zettel. - - Default: (an invalid zettel identifier) +; [!footer-html|''footer-html''] +: Contains some HTML code that will be included into the footer of each Zettelstore web page. + It only affects the [[web user interface|00001014000000]]. + Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected. + Default: (the empty string). ; [!home-zettel|''home-zettel''] : Specifies the identifier of the zettel, that should be presented for the default view / home view. If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown. - - May be [[overwritten|00001004020200]] in a user zettel. - +; [!marker-external|''marker-external''] +: Some HTML code that is displayed after a [[reference to external material|00001007040310]]. + Default: ""&\#10138;"", to display a ""➚"" sign. ; [!lang|''lang''] : Language to be used when displaying content. Default: ""en"". @@ -56,34 +47,17 @@ Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]]. ; [!max-transclusions|''max-transclusions''] : Maximum number of indirect transclusion. This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]]. - Default: ""1024"". -; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-subordinate-links|''show-subordinate-links''], [!show-successor-links|''show-successor-links''] -: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel. - This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''subordinates''|00001006020000#subordinates]], and [[''successors''|00001006020000#successors]]. - - These configuration keys may be used to show, not to show, or to close the list of referenced zettel. - - Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list). - - Default: """". - - May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user. - Alternatively, it may be overwritten in a zettel, so that that the setting will affect only the given zettel. - - This zettel is an example of a zettel that sets ''show-back-links'' to ""false"". ; [!site-name|''site-name''] : Name of the Zettelstore instance. Will be used when displaying some lists. - Default: ""Zettelstore"". ; [!yaml-header|''yaml-header''] : If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n''). - Default: ""False"". You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata. ; [!zettel-file-syntax|''zettel-file-syntax''] : If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files: DELETED docs/manual/00001004020200.zettel Index: docs/manual/00001004020200.zettel ================================================================== --- docs/manual/00001004020200.zettel +++ docs/manual/00001004020200.zettel @@ -1,22 +0,0 @@ -id: 00001004020200 -title: Runtime configuration data that may be user specific or zettel specific -role: manual -tags: #configuration #manual #zettelstore -syntax: zmk -created: 20221205155521 -modified: 20231126180752 - -Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]]. -A subset of those may be overwritten in zettel that is currently used. -This allows to specify user specific or zettel specific behavior. - -The following metadata keys are supported to provide a more specific behavior: - -|=Key|User:|Zettel:|Remarks -|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N| -|[[''home-zettel''|00001004020000#home-zettel]]|Y|N| -|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful -|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y| -|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y| -|[[''show-subordinate-links''|00001004020000#show-subordinate-links]]|Y|Y| -|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y| Index: docs/manual/00001004050000.zettel ================================================================== --- docs/manual/00001004050000.zettel +++ docs/manual/00001004050000.zettel @@ -1,12 +1,11 @@ id: 00001004050000 title: Command line parameters role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20221128161932 +modified: 20220805174626 Zettelstore is not just a service that provides services of a zettelkasten. It allows to some tasks to be executed at the command line. Typically, the task (""sub-command"") will be given at the command line as the first parameter. @@ -26,13 +25,6 @@ * [[``zettelstore run``|00001004051000]] to start the Zettelstore service. * [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI. * [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services. * [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]]. -Every sub-command allows the following command line options: -; [!h|''-h''] (or ''--help'') -: Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help''). -; [!l|''-l LOGSPEC''] -: Makes the given logging level specification effective for this command. - Details, including syntax, can be found in the description for the [[''log-level''|00001004010000#log-level]] key of the startup configuration. - To measure potential bottlenecks within the software Zettelstore, there are some [[command line flags for profiling the application|00001004059900]]. Index: docs/manual/00001004051100.zettel ================================================================== --- docs/manual/00001004051100.zettel +++ docs/manual/00001004051100.zettel @@ -1,12 +1,11 @@ id: 00001004051100 title: The ''run-simple'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20221128161922 +modified: 20220724162843 === ``zettelstore run-simple`` This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon. It is s simplified variant of the [[''run'' sub-command|00001004051000]]. Index: docs/manual/00001004051200.zettel ================================================================== --- docs/manual/00001004051200.zettel +++ docs/manual/00001004051200.zettel @@ -1,12 +1,11 @@ id: 00001004051200 title: The ''file'' sub-command role: manual tags: #command #configuration #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230316182711 +modified: 20220423131738 Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout. This allows Zettelstore to render files manually. ``` zettelstore file [-t FORMAT] [file-1 [file-2]] @@ -14,17 +13,16 @@ ; ''-t FORMAT'' : Specifies the output format. Supported values are: [[''html''|00001012920510]] (default), - [[''md''|00001012920513]], - [[''shtml''|00001012920525]], - [[''sz''|00001012920516]], + [[''sexpr''|00001012920516]], [[''text''|00001012920519]], + [[''zjson''|00001012920503]], and [[''zmk''|00001012920522]]. ; ''file-1'' : Specifies the file name, where at least metadata is read. If ''file-2'' is not given, the zettel content is also read from here. ; ''file-2'' : File name where the zettel content is stored. If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin. Index: docs/manual/00001004059700.zettel ================================================================== --- docs/manual/00001004059700.zettel +++ docs/manual/00001004059700.zettel @@ -1,24 +1,27 @@ id: 00001004059700 title: List of supported logging levels role: manual tags: #configuration #manual #zettelstore syntax: zmk -created: 20211204182643 -modified: 20240221134619 +modified: 20220113183606 Zettelstore supports various levels of logging output. This allows you to see the inner workings of Zettelstore, or to avoid it. Each level has an associated name and number. A lower number signals more logging output. -|= Name | Number :| Description +|= Name | Number >| Description | Trace | 1 | Show most of the inner workings | Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]]. -| Info | 3 | Display information about an event. In most cases, there is no required action expected from you. -| Error | 4 | Notify about an error, which was handled automatically. Something is broken. User intervention may be required, some important functionality may be disabled. Monitor the application. -| Mandatory | 5 | Important message will be shown, e.g. the Zettelstore version at startup time. -| Disabled | 6 | No messages will be shown +| Sense | 3 | Display sensing events, which are not essential information. +| Info | 4 | Display information about an event. In most cases, there is no required action expected from you. +| Warn | 5 | Show a warning, i.e. an event that might become an error or more. Mostly invalid data. +| Error | 6 | Notify about an error, which was handled automatically. Something is broken. User intervention is not required, in most cases. Monitor the application. +| Fatal | 7 | Notify about a significant error that cannot be handled automatically. At least some important functionality is disabled. +| Panic | 8 | The application is in an uncertain state and notifies you about its panic. At least some part of the application is possibly restarted. +| Mandatory | 9 | Important message will be shown, e.g. the Zettelstore version at startup time. +| Disabled | 10 | No messages will be shown If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown. -E.g. if you set the logging level to ""error"", no ""trace"", ""debug"", and ""info"" messages are shown, but ""error"" and ""mandatory"" messages. +E.g. if you set the logging level to ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages. 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: 20240318115839 +modified: 20220909180240 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore @@ -14,11 +14,10 @@ | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore | [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore | [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore | [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content | [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages -| [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage | [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process | [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more | [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore | [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]] | [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]] @@ -28,26 +27,17 @@ | [[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 -| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates -| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn 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 -| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]"" -| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]"" -| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]"" -| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]"" -| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu -| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]"" +| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu +| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] -| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]] -| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. -In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]]. **Important:** All identifier may change until a stable version of the software is released. Index: docs/manual/00001006000000.zettel ================================================================== --- docs/manual/00001006000000.zettel +++ docs/manual/00001006000000.zettel @@ -1,12 +1,11 @@ id: 00001006000000 title: Layout of a Zettel role: manual tags: #design #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230403123541 +modified: 20220724165931 A zettel consists of two parts: the metadata and the zettel content. Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore. The zettel content is, well, the actual content. In many cases, the content is in plain text form. @@ -28,22 +27,22 @@ Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.]. There is support for a graphical format with a text representation: SVG. And there is support for some binary image formats, like GIF, PNG, and JPEG. === Plain, parsed, and evaluated zettel -Zettelstore may present your zettel in various forms, typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''. +Zettelstore may present your zettel in various forms. One way is to present the zettel as it was read by Zettelstore. -This is called ""[[plain zettel|00001012053300]]"". +This is called ""[[plain zettel|00001003000000#plain]]"", typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''. The second way is to present the zettel as it was recognized by Zettelstore. -This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''. +This is called ""[[parsed zettel|00001012053600]]"", typically retrieved with the [[endpoint|00001012920000]] ''/p/{ID}''. Such a zettel was read and analyzed. It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.] -However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding. +However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", typically retrieved with the [[endpoint|00001012920000]] ''/v/{ID}''. The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel. It can also be presented in various encoding, including the ""zmk"" encoding. Evaluations also applies to metadata of a zettel, if appropriate. Please note, that searching for content is based on parsed zettel. Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content. However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel. Index: docs/manual/00001006010000.zettel ================================================================== --- docs/manual/00001006010000.zettel +++ docs/manual/00001006010000.zettel @@ -1,26 +1,24 @@ id: 00001006010000 title: Syntax of Metadata role: manual tags: #manual #syntax #zettelstore syntax: zmk -created: 20210126175322 -modified: 20240219193158 +modified: 20220218131923 The metadata of a zettel is a collection of key-value pairs. The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]). The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed. It begins at the first position of a new line. -Uppercase letters of a key are translated to their lowercase equivalence. A key is separated from its value either by * a colon character (""'':''""), * a non-empty sequence of space characters, * a sequence of space characters, followed by a colon, followed by a sequence of space characters. -A value is a sequence of printable characters. +A Value is a sequence of printable characters. If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters. The rest of the following line will be interpreted as the next part of the value. There can be more than one continuation line for a value. A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line. 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: 20230704161159 +modified: 20221004134841 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]]. @@ -42,21 +42,12 @@ It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key. It is only used for zettel with a ''role'' value of ""user"". ; [!dead|''dead''] : Property that contains all references that does __not__ identify a zettel. -; [!expire|''expire''] -: A user-entered time stamp that document the point in time when the zettel should expire. - When a zettel is expires, Zettelstore does nothing. - It is up to you to define required actions. - ''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. @@ -92,35 +83,24 @@ It can be used for [[sorting|00001007700000]] zettel based on their publication date. It is a computed value. There is no need to set it via Zettelstore. -; [!query|''query''] -: Stores the [[query|00001007031140]] that was used to create the zettel. - This is for future reference. ; [!read-only|''read-only''] : Marks a zettel as read-only. The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not. ; [!role|''role''] : Defines the role of the zettel. Can be used for selecting zettel. See [[supported zettel roles|00001006020100]]. If not given, it is ignored. -; [!subordinates|''subordinates''] -: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value. ; [!successors|''successors''] : Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value. Therefore, it references all zettel that contain a new version of the content and/or metadata. In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons. In most cases, zettel referencing the current zettel should be updated to reference a successor zettel. The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel. -; [!summary|''summary''] -: Summarizes the content of the zettel. - You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup. -; [!superior|''superior''] -: Specifies a zettel that is conceptually a superior zettel. - This might be a more abstract zettel, or a zettel that should be higher in a hierarchy. ; [!syntax|''syntax''] : Specifies the syntax that should be used for interpreting the zettel. The zettel about [[other markup languages|00001008000000]] defines supported values. If it is not given, it defaults to ''plain''. ; [!tags|''tags''] @@ -127,10 +107,12 @@ : Contains a space separated list of tags to describe the zettel further. Each Tag must begin with the number sign character (""''#''"", U+0023). ; [!title|''title''] : Specifies the title of the zettel. If not given, the value of [[''id''|#id]] will be used. + + You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup. ; [!url|''url''] : Defines an URL / URI for this zettel that possibly references external material. One use case is to specify the document that the current zettel comments on. The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template. ; [!useless-files|''useless-files''] Index: docs/manual/00001006020100.zettel ================================================================== --- docs/manual/00001006020100.zettel +++ docs/manual/00001006020100.zettel @@ -1,34 +1,24 @@ id: 00001006020100 title: Supported Zettel Roles role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -created: 20210126175322 -modified: 20231129173620 +modified: 20220623183234 The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing. You are free to define your own roles. It is allowed to set an empty value or to omit the role. Some roles are defined for technical reasons: ; [!configuration|''configuration''] -: A zettel that contains some configuration data / information for the Zettelstore. +: A zettel that contains some configuration data for the Zettelstore. Most prominent is [[00000000000100]], as described in [[00001004020000]]. ; [!manual|''manual''] : All zettel that document the inner workings of the Zettelstore software. This role is only used in this specific Zettelstore. -; [!role|''role''] -: A zettel with the role ""role"" and a title, which names a [[role|00001006020000#role]], is treated as a __role zettel__. - Basically, role zettel describe the role, and form a hierarchiy of meta-roles. -; [!tag|''tag''] -: A zettel with the role ""tag"" and a title, which names a [[tag|00001006020000#tags]], is treated as a __tag zettel__. - Basically, tag zettel describe the tag, and form a hierarchiy of meta-tags. -; [!zettel|''zettel''] -: A zettel that contains your own thoughts. - The real reason to use this software. If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles: ; [!note|''note''] : A small note, to remember something. @@ -35,9 +25,9 @@ Notes are not real zettel, they just help to create a real zettel. Think of them as Post-it notes. ; [!literature|''literature''] : Contains some remarks about a book, a paper, a web page, etc. You should add a citation key for citing it. -; ''zettel'' -: (as described above) +; [!zettel|''zettel''] +: A real zettel that contains your own thoughts. However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ... Index: docs/manual/00001006030000.zettel ================================================================== --- docs/manual/00001006030000.zettel +++ docs/manual/00001006030000.zettel @@ -1,22 +1,20 @@ id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk -created: 20210126175322 -modified: 20240219161909 +modified: 20220304114106 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. |=Suffix|Type -| ''-date'' | [[Timestamp|00001006034500]] | ''-number'' | [[Number|00001006033000]] | ''-role'' | [[Word|00001006035500]] -| ''-time'' | [[Timestamp|00001006034500]] +| ''-set'' | [[WordSet|00001006036000]] | ''-title'' | [[Zettelmarkup|00001006036500]] | ''-url'' | [[URL|00001006035000]] | ''-zettel'' | [[Identifier|00001006032000]] | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] @@ -23,11 +21,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 value|00001007706000]] when selecting some zettel. +There is also a rule how values are matched, e.g. against a search term when selecting some zettel. And there is a rule how values compare for sorting. * [[Credential|00001006031000]] * [[EString|00001006031500]] * [[Identifier|00001006032000]] @@ -36,6 +34,7 @@ * [[String|00001006033500]] * [[TagSet|00001006034000]] * [[Timestamp|00001006034500]] * [[URL|00001006035000]] * [[Word|00001006035500]] +* [[WordSet|00001006036000]] * [[Zettelmarkup|00001006036500]] Index: docs/manual/00001006031000.zettel ================================================================== --- docs/manual/00001006031000.zettel +++ docs/manual/00001006031000.zettel @@ -2,19 +2,19 @@ title: Credential Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175515 +modified: 20220914130324 Values of this type denote a credential value, e.g. an encrypted password. === Allowed values All printable characters are allowed. Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore. -=== Query comparison +=== Query operators A credential never compares to any other value. A comparison will never match in any way. === Sorting If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead. Index: docs/manual/00001006031500.zettel ================================================================== --- docs/manual/00001006031500.zettel +++ docs/manual/00001006031500.zettel @@ -2,24 +2,24 @@ title: EString Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175525 +modified: 20220914130448 Values of this type are just a sequence of character, possibly an empty sequence. An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].] === Allowed values All printable characters are allowed. -=== Query comparison +=== Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. Index: docs/manual/00001006032000.zettel ================================================================== --- docs/manual/00001006032000.zettel +++ docs/manual/00001006032000.zettel @@ -2,30 +2,20 @@ title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230612183459 +modified: 20220914134914 Values of this type denote a [[zettel identifier|00001006050000]]. === Allowed values Must be a sequence of 14 digits (""0""--""9""). -=== Query comparison -[[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. - +=== Query operator +Comparison is done with the string representation of the identifiers. For example, ""000010"" matches ""[[00001006032000]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. If both values are identifiers, this works well because both have the same length. Index: docs/manual/00001006032500.zettel ================================================================== --- docs/manual/00001006032500.zettel +++ docs/manual/00001006032500.zettel @@ -2,21 +2,21 @@ title: IdentifierSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175551 +modified: 20220914131354 Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]]. A set is different to a list, as no duplicate values are allowed. === Allowed values Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters. -=== Query comparison +=== Query operator A value matches an identifier set value, if the value matches any of the identifier set values. For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006033000.zettel ================================================================== --- docs/manual/00001006033000.zettel +++ docs/manual/00001006033000.zettel @@ -2,23 +2,17 @@ title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230612183900 +modified: 20220914131211 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 -[[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"". +=== Query operator +All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"". === Sorting Sorting is done by comparing the numeric values. Index: docs/manual/00001006033500.zettel ================================================================== --- docs/manual/00001006033500.zettel +++ docs/manual/00001006033500.zettel @@ -2,23 +2,23 @@ title: String Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175633 +modified: 20220914130505 Values of this type are just a sequence of character, but not an empty sequence. === Allowed values All printable characters are allowed. There must be at least one such character. -=== Query comparison +=== Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting To sort two values, the underlying encoding is used to determine which value is less than the other. Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``. Comparison is done character-wise by finding the first difference in the respective character sequence. For example, ``abc > aBc``. Index: docs/manual/00001006034000.zettel ================================================================== --- docs/manual/00001006034000.zettel +++ docs/manual/00001006034000.zettel @@ -2,11 +2,11 @@ title: TagSet Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175642 +modified: 20220914131048 Values of this type denote a (sorted) set of tags. A set is different to a list, as no duplicate values are allowed. @@ -14,10 +14,10 @@ Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character. Tags are separated by space characters. All characters are mapped to their lower case values. -=== Query comparison +=== Query operator All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006034500.zettel ================================================================== --- docs/manual/00001006034500.zettel +++ docs/manual/00001006034500.zettel @@ -2,36 +2,26 @@ title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20231030182858 +modified: 20220914130919 Values of this type denote a point in time. === Allowed values -Must be a sequence of 4, 6, 8, 10, 12, or 14 digits (""0""--""9"") (similar to an [[Identifier|00001006032000]]), with the restriction that it must conform to the pattern ""YYYYMMDDhhmmss"". +Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"". * YYYY is the year, * MM is the month, * DD is the day, * hh is the hour, * mm is the minute, * ss is the second. -If the sequence is less than 14 digits, they are expanded with the following rule: - -* YYYY is expanded to YYYY0101000000 -* YYYYMM is expanded to YYYYMM01000000 -* YYYYMMDD is expanded to YYYYMMDD000000 -* YYYYMMDDhh is expanded to YYYYMMDDhh0000 -* YYYYMMDDhhmm is expanded to YYYYMMDDhhmm00 - -=== Query comparison -[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters. -Then, they are treated as timestamp data, as describe above, if they contain 4, 6, 8, 10, or 12 digits. - -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. +=== Query operator +All comparisons assume that up to 14 digits are given. === Sorting -Sorting is done by comparing the possibly expanded values. +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/00001006035000.zettel ================================================================== --- docs/manual/00001006035000.zettel +++ docs/manual/00001006035000.zettel @@ -2,18 +2,18 @@ title: URL Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175725 +modified: 20220914130809 Values of this type denote an URL. === Allowed values All characters of an URL / URI are allowed. -=== Query comparison +=== Query operator All comparisons are done case-insensitive. For example, ""hello"" is the suffix of ""http://example.com/Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006035500.zettel ================================================================== --- docs/manual/00001006035500.zettel +++ docs/manual/00001006035500.zettel @@ -2,19 +2,19 @@ title: Word Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175735 +modified: 20220914130655 Values of this type denote a single word. === Allowed values Must be a non-empty sequence of characters, but without the space character. All characters are mapped to their lower case values. -=== Query comparison +=== Query operator All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"". === Sorting Sorting is done by comparing the [[String|00001006033500]] values. ADDED docs/manual/00001006036000.zettel Index: docs/manual/00001006036000.zettel ================================================================== --- docs/manual/00001006036000.zettel +++ docs/manual/00001006036000.zettel @@ -0,0 +1,20 @@ +id: 00001006036000 +title: WordSet Key Type +role: manual +tags: #manual #meta #reference #zettel #zettelstore +syntax: zmk +created: 20210212135017 +modified: 20220914130725 + +Values of this type denote a (sorted) set of [[words|00001006035500]]. + +A set is different to a list, as no duplicate values are allowed. + +=== Allowed values +Must be a sequence of at least one word, separated by space characters. + +=== Query operator +All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"". + +=== Sorting +Sorting is done by comparing the [[String|00001006033500]] values. Index: docs/manual/00001006036500.zettel ================================================================== --- docs/manual/00001006036500.zettel +++ docs/manual/00001006036500.zettel @@ -2,19 +2,19 @@ title: Zettelmarkup Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 -modified: 20230419175441 +modified: 20220914135405 Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]]. === Allowed values All printable characters are allowed. There must be at least one such character. -=== Query comparison +=== Query operator Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers. Letters are mapped to the corresponding lower-case value. For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"". Index: docs/manual/00001007000000.zettel ================================================================== --- docs/manual/00001007000000.zettel +++ docs/manual/00001007000000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210126175322 -modified: 20221209192105 +modified: 20221018104725 Zettelmarkup is a rich plain-text based markup language for writing zettel content. Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel. Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer. @@ -37,6 +37,5 @@ * [[Inline-structured element|00001007040000]] * [[Attributes|00001007050000]] * [[Query expressions|00001007700000]] * [[Summary of formatting characters|00001007800000]] * [[Tutorial|00001007900000]] -* [[Cheat Sheet|00001007990000]] Index: docs/manual/00001007030900.zettel ================================================================== --- docs/manual/00001007030900.zettel +++ docs/manual/00001007030900.zettel @@ -1,21 +1,20 @@ id: 00001007030900 title: Zettelmarkup: Comment Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230807170858 +modified: 20220218130330 Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted. While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.]. Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks. Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters. The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment. -When rendered to a symbolic expression, the comment block will not be ignored but it will output some text. +When rendered to JSON, the comment block will not be ignored but it will output some JSON text. Same for other renderer. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. Index: docs/manual/00001007031140.zettel ================================================================== --- docs/manual/00001007031140.zettel +++ docs/manual/00001007031140.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Query Transclusion role: manual tags: #manual #search #zettelmarkup #zettelstore syntax: zmk created: 20220809132350 -modified: 20240219161800 +modified: 20221014164027 A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''. The line must literally start with the sequence ''{{{query:''. Everything after this prefix is interpreted as a [[query expression|00001007700000]]. @@ -45,25 +45,16 @@ : Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed. The document is embedded into the referencing zettel. ; ''RSS'' (aggregate) : Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed. The document is embedded into the referencing zettel. -; ''KEYS'' (aggregate) -: Emit a list of all metadata keys, together with the number of zettel having the key. -; ''REDIRECT'', ''REINDEX'' (aggregate) -: Will be ignored. - These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful). -; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates) +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. - The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.]. + The key can be given in any letter case. -To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. -In this case the list of selected zettel is returned. - -Example: ```zmk {{{query:tags:#search | tags}}} ``` This is a tag cloud of all tags that are used together with the tag #search: :::example {{{query:tags:#search | tags}}} ::: Index: docs/manual/00001007031300.zettel ================================================================== --- docs/manual/00001007031300.zettel +++ docs/manual/00001007031300.zettel @@ -1,22 +1,21 @@ id: 00001007031300 title: Zettelmarkup: Evaluation Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20220310184916 -modified: 20230109105402 +modified: 20220311120658 Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software. They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line. You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters. The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".] -The main reason for an evaluation block is to be used with external software via the [[sz encoding|00001012920516]]. +The main reason for an evaluation block is to be used with external software via the [[ZJSON encoding|00001012920503]]. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter some tilde characters in the text that should not be interpreted. Index: docs/manual/00001007031400.zettel ================================================================== --- docs/manual/00001007031400.zettel +++ docs/manual/00001007031400.zettel @@ -1,12 +1,11 @@ id: 00001007031400 title: Zettelmarkup: Math-mode Blocks role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20220311173226 -modified: 20230109105340 +modified: 20220311182505 Math-mode blocks are used to enter mathematical formulas / equations in a display style mode. Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software. They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line. @@ -14,11 +13,11 @@ A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423). If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value. It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content. Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax. Not all syntax values are supported by Zettelstore.[^Currently: none.] -External software might support several values via the [[sz encoding|00001012920516]]. +External software might support several values via the [[ZJSON encoding|00001012920503]]. Any other character in this line will be ignored Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line. This allows to enter some dollar-sign characters in the text that should not be interpreted. Index: docs/manual/00001007040100.zettel ================================================================== --- docs/manual/00001007040100.zettel +++ docs/manual/00001007040100.zettel @@ -1,12 +1,11 @@ id: 00001007040100 title: Zettelmarkup: Text Formatting role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20210126175322 -modified: 20231113191353 +modified: 20220218131003 Text formatting is the way to make your text visually different. Every text formatting element begins with two same characters. It ends when these two same characters occur the second time. It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character. @@ -28,10 +27,7 @@ * The comma character (""'',''"", U+002C) produces sub-scripted text. ** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}. * The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]]. ** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}. ** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}. -* The number sign (""''#''"", U+0023) marks the text visually, where the mark does not belong to the text itself. - It is typically used to highlight some text that is important for you, but was not important for the original author. -** Example: ``abc ##def## ghi`` is rendered in HTML as: ::abc ##def## ghi::{=example}. * The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements. ** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi. Index: docs/manual/00001007040310.zettel ================================================================== --- docs/manual/00001007040310.zettel +++ docs/manual/00001007040310.zettel @@ -2,15 +2,14 @@ title: Zettelmarkup: Links role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210810155955 -modified: 20221024173849 +modified: 20220922154843 There are two kinds of links, regardless of links to (internal) other zettel or to (external) material. Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D). -If the content starts with more than two left square bracket characters, all but the last two will be treated as text. The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``. The text is a sequence of [[inline elements|00001007040000]]. However, it should not contain links itself. Index: docs/manual/00001007040320.zettel ================================================================== --- docs/manual/00001007040320.zettel +++ docs/manual/00001007040320.zettel @@ -1,21 +1,19 @@ id: 00001007040320 title: Zettelmarkup: Inline Embedding / Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20210810155955 -modified: 20221024173926 +modified: 20220803183936 To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]]. Both contain a reference specification and optionally some text. In contrast to a link, the specification of embedded material must currently resolve to some kind of real content. This content replaces the embed specification. An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D). The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link. -If the content starts with more than two left curly bracket characters, all but the last two will be treated as text. One difference to a link: if the text was not given, an empty string is assumed. The reference must point to some content, either zettel content or URL-referenced content. If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored. Index: docs/manual/00001007040322.zettel ================================================================== --- docs/manual/00001007040322.zettel +++ docs/manual/00001007040322.zettel @@ -1,22 +1,20 @@ id: 00001007040322 title: Zettelmarkup: Image Embedding role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk -created: 20210811154251 -modified: 20221112111054 +modified: 20220214180955 Image content is assumed, if an URL is used or if the referenced zettel contains an image. Supported formats are: * Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]]. * Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]]. * JPEG / JPG, defined by the __Joint Photographic Experts Group__. * Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]] -* WebP, defined by [[Google|https://developers.google.com/speed/webp]] If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities. [[Attributes|00001007050000]] are supported. They must follow the last right curly bracket character immediately. Index: docs/manual/00001007040324.zettel ================================================================== --- docs/manual/00001007040324.zettel +++ docs/manual/00001007040324.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: Inline-mode Transclusion role: manual tags: #manual #zettelmarkup #zettelstore syntax: zmk created: 20210811154251 -modified: 20231222164501 +modified: 20220926183509 Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content. For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]). Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements. @@ -36,14 +36,14 @@ ** Just specifying the fragment identifier will reference something in the current page. This is not allowed, to prevent a possible endless recursion. * If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered. - Example: ``{{//z/00000000040001}}{alt=Emoji}`` is rendered as ::{{//z/00000000040001}}{alt=Emoji}::{=example} + Example: ``{{/z/00000000040001}}`` is rendered as ::{{/z/00000000040001}}::{=example} If no inline-structured elements are found, the transclude specification is replaced by an error message. To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited. The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel. === See also [[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]]. Index: docs/manual/00001007700000.zettel ================================================================== --- docs/manual/00001007700000.zettel +++ docs/manual/00001007700000.zettel @@ -1,30 +1,49 @@ id: 00001007700000 -title: Query Expression +title: Query expression role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 -modified: 20230731161954 +modified: 20220913135434 A query expression allows you to search for specific zettel and to perform some actions on them. -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. +You may select zettel based on a full-text search, based on specific metadata values, or both. -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 consists of a __search expression__ and of an optional __action list__. +Both are separated by a vertical bar character (""''|''"", U+007C). A query expression follows a [[formal syntax|00001007780000]]. -* [[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: +=== 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 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]]. DELETED docs/manual/00001007701000.zettel Index: docs/manual/00001007701000.zettel ================================================================== --- docs/manual/00001007701000.zettel +++ docs/manual/00001007701000.zettel @@ -1,26 +0,0 @@ -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 @@ -1,12 +1,11 @@ id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk -created: 20220805150154 -modified: 20230925173539 +modified: 20220821163727 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__): @@ -17,14 +16,10 @@ 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. * A metadata key followed by ""''?''"" or ""''!?''"". @@ -34,21 +29,14 @@ Since search literals may be negated, it is possible to form any boolean search expression. Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]]. It has no effect on the following search terms initiated with a special uppercase word. -* The string ''PICK'', followed by a non-empty sequence of spaces and a number greater zero (called ""N""). - - This will pick randomly N elements of the result list, preserving the order of that list. - A zero value of N will produce the same result as if nothing was specified. - If specified multiple times, the lower value takes precedence. - - Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''. * The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list. If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed. - Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSE published'' will return a reversed result order. + Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order. An explicit order field will take precedence over the random order described below. If no random order is effective, a ``ORDER REVERSE id`` will be added. This makes the sort stable. @@ -86,7 +74,6 @@ * A search term containing no [[search operator character|00001007705000]] is treated as a full-text search. * The first search operator character found in a search term divides the term into two pieces. If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search. * Otherwise, the search term is treated as a full-text search. -If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. -For example, ''ORDER 123'' will search for a zettel containing the strings ""ORDER"" (case-insensitive) and ""123"". +If a term like ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"". Index: docs/manual/00001007705000.zettel ================================================================== --- docs/manual/00001007705000.zettel +++ docs/manual/00001007705000.zettel @@ -1,44 +1,32 @@ id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk -created: 20220805150154 -modified: 20230612180539 +modified: 20220819194709 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 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 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 18 possible combinations: +* The exclamation mark character (""!"", U+0021) negates the meaning +* 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 colon character (""'':''"", U+003A) compares on equal words (""has operator"") +* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"") + +Since the exclamation mark character can be combined with the other, there are 10 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 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 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 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. DELETED docs/manual/00001007710000.zettel Index: docs/manual/00001007710000.zettel ================================================================== --- docs/manual/00001007710000.zettel +++ docs/manual/00001007710000.zettel @@ -1,17 +0,0 @@ -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. DELETED docs/manual/00001007720000.zettel Index: docs/manual/00001007720000.zettel ================================================================== --- docs/manual/00001007720000.zettel +++ docs/manual/00001007720000.zettel @@ -1,19 +0,0 @@ -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]] DELETED docs/manual/00001007720300.zettel Index: docs/manual/00001007720300.zettel ================================================================== --- docs/manual/00001007720300.zettel +++ docs/manual/00001007720300.zettel @@ -1,41 +0,0 @@ -id: 00001007720300 -title: Query: Context Directive -role: manual -tags: #manual #search #zettelstore -syntax: zmk -created: 20230707204706 -modified: 20240209191045 - -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: -* ''FULL'': additionally search for zettel with the same tags, -* ''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 subordinate zettel or single superior zettel has the cost of the originating zettel, plus 1.2. -* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus seven. -* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus two. -* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the four choices above and multiplied with roughly a linear-logarithmic value based on the size of the set. -* A zettel with the same tag, has the cost of the originating zettel, plus a linear-logarithmic number based on the number of zettel with this tag. - If a zettel belongs to more than one tag compared with the current zettel, there is a discount of 90% per additional tag. - This only applies if the ''FULL'' directive was specified. - -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 effort. DELETED docs/manual/00001007720600.zettel Index: docs/manual/00001007720600.zettel ================================================================== --- docs/manual/00001007720600.zettel +++ docs/manual/00001007720600.zettel @@ -1,12 +0,0 @@ -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]]. DELETED docs/manual/00001007720900.zettel Index: docs/manual/00001007720900.zettel ================================================================== --- docs/manual/00001007720900.zettel +++ docs/manual/00001007720900.zettel @@ -1,39 +0,0 @@ -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 -```` DELETED docs/manual/00001007721200.zettel Index: docs/manual/00001007721200.zettel ================================================================== --- docs/manual/00001007721200.zettel +++ docs/manual/00001007721200.zettel @@ -1,44 +0,0 @@ -id: 00001007721200 -title: Query: Unlinked Directive -role: manual -tags: #manual #zettelstore -syntax: zmk -created: 20211119133357 -modified: 20230928190540 - -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:"". DELETED docs/manual/00001007770000.zettel Index: docs/manual/00001007770000.zettel ================================================================== --- docs/manual/00001007770000.zettel +++ docs/manual/00001007770000.zettel @@ -1,21 +0,0 @@ -id: 00001007770000 -title: Query: Action List -role: manual -tags: #manual #search #zettelstore -syntax: zmk -created: 20230707205246 -modified: 20240219161813 - -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]] or [[TagSet|00001006034000]]. - -To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. -In this case the list of selected zettel is returned. Index: docs/manual/00001007780000.zettel ================================================================== --- docs/manual/00001007780000.zettel +++ docs/manual/00001007780000.zettel @@ -2,56 +2,29 @@ title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20240219155949 +modified: 20220913134024 ``` -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 := "FULL" - | "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" - | "RANDOM" - | "PICK" SPACE+ PosInt - | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey - | "OFFSET" SPACE+ PosInt - | "LIMIT" SPACE+ PosInt. -SearchValue := Word. -SearchKey := MetadataKey. -SearchOperator := '!' - | ('!')? ('~' | ':' | '[' | '}'). -ExistOperator := '?' - | '!' '?'. -PosInt := '0' - | ('1' .. '9') DIGIT*. -ActionExpression := '|' (Word (SPACE+ Word)*)? -Action := Word - | 'ATOM' - | 'KEYS' - | 'N' NO-SPACE* - | 'MAX' PosInt - | 'MIN' PosInt - | 'REDIRECT' - | 'REINDEX' - | 'RSS' - | 'TITLE' (SPACE Word)* . -Word := NO-SPACE NO-SPACE* +QueryExpression := SearchExpression ActionExpression? +SearchExpression := SearchTerm (SPACE+ SearchTerm)*. +SearchTerm := SearchOperator? SearchValue + | SearchKey SearchOperator SearchValue? + | SearchKey ExistOperator + | "OR" + | "RANDOM" + | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey + | "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,18 +2,15 @@ title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 -modified: 20240216003702 +modified: 20220917174956 |= 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 -| [[query:PICK 40]] | 40 random zettel, ordered by zettel identifier +| [[query:RANDOM LIMIT 40]] | 40 random zettel | [[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:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]] -| [[query:PICK 1 | REDIRECT]] | Redirect to a random zettel Index: docs/manual/00001007800000.zettel ================================================================== --- docs/manual/00001007800000.zettel +++ docs/manual/00001007800000.zettel @@ -1,19 +1,18 @@ id: 00001007800000 title: Zettelmarkup: Summary of Formatting Characters role: manual tags: #manual #reference #zettelmarkup #zettelstore syntax: zmk -created: 20220805150154 -modified: 20231113191330 +modified: 20220810095559 The following table gives an overview about the use of all characters that begin a markup element. |= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] < | ''!'' | (free) | (free) | ''"'' | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]] -| ''#'' | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]] +| ''#'' | [[Ordered list|00001007030200]] | [[Tag|00001007040000]] | ''$'' | (reserved) | (reserved) | ''%'' | [[Comment block|00001007030900]] | [[Comment|00001007040000]] | ''&'' | (free) | [[Entity|00001007040000]] | ''\''' | (free) | [[Computer input|00001007040200]] | ''('' | (free) | (free) Index: docs/manual/00001007900000.zettel ================================================================== --- docs/manual/00001007900000.zettel +++ docs/manual/00001007900000.zettel @@ -1,10 +1,10 @@ id: 00001007900000 title: Zettelmarkup: Tutorial role: manual -tags: #manual #tutorial #zettelmarkup +tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 -modified: 20221209191820 +modified: 20220811135314 * [[First steps|00001007903000]]: learn something about paragraphs, emphasized text, and lists. * [[Second steps|00001007906000]]: know about links, thematic breaks, and headings. Index: docs/manual/00001007903000.zettel ================================================================== --- docs/manual/00001007903000.zettel +++ docs/manual/00001007903000.zettel @@ -2,11 +2,11 @@ title: Zettelmarkup: First Steps role: manual tags: #manual #tutorial #zettelmarkup #zettelstore syntax: zmk created: 20220810182917 -modified: 20231201135849 +modified: 20220926183359 [[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations. Some characters have a special meaning, but you have to enter them is a defined way to see a visible change. Zettelmarkup is designed to be used for zettel, which are relatively short. It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example. @@ -29,11 +29,11 @@ | ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote. You probably see a principle. One nice thing about the quotation mark characters: they are rendered according to the current language. -Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}. +Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}. You will see later, how to change the current language. === Lists Quite often, text consists of lists. Zettelmarkup supports different types of lists. DELETED docs/manual/00001007990000.zettel Index: docs/manual/00001007990000.zettel ================================================================== --- docs/manual/00001007990000.zettel +++ docs/manual/00001007990000.zettel @@ -1,57 +0,0 @@ -id: 00001007990000 -title: Zettelmarkup: Cheat Sheet -role: manual -tags: #manual #reference #zettelmarkup -syntax: zmk -created: 20221209191905 -modified: 20231201140000 - -=== Overview -This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements. -It can not cover any special case. -If you need more information about any of these elements, please refer to the detailed description. - -=== Basic Syntax -|[[Text formatting|00001007040100]]|''__italic text__'' → __italic text__, ''**bold text**'' → **bold text**, ''""quoted text""'' → ""quoted text"", ''##marked text##'' → ##marked text## -|[[Text editing|00001007040100]]|''>>inserted text>>'' → >>inserted text>>, ''~~deleted text~~'' → ~~deleted text~~ -|[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' → ''entered text'', ''``source code``'' → ``source code``, ''==text output=='' → ==text output== -|[[Superscript, subscript|00001007040100]]|''m^^2^^'' → m^^2^^, ''H,,2,,O'' → H,,2,,O -|[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' → [[Link text|00001007990000]] -|[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' → [[Zettelstore|https://zettelstore.de]] -|[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' → {{Image text|00000000040001}} -|[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' → {{00001007990000}} -|[[Footnote|00001007040330]]|''text[^footnote]'' → text[^footnote] -|[[Special characters / entities|00001007040000]]|''→'' → →, ''ℕ'' → ℕ, ''⌛'' → ⌛ - -=== Structuring -* [[Heading|00001007030300]]: ''=== Heading'', ''==== Sub-Heading'' -* [[Horizontal rule / thematic break|00001007030400]]: ''---'' -* [[Paragraphs|00001007030000]] are separated by an empty line ---- - -=== Lists -[[Unnumbered list|00001007030200]]: -``` -* First list item -* Second list item -* Third list item -``` -[[Numbered list|00001007030200]]: -``` -# Item number 1 -# Item number 2 -# Item number 3 -``` -[[Description List|00001007030100]]: -``` -; Term -: Definition -; Other Term -: Definition for other term -``` -=== [[Tables|00001007031000]] -``` -|=Header|Because|Equal Sign -|Cell 1.1|Cell 1.2| Cell 1.3 -|Cell 2.1|Cell 2.2 -``` Index: docs/manual/00001008000000.zettel ================================================================== --- docs/manual/00001008000000.zettel +++ docs/manual/00001008000000.zettel @@ -2,11 +2,11 @@ title: Other Markup Languages role: manual tags: #manual #zettelstore syntax: zmk created: 20210126175300 -modified: 20240413160242 +modified: 20221018115054 [[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content. Zettelstore is quite agnostic with respect to markup languages. Of course, Zettelmarkup plays an important role. However, with the exception of zettel titles, you can use any (markup) language that is supported: @@ -15,19 +15,17 @@ * HTML template data * Image formats: GIF, PNG, JPEG, SVG * Markdown * Plain text, not further interpreted -The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which markup language / data format should be used. +The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used. If it is not given, it defaults to ''plain''. The following syntax values are supported: ; [!css|''css''] : A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML. -; [!draw|''draw''] -: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters. -; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png'']; [!webp|''webp''] +; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png''] : The formats for pixel graphics. Typically the data is stored in a separate file and the syntax is given in the meta-file, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta''] ; [!html|''html''] : Hypertext Markup Language, will not be parsed further. Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]). @@ -39,23 +37,31 @@ See [[security aspects of Markdown|00001008010000#security-aspects]] for some details. ; [!markdown|''markdown''], [!md|''md''] : For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]]. Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used. See [[Use Markdown within Zettelstore|00001008010000]]. +; [!mustache|''mustache''] +: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]]. ; [!none|''none''] : Only the metadata of a zettel is ""parsed"". Useful for displaying the full metadata. The [[runtime configuration zettel|00000000000100]] uses this syntax. The zettel content is ignored. +; [!pikchr]''pikchr'' +: A [[PIC|https://en.wikipedia.org/wiki/Pic_language]]-like [[markup language for diagrams|https://pikchr.org/]]. ; [!svg|''svg''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. -; [!sxn|''sxn''] -: S-Expressions, as implemented by [[Sx|https://t73f.de/r/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]]. The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]]. If you specify something else, your content will be interpreted as plain text. + +=== Language for other elements of a zettel +[[Zettelmarkup|00001007000000]] allows to specify [[evaluation blocks|00001007031300]], which also receive a syntax value. +An evaluation blocks is typically interpreted by external software, for example [[Zettel Presenter|00001006055000#external-applications]]. +However, some values are interpreted by Zettelstore during evaluation of a zettel: +; [!draw|''draw''] +: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters. Index: docs/manual/00001008050000.zettel ================================================================== --- docs/manual/00001008050000.zettel +++ docs/manual/00001008050000.zettel @@ -1,12 +1,11 @@ id: 00001008050000 -title: The "draw" language +title: The ""draw"" language role: manual tags: #graphic #manual #zettelstore syntax: zmk -created: 20220131142036 -modified: 20230403123738 +modified: 20220311120439 Sometimes, ""a picture is worth a thousand words"". To create some graphical representations, Zettelmarkup provides a simple mechanism. Characters like ""''|''"" or ""''-''"" already provide some visual feedback. For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible: Index: docs/manual/00001010040200.zettel ================================================================== --- docs/manual/00001010040200.zettel +++ docs/manual/00001010040200.zettel @@ -1,12 +1,11 @@ id: 00001010040200 title: Creating an user zettel role: manual tags: #authentication #configuration #manual #security #zettelstore syntax: zmk -created: 20210126175322 -modified: 20221205160251 +modified: 20211127174207 All data to be used for authenticating a user is store in a special zettel called ""user zettel"". A user zettel must have set the following two metadata fields: ; ''user-id'' (""user identification"") @@ -19,12 +18,10 @@ The following metadata elements are optional: ; ''user-role'' : Associate the user with some basic privileges, e.g. a [[user role|00001010070300]] -A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]]. - A user zettel can only be created by the owner of the Zettelstore. The owner should execute the following steps to create a new user zettel: # Create a new zettel. Index: docs/manual/00001012000000.zettel ================================================================== --- docs/manual/00001012000000.zettel +++ docs/manual/00001012000000.zettel @@ -2,18 +2,18 @@ title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20231128183617 +modified: 20220923105117 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. === Background -The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software. +The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software. There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use. === Authentication If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller. @@ -20,25 +20,26 @@ * [[Authenticate an user|00001012050200]] to obtain an access token * [[Renew an access token|00001012050400]] without costly re-authentication * [[Provide an access token|00001012050600]] when doing an API call === Zettel lists -* [[List all zettel|00001012051200]] * [[Query the list of all zettel|00001012051400]] -* [[Determine a tag zettel|00001012051600]] -* [[Determine a role zettel|00001012051800]] +* [[List plain text titles of all zettel|00001012051200]] === Working with zettel * [[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 context of an existing zettel|00001012053800]] +* [[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/00001012050200.zettel ================================================================== --- docs/manual/00001012050200.zettel +++ docs/manual/00001012050200.zettel @@ -1,52 +1,51 @@ id: 00001012050200 title: API: Authenticate a client role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230412150544 +modified: 20220107215844 Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]]. This token has to be used for other API calls. It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes). The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a -("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA2MiwiaWF0IjoxNjgxMzA0MDAyLCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kdF8PdiL50gIPkRD3ovgR6nUXR0-80EKAXcY2zVYgYvryF09iXnNR3zrvYnGzdrArMcnvAYqVvuXtqhQj2jG9g" 600) +{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL: ```sh # curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a -("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 600) +{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data: ```sh # curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a -("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600) +{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600} ``` -In all cases, you will receive a list with three elements that will contain all [[relevant data|00001012921000]] to be used for further API calls. +In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls. **Important:** obtaining a token is a time-intensive process. Zettelstore will delay every request to obtain a token for a certain amount of time. Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more. However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a -("Bearer" "freeaccess" 316224000) +{"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the user identification/password. === HTTP Status codes -In all cases of successful authentication, a list is returned, which contains the token as the second element. +In all cases of successful authentication, a JSON object is returned, which contains the token under the key ''"token"''. A successful authentication is signaled with the HTTP status code 200, as usual. Other status codes possibly send by the Zettelstore: ; ''400'' : Unable to process the request. Index: docs/manual/00001012050400.zettel ================================================================== --- docs/manual/00001012050400.zettel +++ docs/manual/00001012050400.zettel @@ -1,38 +1,37 @@ id: 00001012050400 title: API: Renew an access token role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230412160219 +modified: 20220107215751 An access token is only valid for a certain duration. Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data. Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/a'' and include the current access token in the ''Authorization'' header: ```sh # curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a -("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 456) +{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456} ``` You may receive a new access token, or the current one if it was obtained not a long time ago. However, the lifetime of the returned [[access token|00001012921000]] is accurate. If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay: ```sh # curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a -("Bearer" "freeaccess" 316224000) +{"token":"freeaccess","token_type":"Bearer","expires_in":316224000} ``` In this case, it is even possible to omit the access token. === HTTP Status codes ; ''200'' -: Renew process was successful, the body contains a [[list|00001012921000]] with the relevant data. +: Renew process was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : The renew process was not successful. There are several reasons for this. Maybe access bearer token was not valid. Probably you should [[authenticate|00001012050200]] again with user identification and password. Index: docs/manual/00001012051200.zettel ================================================================== --- docs/manual/00001012051200.zettel +++ docs/manual/00001012051200.zettel @@ -1,81 +1,63 @@ id: 00001012051200 -title: API: List all zettel +title: API: List plain text titles of all or some zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230807170810 +modified: 20220923105412 -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. +To list the plain text titles of 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]. -Without further specifications, a plain text document is returned, with one line per zettel. +A plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: ```sh # curl http://127.0.0.1:23123/z -... -00001012051200 API: List all zettel +00001012051200 API: Renew an access token 00001012050600 API: Provide an access token 00001012050400 API: Renew an access token 00001012050200 API: Authenticate a client +00001012000000 API +``` + +The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata, similar to endpoint ''/q'' to [[query zettel|00001012051400]]. + +You are allowed to specify this query parameter more than once. + +This parameter loosely resembles the search form of the [[web user interface|00001014000000]]. + +For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: +```sh +# curl 'http://127.0.0.1:23123/z?q=title%3AAPI' +00001012921000 API: JSON structure of an access token +00001012920500 Formats available by the API +00001012920000 Endpoints used by the API +... +``` + +An implicit precondition is that the zettel must contain the given metadata key. +For a metadata key like [[''title''|00001006020000#title]], which has 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. + +Alternatively, you also can use the [[endpoint|00001012920000]] ''/z'' for a simpler result format. +The first example translates to: +```sh +# curl 'http://127.0.0.1:23123/z?q=title%3AAPI' +00001012921000 API: JSON structure of an access token +00001012920500 Formats available by the API +00001012920000 Endpoints used by the API ... ``` -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. - -=== Note -This request (and similar others) will always return a list of metadata, provided the request was syntactically correct. -There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token. -In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty. - -With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token). +=== Other output formats +If you want to get the list of metadata of all or some zettel in JSON format, use endpoint ''/q'' to [[query the list of zettel|00001012051400]]. === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate data value. +: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]]. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid. Index: docs/manual/00001012051400.zettel ================================================================== --- docs/manual/00001012051400.zettel +++ docs/manual/00001012051400.zettel @@ -2,139 +2,149 @@ title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 -modified: 20240219161831 -precursor: 00001012051200 +modified: 20220923105405 -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. +The [[endpoint|00001012920000]] ''/q'' allows to query the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. 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. An empty list of action, or no valid action, returns the list of all selected zettel metadata. Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''. -The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata. - -It is allowed to specify this query parameter more than once. - -This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]]. - -For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be: -```sh -# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1' -00001012921000 API: Structure of an access token -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. - -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. - -=== 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. - - -As an example for a query action, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|role''. - -```sh -# curl 'http://127.0.0.1:23123/z?q=|role' -configuration 00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001 -manual 00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000 -zettel 00010000000000 00000000090001 -``` - -The result is a text file. -The first word, separated by a horizontal tab (U+0009) contains the role name. -The rest of the line consists of zettel identifier, where the corresponding zettel have this role. -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. - -=== Actions +For example, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|role''. +If successful, the output is a JSON object: + +```sh +# curl http://127.0.0.1:23123/q?q=|role +{"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. + +Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|tags''. +If successful, the output is a JSON object: + +```sh +# curl http://127.0.0.1:23123/q?q=|tags +{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}} +``` + +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 ''/q?q=|MIN100+tags''. +You see from this that actions are separated by space characters. 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. __n__ must be a positive integer, ''MIN'' must be given in upper-case letters. ; ''MAXn'' (parameter) : Emit only those values with at most __n__ aggregated values. __n__ must be a positive integer, ''MAX'' must be given in upper-case letters. -; ''REDIRECT'' (aggregate) -: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302. - The zettel identifier is in the body. -; ''REINDEX'' (aggregate) -: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call. - It is not really an aggregate, since it is used only for its side effect. - It is allowed to specify another aggregate. -; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates) +; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates) : Emit an aggregate of the given metadata key. The key can be given in any letter case. -First, ''REINDEX'' actions are executed, then ''REDIRECT''. -If no ''REDIRECT'' was found the first other aggregate action will be executed. +Only the first aggregate action will be executed. + +If no valid aggregate action is given, the metadata of all selected zettel are returned.[^For this reason, a HTTP GET to the endpoint ''/j'' is an alias for the endpoint ''/q''.] + +```sh +# curl http://127.0.0.1:23123/q +{"query":"","list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62}]} +``` + +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. + +If you reformat the JSON output from the ''GET /q'' call, you'll see its structure better: + +```json +{ + "query": "", + "human": "", + "list": [ + { + "id": "00001012051200", + "meta": { + "title": "API: List for all zettel some data", + "tags": "#api #manual #zettelstore", + "syntax": "zmk", + "role": "manual" + }, + "rights":62 + }, + { + "id": "00001012050600", + "meta": { + "title": "API: Provide an access token", + "tags": "#api #manual #zettelstore", + "syntax": "zmk", + "role": "manual" + }, + "rights":62 + }, + { + "id": "00001012050400", + "meta": { + "title": "API: Renew an access token", + "tags": "#api #manual #zettelstore", + "syntax": "zmk", + "role": "manual" + }, + "rights":62 + }, + { + "id": "00001012050200", + "meta": { + "title": "API: Authenticate a client", + "tags": "#api #manual #zettelstore", + "syntax": "zmk", + "role": "manual" + }, + "rights":62 + }, + { + "id": "00001012000000", + "meta": { + "title": "API", + "tags": "#api #manual #zettelstore", + "syntax": "zmk", + "role": "manual" + }, + "rights":62 + } + ] +} +``` +In this special case, the metadata of each zettel just contains the four default keys ''title'', ''tags'', ''syntax'', and ''role''. + +=== Note +This request (and similar others) will always return a list of metadata, provided the request was syntactically correct. +There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token. +In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty. -To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored. -In this case the list of selected zettel is returned. +With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token). === HTTP Status codes ; ''200'' : Query was successful. ; ''204'' : Query was successful, but results in no content. Most likely, you specified no appropriate aggregator. -; ''302'' -: Query was successful, redirect to first zettel in list. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the access bearer token was not valid, or you forgot to specify a valid query. DELETED docs/manual/00001012051600.zettel Index: docs/manual/00001012051600.zettel ================================================================== --- docs/manual/00001012051600.zettel +++ docs/manual/00001012051600.zettel @@ -1,82 +0,0 @@ -id: 00001012051600 -title: API: Determine a tag zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk -created: 20230928183339 -modified: 20230929114937 - -The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""tag zettel"", i.e. a zettel that documents a given tag. - -The query parameter ""''tag''"" allows you to specify a value that is interpreted as the name of a tag. -Zettelstore tries to determine the corresponding tag zettel. - -A tag zettel is a zettel with the [[''role''|00001006020100]] value ""tag"" and a title that names the tag. -If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used. - -For example, if you want to determine the tag zettel for the tag ""#api"", your request will be: -```sh -# curl -i 'http://127.0.0.1:23123/z?tag=%23api' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/00001019990010 -Content-Length: 14 - -00001019990010 -``` - -Alternatively, you can omit the ''#'' character at the beginning of the tag: -```sh -# curl -i 'http://127.0.0.1:23123/z?tag=api' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/00001019990010 -Content-Length: 14 - -00001019990010 -``` - -If there is a corresponding tag zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the tag zettel. -Its zettel identifier will be returned in the HTTP response body. - -If you specified some more query parameter, these will be part of the URL in the response header ''Location'': - -```sh -# curl -i 'http://127.0.0.1:23123/z?tag=%23api&part=zettel' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/00001019990010?part=zettel -Content-Length: 14 - -00001019990010 -``` - -Otherwise, if no tag zettel was found, the response will use the HTTP status code 404 (""Not found""). - -```sh -# curl -i 'http://127.0.0.1:23123/z?tag=notag' -HTTP/1.1 404 Not Found -Content-Type: text/plain; charset=utf-8 -Content-Length: 29 - -Tag zettel not found: #notag -``` - -To fulfill this service, Zettelstore will evaluate internally the query ''role:tag title=TAG'', there ''TAG'' is the actual tag. - -Of course, if you are interested in the URL of the tag zettel, you can make use of the HTTP ''HEAD'' method: - -```sh -# curl -I 'http://127.0.0.1:23123/z?tag=%23api' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/00001019990010 -Content-Length: 14 -``` - -=== HTTP Status codes -; ''302'' -: Tag zettel was found. - The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier. -; ''404'' -: No zettel for the given tag was found. DELETED docs/manual/00001012051800.zettel Index: docs/manual/00001012051800.zettel ================================================================== --- docs/manual/00001012051800.zettel +++ docs/manual/00001012051800.zettel @@ -1,71 +0,0 @@ -id: 00001012051800 -title: API: Determine a role zettel -role: manual -tags: #api #manual #zettelstore -syntax: zmk -created: 20231128183917 -modified: 20231128184701 - -The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""role zettel"", i.e. a zettel that documents a given role. - -The query parameter ""''role''"" allows you to specify a value that is interpreted as the name of a role. -Zettelstore tries to determine the corresponding role zettel. - -A role zettel is a zettel with the [[''role''|00001006020100]] value ""role"" and a title that names the role. -If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used. - -For example, if you want to determine the role zettel for the role ""manual"", your request will be: -```sh -# curl -i 'http://127.0.0.1:23123/z?role=manual' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/20231128184200 -Content-Length: 14 - -20231128184200 -``` - -If there is a corresponding role zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the role zettel. -Its zettel identifier will be returned in the HTTP response body. - -If you specified some more query parameter, these will be part of the URL in the response header ''Location'': - -```sh -# curl -i 'http://127.0.0.1:23123/z?role=manual&part=zettel' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/20231128184200?part=zettel -Content-Length: 14 - -20231128184200 -``` - -Otherwise, if no role zettel was found, the response will use the HTTP status code 404 (""Not found""). - -```sh -# curl -i 'http://127.0.0.1:23123/z?role=norole' -HTTP/1.1 404 Not Found -Content-Type: text/plain; charset=utf-8 -Content-Length: 30 - -Role zettel not found: norole -``` - -To fulfill this service, Zettelstore will evaluate internally the query ''role:role title=ROLE'', there ''ROLE'' is the actual role. - -Of course, if you are only interested in the URL of the role zettel, you can make use of the HTTP ''HEAD'' method: - -```sh -# curl -I 'http://127.0.0.1:23123/z?role=manual' -HTTP/1.1 302 Found -Content-Type: text/plain; charset=utf-8 -Location: /z/20231128184200 -Content-Length: 14 -``` - -=== HTTP Status codes -; ''302'' -: Role zettel was found. - The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier. -; ''404'' -: No zettel for the given role was found. Index: docs/manual/00001012053200.zettel ================================================================== --- docs/manual/00001012053200.zettel +++ docs/manual/00001012053200.zettel @@ -1,40 +1,65 @@ id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210713150005 -modified: 20230807170416 - -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. - - -The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. -This is the same format as used by storing zettel within a [[directory box|00001006010000]]. - +modified: 20220628111320 + +A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]]. +Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/j'', but you must send the data of the new zettel via a HTTP POST request. + +The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created. +The following keys of the JSON object are used: +; ''"meta"'' +: References an embedded JSON object with only string values. + 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. + +Other keys will be ignored. +Even these three keys are just optional. +The body of the HTTP POST request must not be empty and it must contain a JSON object. + +Therefore, a body containing just ''{}'' is perfectly valid. +The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]: + +``` +# curl -X POST --data '{}' http://127.0.0.1:23123/j +{"id":"20210713161000"} +``` +If creating the zettel was successful, the HTTP response will contain a JSON object with one key: +; ''"id"'' +: Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage. + +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. + +As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing: +``` +# curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/j +{"id":"20210713163100"} +``` + +[!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z'' to create a new zettel. +In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. +This is the same format as used by storing zettel within a [[directory box|00001006010000]]. ``` # curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z 20210903211500 ``` -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. - === HTTP Status codes ; ''201'' -: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (data value or plain text). +: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text). ; ''400'' : Request was not valid. There are several reasons for this. - Most likely, the symbolic expression was not formed according to above rules. + Most likely, the JSON was not formed according to above rules. ; ''403'' : You are not allowed to create a new zettel. Index: docs/manual/00001012053300.zettel ================================================================== --- docs/manual/00001012053300.zettel +++ docs/manual/00001012053300.zettel @@ -2,83 +2,93 @@ title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 -modified: 20230807170259 - -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]. - -````sh -# curl 'http://127.0.0.1:23123/z/00001012053300' -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]. - +modified: 20220908162927 + +The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{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 ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: ```sh -... -```` +# curl http://127.0.0.1:23123/j/00001012053300 +{"id":"00001012053300","meta":{"title":"API: Retrieve data for an existing zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern ","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000}}.\n\nFor example, ... +``` + +Pretty-printed, this results in: +``` +{ + "id": "00001012053300", + "meta": { + "back": "00001012000000 00001012053200 00001012054400", + "backward": "00001012000000 00001012053200 00001012054400 00001012920000", + "box-number": "1", + "forward": "00001010040100 00001012050200 00001012920000 00001012920800", + "modified": "20210726190012", + "published": "20210726190012", + "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 ''/j/{ID}'', where ''{ID}'' (...) + "rights": 62 +} +``` + +The following keys of the JSON object are used: +; ''"id"'' +: The zettel identifier of the zettel you requested. +; ''"meta"'' +: References an embedded JSON object with only string values. + 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. +=== Plain zettel +[!plain]Additionally, you can retrieve the plain zettel, without using JSON. +Just change the [[endpoint|00001012920000]] to ''/z/{ID}'' Optionally, you may provide which parts of the zettel you are requesting. In this case, add an additional query parameter ''part=PART''. Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value). +````sh +# curl 'http://127.0.0.1:23123/z/00001012053300' +The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{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 ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: +```sh +... +```` ````sh # curl 'http://127.0.0.1:23123/z/00001012053300?part=zettel' title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk -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]]. +The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{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 ... ```` -=== 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. - === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate data value. +: Retrieval was successful, the body contains an appropriate JSON object / plain zettel data. ; ''204'' : Request was valid, but there is no data to be returned. Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content. ; ''400'' : Request was not valid. Index: docs/manual/00001012053400.zettel ================================================================== --- docs/manual/00001012053400.zettel +++ docs/manual/00001012053400.zettel @@ -2,61 +2,65 @@ title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 -modified: 20230807170155 +modified: 20220917175233 + +The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{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 ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: +```sh +# curl http://127.0.0.1:23123/m/00001012053400 +{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012920800","modified":"20211004111240","published":"20211004111240","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62} +``` -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]. +Pretty-printed, this results in: +``` +{ + "meta": { + "back": "00001012000000 00001012053300", + "backward": "00001012000000 00001012053300 00001012920000", + "box-number": "1", + "forward": "00001010040100 00001012050200 00001012920000 00001012920800", + "modified": "20211004111240", + "published": "20211004111240", + "role": "manual", + "syntax": "zmk", + "tags": "#api #manual #zettelstore", + "title": "API: Retrieve metadata of an existing zettel" + }, + "rights": 62 +} +``` +The following keys of the JSON object are used: +; ''"meta"'' +: References an embedded JSON object with only string values. + 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). +; ''"rights"'' +: An integer number that describes the [[access rights|00001012921200]] for the zettel. -To retrieve the plain metadata of a zettel, use the query parameter ''part=meta'' +[!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON. +Just change the [[endpoint|00001012920000]] to ''/z/{ID}?part=meta'' ````sh # curl 'http://127.0.0.1:23123/z/00001012053400?part=meta' title: API: Retrieve metadata of an existing zettel 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. - === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate data value. +: Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits. ; ''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/00001012053500.zettel ================================================================== --- docs/manual/00001012053500.zettel +++ docs/manual/00001012053500.zettel @@ -2,59 +2,70 @@ title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 -modified: 20230807170112 - -The [[endpoint|00001012920000]] to work with evaluated 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 evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''. -If successful, the output is a symbolic expression value: -```sh -# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz' -((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ... +modified: 20220908162843 + +The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/v/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. + +For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: +```sh +# curl http://127.0.0.1:23123/v/00001012053500 +{"meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"evaluated"},{"t":"Space"},{"t":"Text","s":"metadata"},{"t":"Space"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"content"},{"t":"Space"},{"t":"Text","s":"of"},{"t":"Space"},{"t":"Text","s":"an"},{"t":"Space"},{"t":"Text","s":"existing"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"}, ... ``` -To select another encoding, you must provide the query parameter ''enc=ENCODING''. -Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]]. -In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel. +To select another encoding, you can provide a query parameter ''enc=ENCODING''. +The default value for [[''ENCODING''|00001012920500]] is ""[[zjson|00001012920503]]"". +Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some more. ```sh -# curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel' - +# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html' + + API: Retrieve evaluated metadata and content of an existing zettel in various encodings - - + - - + + - - - - - - - - + + + -

API: Retrieve evaluated metadata and content of an existing zettel in various encodings

-

The endpoint to work with evaluated metadata and content of a specific zettel is /z/{ID}, +

The endpoint to work with evaluated metadata and content of a specific zettel is /v/{ID}, where {ID} is a placeholder for the zettel identifier.

... ``` + +You also can use the query parameter ''part=PART'' to specify which [[parts|00001012920800]] of a zettel must be encoded. +In this case, its default value is ''content''. +```sh +# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html&part=meta' + + + + + + + + + + + +``` === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate data value. +: Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''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/00001012053600.zettel ================================================================== --- docs/manual/00001012053600.zettel +++ docs/manual/00001012053600.zettel @@ -2,34 +2,34 @@ title: API: Retrieve parsed metadata and content of an existing zettel in various encodings role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 -modified: 20230807170019 +modified: 20220908163514 -The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. +The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/p/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__. By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated. -For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter). -For example: +For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. +If successful, the output is a JSON object: ```sh -# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly' -((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ... +# curl http://127.0.0.1:23123/p/00001012053600 +[{"t":"Para","i":[{"t":"Text","s":"The"},{"t":"Space"},{"t":"Link","q":"zettel","s":"00001012920000","i":[{"t":"Text","s":"endpoint"}]},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"work"},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"}, ... ``` Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in. The same default values applies to this endpoint. === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate data value. +: Retrieval was successful, the body contains an appropriate JSON object. ; ''400'' : Request was not valid. There are several reasons for this. Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values. ; ''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. ADDED docs/manual/00001012053800.zettel Index: docs/manual/00001012053800.zettel ================================================================== --- docs/manual/00001012053800.zettel +++ docs/manual/00001012053800.zettel @@ -0,0 +1,79 @@ +id: 00001012053800 +title: API: Retrieve context of an existing zettel +role: manual +tags: #api #manual #zettelstore +syntax: zmk +modified: 20220202112607 + +The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel. +Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]]. + +The context is defined by a __direction__, a __depth__, and a __limit__: +* Direction: connections are directed. + For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''forward'' list all zettel to which the current zettel links to. + When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"". + All other values, including a missing value, is interpreted as ""both"". +* Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel. + You should limit the depth by using the parameter ''depth''. + Its default value is ""5"". + A value of ""0"" does disable any depth check. +* Limit: to set an upper bound for the returned context, you should use the parameter ''limit''. + Its default value is ""200"". + A value of ""0"" disables does not limit the number of elements returned. + +The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]]. +This zettel is connected to all other zettel. +If it is included, the context would become too big and therefore unusable. + +To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t]. + +```` +# curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2' +{"id": "00001012053800","meta": {...},"rights":62,"list":[{"id": "00001012921000","meta": {...},"rights":62},{"id": "00001012920800","meta": {...},"rights":62},{"id": "00010000000000","meta": {...},"rights":62}]} +```` +Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.] +````json +{ + "id": "00001012053800", + "meta": {...}, + "rights": 62, + "list": [ + { + "id": "00001012921000", + "meta": {...}, + "rights":62 + }, + { + "id": "00001012920800", + "meta": {...}, + "rights":62 + }, + { + "id": "00010000000000", + "meta": {...}, + "rights":62 + } + ] +} +```` +=== Keys +The following top-level JSON keys are returned: +; ''id'' +: The [[zettel identifier|00001006050000]] for which the context was 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 contains the zettel of the context. + +=== 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. ADDED docs/manual/00001012053900.zettel Index: docs/manual/00001012053900.zettel ================================================================== --- docs/manual/00001012053900.zettel +++ docs/manual/00001012053900.zettel @@ -0,0 +1,80 @@ +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. ADDED docs/manual/00001012054000.zettel Index: docs/manual/00001012054000.zettel ================================================================== --- docs/manual/00001012054000.zettel +++ docs/manual/00001012054000.zettel @@ -0,0 +1,84 @@ +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 @@ -1,33 +1,33 @@ id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210713150005 -modified: 20231116110417 +modified: 20211124180943 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. -The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. -You must send a HTTP PUT request to that endpoint. - -The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. -This is the same format as used by storing zettel within a [[directory box|00001006010000]]. - -``` -# curl -X PUT --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. +The [[endpoint|00001012920000]] to update a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. +You must send a HTTP PUT request to that endpoint: + +``` +# curl -X PUT --data '{}' http://127.0.0.1:23123/j/00001012054200 +``` +This will put some empty content and metadata to the zettel you are currently reading. +As usual, some metadata will be calculated if it is empty. + +The body of the HTTP response is empty, if the request was successful. + +[!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z/{ID}'' to update a zettel. +In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line. +This is the same format as used by storing zettel within a [[directory box|00001006010000]]. +``` +# curl -X POST --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200 +``` === HTTP Status codes ; ''204'' : Update was successful, there is no body in the response. ; ''400'' Index: docs/manual/00001012054400.zettel ================================================================== --- docs/manual/00001012054400.zettel +++ docs/manual/00001012054400.zettel @@ -1,30 +1,32 @@ id: 00001012054400 title: API: Rename a zettel role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210713150005 -modified: 20221219154659 +modified: 20211124181324 Renaming a zettel is effectively just specifying a new identifier for the zettel. Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful. If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations. As a consequence, you cannot rename a zettel when its identifier is used in a read-only box. This applies to all [[predefined zettel|00001005090000]], for example. -The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. +The [[endpoint|00001012920000]] to rename a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''. ``` -# curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000 +# curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/j/00001000000000 ``` Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused [[zettel identifier|00001006050000]]. If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''. All other characters, besides those 14 digits, are effectively ignored. However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel. + +[!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. +Both endpoints behave identical. === HTTP Status codes ; ''204'' : Rename was successful, there is no body in the response. ; ''400'' Index: docs/manual/00001012054600.zettel ================================================================== --- docs/manual/00001012054600.zettel +++ docs/manual/00001012054600.zettel @@ -1,22 +1,24 @@ id: 00001012054600 title: API: Delete a zettel role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20210713150005 -modified: 20221219154608 +modified: 20211124181041 Deleting a zettel within the Zettelstore is executed on the first [[box|00001004011200]] that contains that zettel. Zettel with the same identifier, but in subsequent boxes remain. If the first box containing the zettel is read-only, deleting that zettel will fail, as well for a Zettelstore in [[read-only mode|00001004010000#read-only-mode]] or if [[authentication is enabled|00001010040100]] and the user has no [[access right|00001010070600]] to do so. -The [[endpoint|00001012920000]] to delete a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. +The [[endpoint|00001012920000]] to delete a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]]. You must send a HTTP DELETE request to this endpoint: ``` -# curl -X DELETE http://127.0.0.1:23123/z/00001000000000 +# curl -X DELETE http://127.0.0.1:23123/j/00001000000000 ``` + +[!plain]Alternatively, you can also use the [[endpoint|00001012920000]] ''/z/{ID}''. +Both endpoints behave identical. === HTTP Status codes ; ''204'' : Delete was successful, there is no body in the response. ; ''403'' Index: docs/manual/00001012070500.zettel ================================================================== --- docs/manual/00001012070500.zettel +++ docs/manual/00001012070500.zettel @@ -1,29 +1,27 @@ id: 00001012070500 -title: API: Retrieve administrative data +title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk -created: 20220304164242 -modified: 20230928190516 +modified: 20220805174216 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' -(0 13 0 "dev" "f781dc384b-dirty") +{"major":0,"minor":4,"patch":0,"info":"dev","hash":"cb121cc980-dirty"} ```` -* Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. - - 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. - -If any of the three digits has the value -1, its semantic value is unknown. -Similar, the two string might be empty. +Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]]. + +The names ""major"", ""minor"", and ""patch"" are described in this standard. + +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. === HTTP Status codes ; ''200'' -: Retrieval was successful, the body contains an appropriate object. +: Retrieval was successful, the body contains an appropriate JSON 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: 20230731162343 +modified: 20220923101703 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,16 +17,26 @@ 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]] | -| ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute +| ''j'' | GET: [[query zettel list|00001012051400]] (alias of ''/q'') | GET: [[retrieve zettel AS JSON|00001012053300]] | **J**SON +| | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]] +| | | DELETE: [[delete the zettel|00001012054600]] +| | | MOVE: [[rename the zettel|00001012054400]] +| ''m'' | | GET: [[retrieve metadata|00001012053400]] | **M**etadata +| ''o'' | | GET: [[list zettel order|00001012054000]] | **O**rder +| ''p'' | | GET: [[retrieve parsed zettel|00001012053600]]| **P**arsed +| ''q'' | GET: [[query zettel list|00001012051400]] | | **Q**uery +| ''u'' | | GET [[unlinked references|00001012053900]] | **U**nlinked +| ''v'' | | GET: [[retrieve evaluated zettel|00001012053500]] | E**v**aluated +| ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t | | POST: [[execute command|00001012080100]] -| ''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]] +| ''z'' | GET: [[list zettel|00001012051200#plain]] | GET: [[retrieve zettel|00001012053300#plain]] | **Z**ettel +| | POST: [[create new zettel|00001012053200#plain]] | PUT: [[update a zettel|00001012054200#plain]] +| | | DELETE: [[delete zettel|00001012054600#plain]] +| | | MOVE: [[rename zettel|00001012054400#plain]] 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/00001012920500.zettel ================================================================== --- docs/manual/00001012920500.zettel +++ docs/manual/00001012920500.zettel @@ -1,17 +1,14 @@ id: 00001012920500 -title: Encodings available via the API +title: Encodings available via the [[API|00001012000000]] role: manual tags: #api #manual #reference #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230403123653 +modified: 20220423131535 A zettel representation can be encoded in various formats for further processing. -These will be retrieved via the [[API|00001012000000]]. * [[html|00001012920510]] -* [[md|00001012920513]] -* [[shtml|00001012920525]] -* [[sz|00001012920516]] +* [[sexpr|00001012920516]] * [[text|00001012920519]] +* [[zjson|00001012920503]] (default) * [[zmk|00001012920522]] ADDED docs/manual/00001012920503.zettel Index: docs/manual/00001012920503.zettel ================================================================== --- docs/manual/00001012920503.zettel +++ docs/manual/00001012920503.zettel @@ -0,0 +1,36 @@ +id: 00001012920503 +title: ZJSON Encoding +role: manual +tags: #api #manual #reference #zettelstore +syntax: zmk +created: 20210126175322 +modified: 20220908163450 + +A zettel representation that allows to process the syntactic structure of a zettel. +It is a JSON-based encoding format, but different to the structures returned by [[endpoint|00001012920000]] ''/j/{ID}''. + +For an example, take a look at the ZJSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: + +* [[//v/00001012920503?enc=zjson&part=zettel]], +* [[//v/00001012920503?enc=zjson&part=meta]], +* [[//v/00001012920503?enc=zjson&part=content]]. + +If transferred via HTTP, the content type will be ''application/json''. + +A full zettel encoding results in a JSON object with two keys: ''"meta"'' and ''"content"''. +Both values are the same as if you have requested just the appropriate [[part|00001012920800]]. + +=== Encoding of metadata +Metadata encoding results in a JSON object, where each metadata key is mapped to the same JSON object name. +The associated value is itself a JSON object with two names. +The first name ``""`` references the [[metadata key type|00001006030000]]. +Depending on the key type, the other name denotes the value of the metadata element. +The meaning of these names is [[well defined|00001012920582]], as well as the [[mapping of key types to used object names|00001012920584]]. + +=== Encoding of zettel content +The content encoding results in a JSON array of objects, where each objects represents a Zettelmarkup element. + +Every [!zettelmarkup|Zettelmarkup] element is encoded as a JSON object. +These objects always contain the empty name ''""'' with a string value describing the type of Zettelmarkup element. +Depending on the type, other one letter names denotes the details of the element. +The meaning of these names is [[well defined|00001012920588]]. DELETED docs/manual/00001012920513.zettel Index: docs/manual/00001012920513.zettel ================================================================== --- docs/manual/00001012920513.zettel +++ docs/manual/00001012920513.zettel @@ -1,30 +0,0 @@ -id: 00001012920513 -title: Markdown Encoding -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20221107183011 -modified: 20221107185130 - -A zettel representation that tries to recreate a [[Markdown|00001008010500]] representation of the zettel. -Useful if you want to convert [[other markup languages|00001008000000]] to Markdown (e.g. [[Zettelmarkup|00001007000000]]). - -If transferred via HTTP, the content type will be ''text/markdown''. - -Please note that many elements of Zettelmarkup cannot be encoded in Markdown / CommonMark. -Examples are: -* [[Description lists|00001007030100]] -* [[Verse blocks|00001007030700]] -* [[Region blocks|00001007030800]] -* [[Comment blocks|00001007030900]] (and inline comments) -* [[Evaluation blocks|00001007031300]] -* [[Math-mode blocks|00001007031400]] -* [[Tables|00001007031000]] -* Most [[text formatting|00001007040100]] elements (except emphasis and quotation) -* Most [[literal-like formatting|00001007040200]] (except literal text / code spans) -Some elements are restricted, e.g. [[quotation lists|00001007030200]] are only supported as a top-level element. - -Restricted and unsupported elements are not encoded. -They will not appear on the encoded output. - -Maybe in the future, ignored elements may be shown as an HTML-like comment. Index: docs/manual/00001012920516.zettel ================================================================== --- docs/manual/00001012920516.zettel +++ docs/manual/00001012920516.zettel @@ -1,20 +1,45 @@ id: 00001012920516 -title: Sz Encoding +title: Sexpr Encoding role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20220422181104 -modified: 20230403161458 - -A zettel representation that is a [[s-expression|00001012930000]] (also known as symbolic expression). - -It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content. -For example, take a look at the Sz encoding of this page, which is available via the ""Info"" sub-page of this zettel: - -* [[//z/00001012920516?enc=sz&part=zettel]], -* [[//z/00001012920516?enc=sz&part=meta]], -* [[//z/00001012920516?enc=sz&part=content]]. - -Some zettel describe the [[Sz encoding|00001012931000]] in a more detailed way. +modified: 20220908163427 + +A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression). + +It is an alternative to the [[ZJSON encoding|00001012920503]]. +Both encodings are (relatively) easy to parse and contain all relevant information of a zettel, metadata and content. + +For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: + +* [[//v/00001012920516?enc=sexpr&part=zettel]], +* [[//v/00001012920516?enc=sexpr&part=meta]], +* [[//v/00001012920516?enc=sexpr&part=content]]. If transferred via HTTP, the content type will be ''text/plain''. + +=== Syntax of s-expressions +There are only two types of elements: atoms and 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. + +There are three syntactic forms for an atom: numbers, symbols and strings. + +A number is a non-empty sequence of digits (""0"" ... ""9""). +The smallest number is ``0``, there are no negative numbers. + +A symbol is a non-empty sequence of printable characters, except left or right parenthesis. +Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S). +Symbols are case-insensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote the same symbol. + +A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark. +To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C). +To allow a string to contain a backslash, it also must be prefixed by one backslash. +Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character. +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). + +Atoms are separated by Unicode characters of category separator (Z). DELETED docs/manual/00001012920525.zettel Index: docs/manual/00001012920525.zettel ================================================================== --- docs/manual/00001012920525.zettel +++ docs/manual/00001012920525.zettel @@ -1,36 +0,0 @@ -id: 00001012920525 -title: SHTML Encoding -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230316181044 -modified: 20230403150657 - -A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics. -It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]]. - -It is (relatively) easy to parse and contains everything to transform it into real HTML. -In contrast to HTML, SHTML is easier to parse and to manipulate. -For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: - -* [[//z/00001012920525?enc=shtml&part=zettel]], -* [[//z/00001012920525?enc=shtml&part=meta]], -* [[//z/00001012920525?enc=shtml&part=content]]. - -If transferred via HTTP, the content type will be ''text/plain''. - -Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sz encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]]. - -=== Syntax of SHTML -There are only two types of elements: atoms and lists, similar to the Sz encoding. - -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. -Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements. -This allows a more space economic storage of data. - -An HTML tag like ``< a href="link">Text`` is encoded in SHTML with a list, where the first element is a symbol named a the tag. -The second element is an optional encoding of the tag's attributes. -Further elements are either other tag encodings or a string. -The above tag is encoded as ``(a (@ (href . "link")) "Text")``. -Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character). ADDED docs/manual/00001012920582.zettel Index: docs/manual/00001012920582.zettel ================================================================== --- docs/manual/00001012920582.zettel +++ docs/manual/00001012920582.zettel @@ -0,0 +1,14 @@ +id: 00001012920582 +title: ZJSON Encoding: List of Valid Metadata Value Objects Names +role: manual +tags: #api #manual #reference #zettelstore +syntax: zmk +modified: 20220223184324 + +Every Metadata value element is mapped to a JSON object with some well defined names / keys. + +|=Name | JSON Value | Meaning +| ''"\"'' | string | The type of the Zettelmarkup element. +| ''"i"'' | array | A sequence of [[inline-structured|00001007040000]] elements. +| ''"s"'' | string | The first / major string value of an element. +| ''"y"'' | array | A set of string values. ADDED docs/manual/00001012920584.zettel Index: docs/manual/00001012920584.zettel ================================================================== --- docs/manual/00001012920584.zettel +++ docs/manual/00001012920584.zettel @@ -0,0 +1,28 @@ +id: 00001012920584 +title: ZJSON Encoding: Mapping of Metadata Key Types to Object Names +role: manual +tags: #api #manual #reference #zettelstore +syntax: zmk +modified: 20220304114135 + +Every [[Metadata key|00001006030000]] is mapped to an [[object name|00001012920582]] where its value is encoded. + +|=Type | JSON Object Name | Remark +| [[Credential|00001006031000]] | ''"s"'' | A string with the decrypted credential. +| [[EString|00001006031500]] | ''"s"'' | A possibly empty string. +| [[Identifier|00001006032000]] | ''"s"'' | A string containing a [[zettel identifier|00001006050000]]. +| [[IdentifierSet|00001006032500]] | ''"y"'' | An array of strings containing [[zettel identifier|00001006050000]]. +| [[Number|00001006033000]] | ''"s"'' | A string containing a numeric value. +| [[String|00001006033500]] | ''"s"'' | A non-empty string. +| [[TagSet|00001006034000]] | ''"y"'' | An array of string containing zettel tags. +| [[Timestamp|00001006034500]] | ''"s"'' | A string containing a timestamp in the format YYYYMMDDHHmmSS. +| [[URL|00001006035000]] | ''"s"'' | A string containing an URL. +| [[Word|00001006035500]] | ''"s"'' | A string containing a word (no space characters) +| [[WordSet|00001006036000]] | ''"y"'' | An array of strings containing words. +| [[Zettelmarkup|00001006036500]] | ''"i"'' | A sequence of [[inline-structured|00001007040000]] elements. + +Please note, that metadata is weakly typed. +Every metadata key expects a certain type. +But the user is free to enter something different. +For example, even if the metadata type is ""number"", its value could still be ""abc"". +However, the mapping itself is always valid. ADDED docs/manual/00001012920588.zettel Index: docs/manual/00001012920588.zettel ================================================================== --- docs/manual/00001012920588.zettel +++ docs/manual/00001012920588.zettel @@ -0,0 +1,24 @@ +id: 00001012920588 +title: ZJSON Encoding: List of Valid Zettelmarkup Element Objects Names +role: manual +tags: #api #manual #reference #zettelstore +syntax: zmk +modified: 20220301102447 + +Every [[Zettelmarkup|00001007000000]] element is mapped to a JSON object with some well defined names / keys. + +|=Name | JSON Value | Meaning +| ''"\"'' | string | The type of the Zettelmarkup element. +| ''"a"'' | object | Additional attributes of the element. +| ''"b"'' | array | A sequence of [[block-structured|00001007030000]] elements. +| ''"c"'' | array | A sequence of a sequence of (sub-) list elements or [[inline-structured|00001007040000]] elements. Used for nested lists. +| ''"d"'' | array | A sequence of description list elements, where each element is an object of a definition term and a list of descriptions. +| ''"e"'' | array | A sequence of descriptions: a JSON array of simple description, which is itself a JSON array of block structured elements. +| ''"i"'' | array | A sequence of [[inline-structured|00001007040000]] elements. +| ''"j"'' | object | An objects describing a BLOB element. +| ''"n"'' | number | A numeric value, e.g. for specifying the [[heading|00001007030300]] level. +| ''"o"'' | string | A base64 encoded binary value. Used in some BLOB elements. +| ''"p"'' | array | A sequence of two elements: a sequence of [[table|00001007031000]] header value, followed by a sequence of sequence of table row values. +| ''"q"'' | string | A second string value, if ''""s""'' is already used. +| ''"s"'' | string | The first / major string value of an element. +| ''"v"'' | string | A third string value, if ''""q""'' is already used. Index: docs/manual/00001012921000.zettel ================================================================== --- docs/manual/00001012921000.zettel +++ docs/manual/00001012921000.zettel @@ -1,23 +1,14 @@ id: 00001012921000 -title: API: Structure of an access token -role: manual +title: API: JSON structure of an access token tags: #api #manual #reference #zettelstore syntax: zmk -created: 20210126175322 -modified: 20230807165915 +role: manual If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned. The same is true, if the access token was [[renewed|00001012050400]]. -The response is structured as a [[symbolic expression|00001012930000]] list, with the following elements: - -# The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]] -# The token itself, which is technically the string representation of a [[symbolic expression|00001012930500]] containing relevant data, plus a check sum. -#* The symbolic expression has the form ''(KIND USERNAME NOW EXPIRE Z-ID)'' -#* ''KIND'' is ''0'' for an API access, ''1'' if it created for the Web user interface. -#* ''USERNAME'' is the user name of the user. -#* ''NOW'' is a timestamp of the current time. -#* ''EXPIRE'' is the timestamp when the access token expires. -#* ''Z-ID'' is the zettel identifier of the user zettel. - The symbolic expression is encoded via ""base64"". - Based on this encoding, a checksum is calculated, also encoded via ""base64"". - Both encoded values are concatenated, with a period (''"."'') as a delimiter. +The response is structured as an JSON object, with the following named values: + +|=Name|Description +|''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915) +|''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]] +|''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds Index: docs/manual/00001012921200.zettel ================================================================== --- docs/manual/00001012921200.zettel +++ docs/manual/00001012921200.zettel @@ -1,15 +1,14 @@ id: 00001012921200 title: API: Encoding of Zettel Access Rights role: manual tags: #api #manual #reference #zettelstore syntax: zmk -created: 20220201173115 -modified: 20230807164817 +modified: 20220201171959 -Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has. -''N'' is an integer number between 0 and 62.[^Not all values in this range are used.] +Various API calls return a JSON key ''"rights"'' that encodes the access rights the user currently has. +It is an integer number between 0 and 62.[^Not all values in this range are used.] The value ""0"" signals that something went wrong internally while determining the access rights. A value of ""1"" says, that the current user has no access right for the given zettel. In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user. DELETED docs/manual/00001012930000.zettel Index: docs/manual/00001012930000.zettel ================================================================== --- docs/manual/00001012930000.zettel +++ docs/manual/00001012930000.zettel @@ -1,26 +0,0 @@ -id: 00001012930000 -title: Symbolic Expression -role: manual -tags: #manual #reference #zettelstore -syntax: zmk -created: 20230403145644 -modified: 20230403154010 - -A symbolic expression (also called __s-expression__) is a notation of a list-based tree. -Inner nodes are lists of arbitrary length, outer nodes are primitive values (also called __atoms__) or the empty list. - -A symbolic expression is either -* a primitive value (__atom__), or -* a list of the form __(E,,1,, E,,2,, … E,,n,,)__, where __E,,i,,__ is itself a symbolic expression, separated by space characters. - -An atom is a number, a string, or a symbol. - -Symbolic expressions are used in programming languages like LISP or Scheme, where they denote both data structures and the program itself. -This allows a LISP or Scheme program to process LISP or Scheme programs. -That property is also used within Zettelstore. - -Symbolic expressions are relatively easy to read, to parse, and to process. - -=== See also -* [[Syntax|00001012930500]] of symbolic expressions in the Zettelstore -* [[S-expression @ Wikipedia|https://en.wikipedia.org/wiki/S-expression]] DELETED docs/manual/00001012930500.zettel Index: docs/manual/00001012930500.zettel ================================================================== --- docs/manual/00001012930500.zettel +++ docs/manual/00001012930500.zettel @@ -1,77 +0,0 @@ -id: 00001012930500 -title: Syntax of Symbolic Expressions -role: manual -tags: #manual #reference #zettelstore -syntax: zmk -created: 20230403151127 -modified: 20240413160345 - -=== 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. - -Internally, lists are composed of __cells__. -A cell allows to store two values. -The first value references the first element of a list. -The second value references the rest of the list, or is the special value __nil__ if there is no rest of the list. - -However, it is possible to store an atom as the second value of the last cell. -In this case, before the last element of a list of at least two elements, a full stop character (""''.''"", U+002E) signals such a cell as the last two elements. -This allows a more space economic storage of data. - -A __proper__ list, which contains __nil__ as the second value of the last element might be pictured as follows: - -~~~draw -+---+---+ +---+---+ +---+---+ -| V | N +-->| V | N +--> -->| V | | -+-+-+---+ +-+-+---+ +-+-+---+ - | | | - v v v -+-------+ +-------+ +-------+ -| 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)'' - -An improper list will have a non-__nil__ reference to an atom as the very last element - -~~~draw -+---+---+ +---+---+ +---+---+ -| V | N +-->| V | N +--> -->| V | V | -+-+-+---+ +-+-+---+ +-+-+-+-+ - | | | | - v v v v -+-------+ +-------+ +-------+ +------+ -| Elem1 | | Elem2 | | ElemN | | Atom | -+-------+ +-------+ +-------+ +------+ -~~~ - -Above improper list will be represented as an symbolic expression as ''(Elem1 Elem2 ... ElemN . Atom)'' - - -=== Syntax of numbers (atom) -A number is a non-empty sequence of digits (""0"" ... ""9""). -The smallest number is ``0``, there are no negative numbers. - -=== Syntax of symbols (atom) -A symbol is a non-empty sequence of printable characters, except left or right parenthesis. -Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S). -Symbols are case-sensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote different symbols. - -=== Syntax of string (atom) - -A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark. -To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C). -To allow a string to contain a backslash, it also must be prefixed by one backslash. -Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character. -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 [[Sx|https://t73f.de/r/sx]] (""Symbolic eXpression framework"") to implement symbolic expressions. - The project page might contain additional information about the full syntax. - - Zettelstore only uses lists, numbers, string, and symbols to represent zettel. DELETED docs/manual/00001012931000.zettel Index: docs/manual/00001012931000.zettel ================================================================== --- docs/manual/00001012931000.zettel +++ docs/manual/00001012931000.zettel @@ -1,80 +0,0 @@ -id: 00001012931000 -title: Encoding of Sz -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230403153903 -modified: 20240123120319 - -Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]]. -To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression. - -Basically, each part of a zettel is represented as a list, often a nested list. -The first element of that list is always an unique symbol, which denotes that part. -The meaning / semantic of all other elements depend on that symbol. - -=== Zettel -A full zettel is represented by a list of two elements. -The first elements represents the metadata, the second element represents the zettel content. - -:::syntax -__Zettel__ **=** ''('' [[__Metadata__|#metadata]] [[__Content__|#content]] '')''. -::: - -=== Metadata - -Metadata is represented by a list, where the first element is the symbol ''META''. -Following elements represent each metadatum[^""Metadatum"" is used as the singular form of metadata.] of a zettel in standard order. - -Standard order is: [[Title|00001006020000#title]], [[Role|00001006020000#role]], [[Tags|00001006020000#tags]], [[Syntax|00001006020000#syntax]], all other [[keys|00001006020000]] in alphabetic order. - -:::syntax -__Metadata__ **=** ''(META'' [[__Metadatum__|00001012931200]] … '')''. -::: -=== Content - -Zettel content is represented by a block. -:::syntax -__Content__ **=** [[__Block__|#block]]. -::: - -==== Block -A block is represented by a list with the symbol ''BLOCK'' as the first element. -All following elements represent a nested [[block-structured element|00001007030000]]. - -:::syntax -[!block|__Block__] **=** ''(BLOCK'' [[__BlockElement__|00001012931400]] … '')''. -::: - -==== Inline -Both block-structured elements and some metadata values may contain [[inline-structured elements|00001007040000]]. -Similar, inline-structured elements are represented as follows: - -:::syntax -__Inline__ **=** ''(INLINE'' [[__InlineElement__|00001012931600]] … '')''. -::: - -==== Attribute -[[Attributes|00001007050000]] may be specified for both block- and inline- structured elements. -Attributes are represented by the following schema. - -:::syntax -__Attribute__ **=** ''('' **[** [[__AttributeKeyValue__|00001012931800]] … **]** ')'. -::: - -Either, there are no attributes. -These are specified by the empty list ''()''. -Or there are attributes. -In this case, the first element of the list must be the symbol ''quote'': ''(quote'' ''('' A,,1,, A,,2,, … A,,n,, '')'''')''. - -=== Other -A list with ''UNKNOWN'' as its first element signals an internal error during transforming a zettel into the Sz encoding. -It may be ignored, or it may produce an error. - -:::syntax -__Unknown__ **=** ''(UNKNOWN'' Object … '')''. -::: - -The list may only contain the symbol ''UNKNOWN'', or additionally an unlimited amount of other objects. - -Similar, any symbol with the pattern ''**xyz:NOT-FOUND**'', where ''xyz'' is any string, signals an internal error. DELETED docs/manual/00001012931200.zettel Index: docs/manual/00001012931200.zettel ================================================================== --- docs/manual/00001012931200.zettel +++ docs/manual/00001012931200.zettel @@ -1,35 +0,0 @@ -id: 00001012931200 -title: Encoding of Sz Metadata -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230403161618 -modified: 20240219161848 - -A single metadata (""metadatum"") is represented by a triple: a symbol representing the type, a symbol representing the key, and either a string or a list that represent the value. - -The symbol depends on the [[metadata key type|00001006030000]]. -The value also depends somehow on the key type: a set of values is represented as a list, all other values are represented by a string, even if it is a number. - -The following table maps key types to symbols and to the type of the value representation. - -|=Key Type<| Symbol<| Value< -| [[Credential|00001006031000]] | ''CREDENTIAL'' | string -| [[EString|00001006031500]] | ''EMPTY-STRING'' | string -| [[Identifier|00001006032000]] | ''ZID'' | string -| [[IdentifierSet|00001006032500]] | ''ZID-SET'' | ListValue -| [[Number|00001006033000]] | ''NUMBER'' | string -| [[String|00001006033500]] | ''STRING'' | string -| [[TagSet|00001006034000]] | ''TAG-SET'' | ListValue -| [[Timestamp|00001006034500]] | ''TIMESTAMP'' | string -| [[URL|00001006035000]] | ''URL'' | string -| [[Word|00001006035500]] | ''WORD'' | string -| [[Zettelmarkup|00001006036500]] | ''ZETTELMARKUP'' | string - -:::syntax -__ListValue__ **=** ''('' String,,1,, String,,2,, … String,,n,, '')''. -::: - -Examples: -* The title of this zettel is represented as: ''(EMPTY-STRING title "Encoding of Sz Metadata")'' -* The tags of this zettel are represented as: ''(TAG-SET tags ("#api" "#manual" "#reference" "#zettelstore"))'' DELETED docs/manual/00001012931400.zettel Index: docs/manual/00001012931400.zettel ================================================================== --- docs/manual/00001012931400.zettel +++ docs/manual/00001012931400.zettel @@ -1,186 +0,0 @@ -id: 00001012931400 -title: Encoding of Sz Block Elements -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230403161803 -modified: 20240123120132 - -=== ''PARA'' -:::syntax -__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] … '')''. -::: -A paragraph is just a list of inline elements. - -=== ''HEADING'' -:::syntax -__Heading__ **=** ''(HEADING'' Number [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] … '')''. -::: -A heading consists of a number, which specifies its level (1 -- 5), its optional attributes. -The first string is a ""slug"" of the heading text, i.e. transformed it to lower case, replaced space character with minus sign, and some more. -The second string is the slug, but made unique for the whole zettel. -Then the heading text follows as a sequence of inline elements. - -=== ''THEMATIC'' -:::syntax -__Thematic__ **=** ''(THEMATIC'' [[__Attributes__|00001012931000#attribute]] '')''. -::: - -=== ''ORDERED'', ''UNORDERED'', ''QUOTATION'' -These three symbols are specifying different kinds of lists / enumerations: an ordered list, an unordered list, and a quotation list. -Their structure is the same. - -:::syntax -__OrderedList__ **=** ''(ORDERED'' __ListElement__ … '')''. - -__UnorderedList__ **=** ''(UNORDERED'' __ListElement__ … '')''. - -__QuotationList__ **=** ''(QUOTATION'' __ListElement__ … '')''. -::: - -:::syntax -__ListElement__ **=** [[__Block__|00001012931000#block]] **|** [[__Inline__|00001012931000#inline]]. -::: -A list element is either a block or an inline. -If it is a block, it may contain a nested list. -=== ''DESCRIPTION'' -:::syntax -__Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ … '')''. -::: -A description is a sequence of one ore more terms and values. - -:::syntax -__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] … '')''. -::: -A description term is just an inline-structured value. - -:::syntax -__DescriptionValues__ **=** ''(BLOCK'' [[__Block__|00001012931000#block]] … '')''. -::: -Description values are sequences of blocks. - -=== ''TABLE'' -:::syntax -__Table__ **=** ''(TABLE'' __TableHeader__ __TableRow__ … '')''. -::: -A table is a table header and a sequence of table rows. - -:::syntax -__TableHeader__ **=** ''()'' **|** ''('' __TableCell__ … '')''. -::: -A table header is either the empty list or a list of table cells. - -:::syntax -__TableRow__ **=** ''('' __TableCell__ … '')''. -::: -A table row is a list of table cells. - -=== ''CELL'', ''CELL-*'' -There are four kinds of table cells, one for each possible cell alignment. -The structure is the same for all kind. - -:::syntax -__TableCell__ **=** __DefaultCell__ **|** __CenterCell__ **|** __LeftCell__ **|** __RightCell__. -::: - -:::syntax -__DefaultCell__ **=** ''(CELL'' [[__InlineElement__|00001012931600]] … '')''. -::: -The cell content, specified by the sequence of inline elements, used the default alignment. - -:::syntax -__CenterCell__ **=** ''(CELL-CENTER'' [[__InlineElement__|00001012931600]] … '')''. -::: -The cell content, specified by the sequence of inline elements, is centered aligned. - -:::syntax -__LeftCell__ **=** ''(CELL-LEFT'' [[__InlineElement__|00001012931600]] … '')''. -::: -The cell content, specified by the sequence of inline elements, is left aligned. - -:::syntax -__RightCell__ **=** ''(CELL-RIGHT'' [[__InlineElement__|00001012931600]] … '')''. -::: -The cell content, specified by the sequence of inline elements, is right aligned. - -=== ''REGION-*'' -The following lists specifies different kinds of regions. -A region treat a sequence of block elements to be belonging together in certain ways. -The have a similar structure. - -:::syntax -__BlockRegion__ **=** ''(REGION-BLOCK'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. -::: -A block region just treats the block to belong in an unspecified way. -Typically, the reason is given in the attributes. -The inline describes the block. - -:::syntax -__QuoteRegion__ **=** ''(REGION-QUOTE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. -::: -A block region just treats the block to contain a longer quotation. -Attributes may further specify the quotation. -The inline typically describes author / source of the quotation. - -:::syntax -__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] … '')'' [[__InlineElement__|00001012931600]] … '')''. -::: -A block region just treats the block to contain a verse. -Soft line break are transformed into hard line breaks to save the structure of the verse / poem. -Attributes may further specify something. -The inline typically describes author / source of the verse. - -=== ''VERBATIM-*'' -The following lists specifies some literal text of more than one line. -The structure is always the same, the initial symbol denotes the actual usage. -The content is encoded as a string, most likely to contain control characters that signals the end of a line. - -:::syntax -__CommentVerbatim__ **=** ''(VERBATIM-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as an internal comment not to be interpreted further. - -:::syntax -__EvalVerbatim__ **=** ''(VERBATIM-EVAL'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be evaluated by an (external) software to produce some derived content. - -:::syntax -__HTMLVerbatim__ **=** ''(VERBATIM-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains HTML code. - -:::syntax -__MathVerbatim__ **=** ''(VERBATIM-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as special code to be interpreted as mathematical formulas. - -:::syntax -__CodeVerbatim__ **=** ''(VERBATIM-CODE'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as (executable) code. - -:::syntax -__ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as (nested) zettel content. - -=== ''BLOB'' -:::syntax -__BLOB__ **=** ''(BLOB'' ''('' [[__InlineElement__|00001012931600]] … '')'' String,,1,, String,,2,, '')''. -::: -A BLOB contains an image in block mode. -The inline elements states some description. -The first string contains the syntax of the image. -The second string contains the actual image. -If the syntax is ""SVG"", then the second string contains the SVG code. -Otherwise the (binary) image data is encoded with base64. - -=== ''TRANSCLUDE'' -:::syntax -__Transclude__ **=** ''(TRANSCLUDE'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] '')''. -::: -A transclude list only occurs for a parsed zettel, but not for a evaluated zettel. -Evaluating a zettel also means that all transclusions are resolved. - -__Reference__ denotes the zettel to be transcluded. DELETED docs/manual/00001012931600.zettel Index: docs/manual/00001012931600.zettel ================================================================== --- docs/manual/00001012931600.zettel +++ docs/manual/00001012931600.zettel @@ -1,217 +0,0 @@ -id: 00001012931600 -title: Encoding of Sz Inline Elements -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230403161845 -modified: 20240122122448 - -=== ''TEXT'' -:::syntax -__Text__ **=** ''(TEXT'' String '')''. -::: -Specifies the string as some text content, typically a word. - -=== ''SPACE'' -:::syntax -__Space__ **=** ''(SPACE'' **[** String **]** '')''. -::: -Specifies some space, typically white space. -If the string is not given it is assumed to be ''" "'' (one space character). -Otherwise it contains the space characters. - -=== ''SOFT'' -:::syntax -__Soft__ **=** ''(SOFT)''. -::: -Denotes a soft line break. -It is typically translated into a space character, but signals the point in the textual content, where a line break occurred. - -=== ''HARD'' -:::syntax -__Hard__ **=** ''(HARD)''. -::: -Specifies a hard line break, i.e. the user wants to have a line break here. - -=== ''LINK-*'' -The following lists specify various links, based on the full reference. -They all have the same structure, with a trailing sequence of __InlineElements__ that contain the linked text. - -:::syntax -__InvalidLink__ **=** ''(LINK-INVALID'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains the invalid link specification. - -:::syntax -__ZettelLink__ **=** ''(LINK-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains the zettel identifier, a zettel reference. - -:::syntax -__SelfLink__ **=** ''(LINK-SELF'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains the number sign character and the name of a zettel mark. -It reference the same zettel where it occurs. - -:::syntax -__FoundLink__ **=** ''(LINK-FOUND'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a zettel identifier, a zettel reference, of a zettel known to be included in the Zettelstore. - -:::syntax -__BrokenLink__ **=** ''(LINK-BROKEN'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a zettel identifier, a zettel reference, of a zettel known to be __not__ included in the Zettelstore. - -:::syntax -__HostedLink__ **=** ''(LINK-HOSTED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a link starting with one slash character, denoting an absolute local reference. - -:::syntax -__BasedLink__ **=** ''(LINK-BASED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a link starting with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL. - -:::syntax -__QueryLink__ **=** ''(LINK-BASED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a [[query expression|00001007700000]]. - -:::syntax -__ExternalLink__ **=** ''(LINK-EXTERNAL'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains a full URL, referencing a resource outside of the Zettelstore server. - -=== ''EMBED'' -:::syntax -__Embed__ **=** ''(EMBED'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] String [[__InlineElement__|00001012931600]] … '')''. -::: -Specifies an embedded element, in most cases some image content or an inline transclusion. -A transclusion will only be used, if the zettel content was not evaluated. - -__Reference__ denoted the referenced zettel. - -If the string value is empty, it is an inline transclusion. -Otherwise it contains the syntax of an image. - -The __InlineElement__ at the end of the list is interpreted as describing text. - -=== ''EMBED-BLOB'' -:::syntax -__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''. -::: -If used if some processed image has to be embedded inside some inline material. -The first string specifies the syntax of the image content. -The second string contains the image content. -If the syntax is ""SVG"", the image content is not further encoded. -Otherwise a base64 encoding is used. - -=== ''CITE'' -:::syntax -__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] … '')''. -::: -The string contains the citation key. - -=== ''MARK'' -:::syntax -__Mark__ **=** ''(MARK'' String,,1,, String,,2,, String,,3,, [[__InlineElement__|00001012931600]] … '')''. -::: -The first string is the mark string used in the content. -The second string is the mark string transformed to a slug, i.e. transformed to lower case, remove non-ASCII characters. -The third string is the slug string, but made unique for the whole zettel. -Then follows the marked text as a sequence of __InlineElement__s. - -=== ''ENDNOTE'' -:::syntax -__Endnote__ **=** ''(ENDNOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -Specifies endnote / footnote text. - -=== ''FORMAT-*'' -The following lists specifies some inline text formatting. -The structure is always the same, the initial symbol denotes the actual formatting. - -:::syntax -__DeleteFormat__ **=** ''(FORMAT-DELETE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as deleted. - -:::syntax -__EmphasizeFormat__ **=** ''(FORMAT-EMPH'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as emphasized. - -:::syntax -__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as inserted. - -:::syntax -__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as highlighted for the reader (but was not important fto the original author). - -:::syntax -__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as quoted text. - -:::syntax -__SpanFormat__ **=** ''(FORMAT-SPAN'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as belonging together in a certain, yet unspecified way. - -:::syntax -__SubScriptFormat__ **=** ''(FORMAT-SUB'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as sub-scripted text. - -:::syntax -__SuperScriptFormat__ **=** ''(FORMAT-SUPER'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as super-scripted text. - -:::syntax -__StrongFormat__ **=** ''(FORMAT-STRONG'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] … '')''. -::: -The inline text should be treated as strongly emphasized. - -=== ''LITERAL-*'' -The following lists specifies some literal text. -The structure is always the same, the initial symbol denotes the actual usage. - -:::syntax -__CodeLiteral__ **=** ''(LITERAL-CODE'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as executable code. - -:::syntax -__CommentLiteral__ **=** ''(LITERAL-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as an internal comment not to be interpreted further. - -:::syntax -__HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as HTML code. - -:::syntax -__InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as input entered by an user. - -:::syntax -__MathLiteral__ **=** ''(LITERAL-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as special code to be interpreted as mathematical formulas. - -:::syntax -__OutputLiteral__ **=** ''(LITERAL-OUTPUT'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as computer output to be read by an user. - -:::syntax -__ZettelLiteral__ **=** ''(LITERAL-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''. -::: -The string contains text that should be treated as (nested) zettel content. DELETED docs/manual/00001012931800.zettel Index: docs/manual/00001012931800.zettel ================================================================== --- docs/manual/00001012931800.zettel +++ docs/manual/00001012931800.zettel @@ -1,27 +0,0 @@ -id: 00001012931800 -title: Encoding of Sz Attribute Values -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230403161923 -modified: 20240122115245 - -An attribute is represented by a single cell. -The first element of the cell references the attribute key, the second value the corresponding value. - -:::syntax -__AttributeKeyValue__ **=** ''('' __AttributeKey__ ''.'' __AttributeValue__ '')''. -::: - -__AttributeKey__ and __AttributeValue__ are [[string values|00001012930500]]. - -An empty key denotes the generic attribute. - -A key with the value ''"-"'' specifies the default attribute. -In this case, the attribute value is not interpreted. - -Some examples: -* ''()'' represents the absence of attributes, -* ''(("-" . ""))'' represent the default attribute, -* ''(("-" . "") ("" . "syntax"))'' adds the generic attribute with the value ""syntax"", -* ''(("lang" . "en"))'' denotes the attribute key ""lang"" with a value ""en"". DELETED docs/manual/00001012931900.zettel Index: docs/manual/00001012931900.zettel ================================================================== --- docs/manual/00001012931900.zettel +++ docs/manual/00001012931900.zettel @@ -1,42 +0,0 @@ -id: 00001012931900 -title: Encoding of Sz Reference Values -role: manual -tags: #api #manual #reference #zettelstore -syntax: zmk -created: 20230405123046 -modified: 20240122094720 - -A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value. - -:::syntax -__Reference__ **=** ''('' __ReferenceState__ String '')''. -::: -The string contains the actual reference value. - -:::syntax -__ReferenceState__ **=** ''INVALID'' **|** ''ZETTEL'' **|** ''SELF'' **|** ''FOUND'' **|** ''BROKEN'' **|** ''HOSTED'' **|** ''BASED'' **|** ''QUERY'' **|** ''EXTERNAL''. -::: - -The meaning of the state symbols corresponds to that of the symbols used for the description of [[link references|00001012931600#link]]. - -; ''INVALID'' -: The reference value is invalid. -; ''ZETTEL'' -: The reference value is a reference to a zettel. - This value is only possible before evaluating the zettel. -; ''SELF'' -: The reference value is a reference to the same zettel, to a specific mark. -; ''FOUND'' -: The reference value is a valid reference to an existing zettel. - This value is only possible after evaluating the zettel. -; ''BROKEN'' -: The reference value is a valid reference to an missing zettel. - This value is only possible after evaluating the zettel. -; ''HOSTED'' -: The reference value starts with one slash character, denoting an absolute local reference. -; ''BASED'' -: The reference value starts with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL. -; ''QUERY'' -: The reference value contains a query expression. -; ''EXTERNAL'' -: The reference value contains a full URL, referencing a resource outside of the Zettelstore server. Index: docs/manual/00001017000000.zettel ================================================================== --- docs/manual/00001017000000.zettel +++ docs/manual/00001017000000.zettel @@ -2,52 +2,49 @@ title: Tips and Tricks role: manual tags: #manual #zettelstore syntax: zmk created: 20220803170112 -modified: 20231012154803 +modified: 20220916132030 === Welcome Zettel * **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users. In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel. Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]]. -* **Solution 1:** +* **Solution:** *# Create a new zettel with all your references to internal, non-public zettel. Let's assume this zettel receives the zettel identifier ''20220803182600''. *# Create the zettel that should serve as the starting zettel for your users. It must have syntax [[Zettelmarkup|00001008000000#zmk]], i.e. the syntax metadata must be set to ''zmk''. If needed, set the runtime configuration [[''home-zettel|00001004020000#home-zettel]] to the value of the identifier of this zettel. *# At the beginning of the start zettel, add the following [[Zettelmarkup|00001007000000]] text in a separate paragraph: ``{{{20220803182600}}}`` (you have to adapt to the actual value of the zettel identifier for your non-public home zettel). * **Discussion:** As stated in the description for a [[transclusion|00001007031100]], a transclusion will be ignored, if the transcluded zettel is not visible to the current user. In effect, the transclusion statement (above paragraph that contained ''{{{...}}}'') is ignored when rendering the zettel. -* **Solution 2:** Set a user-specific value by adding metadata ''home-zettel'' to the [[user zettel|00001010040200]]. -* **Discussion:** A value for ''home-zettel'' is first searched in the user zettel of the current authenticated user. - Only if it is not found, the value is looked up in the runtime configuration zettel. - If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel. === Role-specific Layout of Zettel in Web User Interface (WebUI) [!role-css] * **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]]. For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel. Or you want a multi-column layout. -* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Sxn Start Code|00000000019000]]"" (its identifier is ''00000000019000''). - This zettel is the starting point for Sxn code, where you can place a definition for a variable named ""CSS-ROLE-map"". +* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000''). + This zettel maps a role name to a zettel that must contain the role-specific CSS code. - But first, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). - Its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"". + First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must). + Set its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"". The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color. Let's assume, the newly created CSS zettel got the identifier ''20220825200100''. - Now, you have to map this freshly created zettel to a role, for example ""zettel"". - Since you have enabled ''expert-mode'', you are allowed to modify the zettel ""[[Zettelstore Sxn Start Code|00000000019000]]"". - Add the following code to the Sxn Start Code zettel: ``(set! CSS-ROLE-map '(("zettel" . "20220825200100")))``. + Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"". + Since you have enabled ''expert-mode'', you are allowed to modify it. + Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS. - In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. - For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should be something like ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``. + In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code. + It is allowed to assign more than one role to a specific CSS zettel. * **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role. For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata. +* **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout. === Zettel synchronization with iCloud (Apple) * **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers. * **Solution:** Place your zettel in an iCloud folder. Index: docs/manual/00001018000000.zettel ================================================================== --- docs/manual/00001018000000.zettel +++ docs/manual/00001018000000.zettel @@ -1,12 +1,12 @@ id: 00001018000000 title: Troubleshooting role: manual tags: #manual #zettelstore syntax: zmk -created: 20211027105921 -modified: 20240221134749 +created: 00010101000000 +modified: 20221020132617 This page lists some problems and their solutions that may occur when using your Zettelstore. === Installation * **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer. @@ -26,11 +26,11 @@ ** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file. ** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema. === Working with Zettel Files * **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change. - If you access the zettel via Zettelstore, an error is reported. + If you access the zettel via Zettelstore, a fatal error is reported. ** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel. This occurs mostly under MacOS. ** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists""). ** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change. ** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible. DELETED docs/manual/00001019990010.zettel Index: docs/manual/00001019990010.zettel ================================================================== --- docs/manual/00001019990010.zettel +++ docs/manual/00001019990010.zettel @@ -1,8 +0,0 @@ -id: 00001019990010 -title: #api -role: tag -syntax: zmk -created: 20230928185004 -modified: 20230928185204 - -Zettel with the tag ''#api'' contain a description of the [[API|00001012000000]]. DELETED docs/manual/20231128184200.zettel Index: docs/manual/20231128184200.zettel ================================================================== --- docs/manual/20231128184200.zettel +++ docs/manual/20231128184200.zettel @@ -1,7 +0,0 @@ -id: 20231128184200 -title: manual -role: role -syntax: zmk -created: 20231128184200 - -Zettel with the role ""manual"" contain the manual of the zettelstore. Index: docs/readmezip.txt ================================================================== --- docs/readmezip.txt +++ docs/readmezip.txt @@ -15,6 +15,7 @@ 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. +To get in contact with the developer, send an email to ds@zettelstore.de or +follow Zettelstore on Twitter: https://twitter.com/zettelstore. ADDED domain/content.go Index: domain/content.go ================================================================== --- domain/content.go +++ domain/content.go @@ -0,0 +1,129 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 domain provides domain specific types, constants, and functions. +package domain + +import ( + "bytes" + "encoding/base64" + "errors" + "io" + "unicode" + "unicode/utf8" + + "zettelstore.de/z/input" +) + +// Content is just the content of a zettel. +type Content struct { + data []byte + isBinary bool +} + +// NewContent creates a new content from a string. +func NewContent(data []byte) Content { + return Content{data: data, isBinary: calcIsBinary(data)} +} + +// Length returns the number of bytes stored. +func (zc *Content) Length() int { return len(zc.data) } + +// Equal compares two content values. +func (zc *Content) Equal(o *Content) bool { + if zc == nil { + return o == nil + } + if zc.isBinary != o.isBinary { + return false + } + return bytes.Equal(zc.data, o.data) +} + +// Set content to new string value. +func (zc *Content) Set(data []byte) { + zc.data = data + zc.isBinary = calcIsBinary(data) +} + +// Write it to a Writer +func (zc *Content) Write(w io.Writer) (int, error) { + return w.Write(zc.data) +} + +// AsString returns the content itself is a string. +func (zc *Content) AsString() string { return string(zc.data) } + +// AsBytes returns the content itself is a byte slice. +func (zc *Content) AsBytes() []byte { return zc.data } + +// IsBinary returns true if the content contains non-unicode values or is, +// interpreted a text, with a high probability binary content. +func (zc *Content) IsBinary() bool { return zc.isBinary } + +// TrimSpace remove some space character in content, if it is not binary content. +func (zc *Content) TrimSpace() { + if zc.isBinary { + return + } + inp := input.NewInput(zc.data) + pos := inp.Pos + for inp.Ch != input.EOS { + if input.IsEOLEOS(inp.Ch) { + inp.Next() + pos = inp.Pos + continue + } + if !input.IsSpace(inp.Ch) { + break + } + inp.Next() + } + zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace) +} + +// Encode content for future transmission. +func (zc *Content) Encode() (data, encoding string) { + if !zc.isBinary { + return zc.AsString(), "" + } + return base64.StdEncoding.EncodeToString(zc.data), "base64" +} + +// SetDecoded content to the decoded value of the given string. +func (zc *Content) SetDecoded(data, encoding string) error { + switch encoding { + case "": + zc.data = []byte(data) + case "base64": + decoded, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return err + } + zc.data = decoded + default: + return errors.New("unknown encoding " + encoding) + } + zc.isBinary = calcIsBinary(zc.data) + return nil +} + +func calcIsBinary(data []byte) bool { + if !utf8.Valid(data) { + return true + } + l := len(data) + for i := 0; i < l; i++ { + if data[i] == 0 { + return true + } + } + return false +} ADDED domain/content_test.go Index: domain/content_test.go ================================================================== --- domain/content_test.go +++ domain/content_test.go @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 domain_test + +import ( + "testing" + + "zettelstore.de/z/domain" +) + +func TestContentIsBinary(t *testing.T) { + t.Parallel() + td := []struct { + s string + exp bool + }{ + {"abc", false}, + {"äöü", false}, + {"", false}, + {string([]byte{0}), true}, + } + for i, tc := range td { + content := domain.NewContent([]byte(tc.s)) + got := content.IsBinary() + if got != tc.exp { + t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got) + } + } +} + +func TestTrimSpace(t *testing.T) { + t.Parallel() + testcases := []struct { + in, exp string + }{ + {"", ""}, + {" ", ""}, + {"abc", "abc"}, + {" abc", " abc"}, + {"abc ", "abc"}, + {"abc \n", "abc"}, + {"abc\n ", "abc"}, + {"\nabc", "abc"}, + {" \nabc", "abc"}, + {" \n abc", " abc"}, + {" \n\n abc", " abc"}, + {" \n \n abc", " abc"}, + {" \n \n abc \n \n ", " abc"}, + } + for _, tc := range testcases { + c := domain.NewContent([]byte(tc.in)) + c.TrimSpace() + got := c.AsString() + if got != tc.exp { + t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got) + } + } +} ADDED domain/id/id.go Index: domain/id/id.go ================================================================== --- domain/id/id.go +++ domain/id/id.go @@ -0,0 +1,163 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 id provides domain specific types, constants, and functions about +// zettel identifier. +package id + +import ( + "strconv" + "time" + + "zettelstore.de/c/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, +// e.g. the seconds should be zero, +type Zid uint64 + +// Some important ZettelIDs. +const ( + Invalid = Zid(0) // Invalid is a Zid that will never be valid +) + +// ZettelIDs that are used as Zid more than once. +// +// Note: if you change some values, ensure that you also change them in the +// Constant box. They are mentioned there literally, because these +// constants are not available there. +var ( + ConfigurationZid = MustParse(api.ZidConfiguration) + BaseTemplateZid = MustParse(api.ZidBaseTemplate) + LoginTemplateZid = MustParse(api.ZidLoginTemplate) + ListTemplateZid = MustParse(api.ZidListTemplate) + ZettelTemplateZid = MustParse(api.ZidZettelTemplate) + InfoTemplateZid = MustParse(api.ZidInfoTemplate) + FormTemplateZid = MustParse(api.ZidFormTemplate) + RenameTemplateZid = MustParse(api.ZidRenameTemplate) + DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) + ContextTemplateZid = MustParse(api.ZidContextTemplate) + ErrorTemplateZid = MustParse(api.ZidErrorTemplate) + RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) + EmojiZid = MustParse(api.ZidEmoji) + TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) + DefaultHomeZid = MustParse(api.ZidDefaultHome) +) + +const maxZid = 99999999999999 + +// ParseUint interprets a string as a possible zettel identifier +// and returns its integer value. +func ParseUint(s string) (uint64, error) { + res, err := strconv.ParseUint(s, 10, 47) + if err != nil { + return 0, err + } + if res == 0 || res > maxZid { + return res, strconv.ErrRange + } + return res, nil +} + +// Parse interprets a string as a zettel identification and +// returns its value. +func Parse(s string) (Zid, error) { + if len(s) != 14 { + return Invalid, strconv.ErrSyntax + } + res, err := ParseUint(s) + if err != nil { + return Invalid, err + } + return Zid(res), nil +} + +// MustParse tries to interpret a string as a zettel identifier and returns +// its value or panics otherwise. +func MustParse(s api.ZettelID) Zid { + zid, err := Parse(string(s)) + if err == nil { + return zid + } + panic(err) +} + +// String converts the zettel identification to a string of 14 digits. +// Only defined for valid ids. +func (zid Zid) String() string { + var result [14]byte + zid.toByteArray(&result) + return string(result[:]) +} + +// Bytes converts the zettel identification to a byte slice of 14 digits. +// Only defined for valid ids. +func (zid Zid) Bytes() []byte { + var result [14]byte + zid.toByteArray(&result) + return result[:] +} + +// toByteArray converts the Zid into a fixed byte array, usable for printing. +// +// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly" +// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/ +func (zid Zid) toByteArray(result *[14]byte) { + date := uint64(zid) / 1000000 + fullyear := date / 10000 + century := fullyear / 100 + year := fullyear % 100 + monthday := date % 10000 + month := monthday / 100 + day := monthday % 100 + time := uint64(zid) % 1000000 + hmtime := time / 100 + second := time % 100 + hour := hmtime / 100 + minute := hmtime % 100 + result[0] = byte(century/10) + '0' + result[1] = byte(century%10) + '0' + result[2] = byte(year/10) + '0' + result[3] = byte(year%10) + '0' + result[4] = byte(month/10) + '0' + result[5] = byte(month%10) + '0' + result[6] = byte(day/10) + '0' + result[7] = byte(day%10) + '0' + result[8] = byte(hour/10) + '0' + result[9] = byte(hour%10) + '0' + result[10] = byte(minute/10) + '0' + result[11] = byte(minute%10) + '0' + result[12] = byte(second/10) + '0' + result[13] = byte(second%10) + '0' +} + +// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits. +func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid } + +// ZidLayout to transform a date into a Zid and into other internal dates. +const ZidLayout = "20060102150405" + +// New returns a new zettel id based on the current time. +func New(withSeconds bool) Zid { + now := time.Now().Local() + var s string + if withSeconds { + s = now.Format(ZidLayout) + } else { + s = now.Format("20060102150400") + } + res, err := Parse(s) + if err != nil { + panic(err) + } + return res +} ADDED domain/id/id_test.go Index: domain/id/id_test.go ================================================================== --- domain/id/id_test.go +++ domain/id/id_test.go @@ -0,0 +1,87 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 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 id_test provides unit tests for testing zettel id specific functions. +package id_test + +import ( + "testing" + + "zettelstore.de/z/domain/id" +) + +func TestIsValid(t *testing.T) { + t.Parallel() + validIDs := []string{ + "00000000000001", + "00000000000020", + "00000000000300", + "00000000004000", + "00000000050000", + "00000000600000", + "00000007000000", + "00000080000000", + "00000900000000", + "00001000000000", + "00020000000000", + "00300000000000", + "04000000000000", + "50000000000000", + "99999999999999", + "00001007030200", + "20200310195100", + } + + for i, sid := range validIDs { + zid, err := id.Parse(sid) + if err != nil { + t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err) + } + s := zid.String() + if s != sid { + t.Errorf( + "i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s) + } + } + + invalidIDs := []string{ + "", "0", "a", + "00000000000000", + "0000000000000a", + "000000000000000", + "20200310T195100", + } + + 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) + } + } +} + +var sResult string // to disable compiler optimization in loop below + +func BenchmarkString(b *testing.B) { + var s string + for n := 0; n < b.N; n++ { + s = id.Zid(12345678901200).String() + } + sResult = s +} + +var bResult []byte // to disable compiler optimization in loop below + +func BenchmarkBytes(b *testing.B) { + var bs []byte + for n := 0; n < b.N; n++ { + bs = id.Zid(12345678901200).Bytes() + } + bResult = bs +} ADDED domain/id/set.go Index: domain/id/set.go ================================================================== --- domain/id/set.go +++ domain/id/set.go @@ -0,0 +1,119 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-2022 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 id + +// Set is a set of zettel identifier +type Set map[Zid]struct{} + +// NewSet returns a new set of identifier with the given initial values. +func NewSet(zids ...Zid) Set { + l := len(zids) + if l < 8 { + l = 8 + } + result := make(Set, l) + result.AddSlice(zids) + return result +} + +// NewSetCap returns a new set of identifier with the given capacity and initial values. +func NewSetCap(c int, zids ...Zid) Set { + l := len(zids) + if c < l { + c = l + } + if c < 8 { + c = 8 + } + result := make(Set, c) + result.AddSlice(zids) + return result +} + +// Zid adds a Zid to the set. +func (s Set) Zid(zid Zid) Set { + if s == nil { + return NewSet(zid) + } + s[zid] = struct{}{} + return s +} + +// Contains return true if the set is nil or if the set contains the given Zettel identifier. +func (s Set) Contains(zid Zid) bool { + if s != nil { + _, found := s[zid] + return found + } + return true +} + +// Add all member from the other set. +func (s Set) Add(other Set) Set { + if s == nil { + return other + } + for zid := range other { + s[zid] = struct{}{} + } + return s +} + +// AddSlice adds all identifier of the given slice to the set. +func (s Set) AddSlice(sl Slice) { + for _, zid := range sl { + s[zid] = struct{}{} + } +} + +// Sorted returns the set as a sorted slice of zettel identifier. +func (s Set) Sorted() Slice { + if l := len(s); l > 0 { + result := make(Slice, 0, l) + for zid := range s { + result = append(result, zid) + } + result.Sort() + return result + } + return nil +} + +// IntersectOrSet removes all zettel identifier that are not in the other set. +// Both sets can be modified by this method. One of them is the set returned. +// It contains the intersection of both, if s is not nil. +// +// If s == nil, then the other set is always returned. +func (s Set) IntersectOrSet(other Set) Set { + if s == nil { + return other + } + if len(s) > len(other) { + s, other = other, s + } + for zid := range s { + _, otherOk := other[zid] + if !otherOk { + delete(s, zid) + } + } + return s +} + +// Remove all zettel identifier from 's' that are in the set 'other'. +func (s Set) Remove(other Set) { + if s == nil || other == nil { + return + } + for zid := range other { + delete(s, zid) + } +} ADDED domain/id/set_test.go Index: domain/id/set_test.go ================================================================== --- domain/id/set_test.go +++ domain/id/set_test.go @@ -0,0 +1,149 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021-2022 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 id_test + +import ( + "testing" + + "zettelstore.de/z/domain/id" +) + +func TestSetContains(t *testing.T) { + t.Parallel() + testcases := []struct { + s id.Set + zid id.Zid + exp bool + }{ + {nil, id.Invalid, true}, + {nil, 14, true}, + {id.NewSet(), id.Invalid, false}, + {id.NewSet(), 1, false}, + {id.NewSet(), id.Invalid, false}, + {id.NewSet(1), 1, true}, + } + for i, tc := range testcases { + got := tc.s.Contains(tc.zid) + if got != tc.exp { + t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got) + } + } +} + +func TestSetAdd(t *testing.T) { + t.Parallel() + testcases := []struct { + s1, s2 id.Set + exp id.Slice + }{ + {nil, nil, nil}, + {id.NewSet(), nil, nil}, + {id.NewSet(), id.NewSet(), nil}, + {nil, id.NewSet(1), id.Slice{1}}, + {id.NewSet(1), nil, id.Slice{1}}, + {id.NewSet(1), id.NewSet(), id.Slice{1}}, + {id.NewSet(1), id.NewSet(2), id.Slice{1, 2}}, + {id.NewSet(1), id.NewSet(1), id.Slice{1}}, + } + for i, tc := range testcases { + sl1 := tc.s1.Sorted() + sl2 := tc.s2.Sorted() + got := tc.s1.Add(tc.s2).Sorted() + if !got.Equal(tc.exp) { + t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) + } + } +} + +func TestSetSorted(t *testing.T) { + t.Parallel() + testcases := []struct { + set id.Set + exp id.Slice + }{ + {nil, nil}, + {id.NewSet(), nil}, + {id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}}, + } + for i, tc := range testcases { + got := tc.set.Sorted() + if !got.Equal(tc.exp) { + t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got) + } + } +} + +func TestSetIntersectOrSet(t *testing.T) { + t.Parallel() + testcases := []struct { + s1, s2 id.Set + exp id.Slice + }{ + {nil, nil, nil}, + {id.NewSet(), nil, nil}, + {nil, id.NewSet(), nil}, + {id.NewSet(), id.NewSet(), nil}, + {id.NewSet(1), nil, nil}, + {nil, id.NewSet(1), id.Slice{1}}, + {id.NewSet(1), id.NewSet(), nil}, + {id.NewSet(), id.NewSet(1), nil}, + {id.NewSet(1), id.NewSet(2), nil}, + {id.NewSet(2), id.NewSet(1), nil}, + {id.NewSet(1), id.NewSet(1), id.Slice{1}}, + } + for i, tc := range testcases { + sl1 := tc.s1.Sorted() + sl2 := tc.s2.Sorted() + got := tc.s1.IntersectOrSet(tc.s2).Sorted() + if !got.Equal(tc.exp) { + t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) + } + } +} + +func TestSetRemove(t *testing.T) { + t.Parallel() + testcases := []struct { + s1, s2 id.Set + exp id.Slice + }{ + {nil, nil, nil}, + {id.NewSet(), nil, nil}, + {id.NewSet(), id.NewSet(), nil}, + {id.NewSet(1), nil, id.Slice{1}}, + {id.NewSet(1), id.NewSet(), id.Slice{1}}, + {id.NewSet(1), id.NewSet(2), id.Slice{1}}, + {id.NewSet(1), id.NewSet(1), id.Slice{}}, + } + for i, tc := range testcases { + sl1 := tc.s1.Sorted() + sl2 := tc.s2.Sorted() + newS1 := id.NewSet(sl1...) + newS1.Remove(tc.s2) + got := newS1.Sorted() + if !got.Equal(tc.exp) { + t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got) + } + } +} + +// func BenchmarkSet(b *testing.B) { +// s := id.Set{} +// for i := 0; i < b.N; i++ { +// s[id.Zid(i)] = true +// } +// } +func BenchmarkSet(b *testing.B) { + s := id.Set{} + for i := 0; i < b.N; i++ { + s[id.Zid(i)] = struct{}{} + } +} ADDED domain/id/slice.go Index: domain/id/slice.go ================================================================== --- domain/id/slice.go +++ domain/id/slice.go @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 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 id provides domain specific types, constants, and functions about +// zettel identifier. +package id + +import ( + "bytes" + "sort" +) + +// Slice is a sequence of zettel identifier. A special case is a sorted slice. +type Slice []Zid + +func (zs Slice) Len() int { return len(zs) } +func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] } +func (zs Slice) Swap(i, j int) { zs[i], zs[j] = zs[j], zs[i] } + +// Sort a slice of Zids. +func (zs Slice) Sort() { sort.Sort(zs) } + +// Copy a zettel identifier slice +func (zs Slice) Copy() Slice { + if zs == nil { + return nil + } + result := make(Slice, len(zs)) + copy(result, zs) + return result +} + +// Equal reports whether zs and other are the same length and contain the samle zettel +// identifier. A nil argument is equivalent to an empty slice. +func (zs Slice) Equal(other Slice) bool { + if len(zs) != len(other) { + return false + } + if len(zs) == 0 { + return true + } + for i, e := range zs { + if e != other[i] { + return false + } + } + return true +} + +func (zs Slice) String() string { + if len(zs) == 0 { + return "" + } + var buf bytes.Buffer + for i, zid := range zs { + if i > 0 { + buf.WriteByte(' ') + } + buf.WriteString(zid.String()) + } + return buf.String() +} ADDED domain/id/slice_test.go Index: domain/id/slice_test.go ================================================================== --- domain/id/slice_test.go +++ domain/id/slice_test.go @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2021 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 id provides domain specific types, constants, and functions about +// zettel identifier. +package id_test + +import ( + "testing" + + "zettelstore.de/z/domain/id" +) + +func TestSliceSort(t *testing.T) { + t.Parallel() + zs := id.Slice{9, 4, 6, 1, 7} + zs.Sort() + exp := id.Slice{1, 4, 6, 7, 9} + if !zs.Equal(exp) { + t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs) + } +} + +func TestCopy(t *testing.T) { + t.Parallel() + var orig id.Slice + got := orig.Copy() + if got != nil { + t.Errorf("Nil copy resulted in %v", got) + } + orig = id.Slice{9, 4, 6, 1, 7} + got = orig.Copy() + if !orig.Equal(got) { + t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got) + } +} + +func TestSliceEqual(t *testing.T) { + t.Parallel() + testcases := []struct { + s1, s2 id.Slice + exp bool + }{ + {nil, nil, true}, + {nil, id.Slice{}, true}, + {nil, id.Slice{1}, false}, + {id.Slice{1}, id.Slice{1}, true}, + {id.Slice{1}, id.Slice{2}, false}, + {id.Slice{1, 2}, id.Slice{2, 1}, false}, + {id.Slice{1, 2}, id.Slice{1, 2}, true}, + } + for i, tc := range testcases { + got := tc.s1.Equal(tc.s2) + if got != tc.exp { + t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got) + } + got = tc.s2.Equal(tc.s1) + if got != tc.exp { + t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got) + } + } +} + +func TestSliceString(t *testing.T) { + t.Parallel() + testcases := []struct { + in id.Slice + exp string + }{ + {nil, ""}, + {id.Slice{}, ""}, + {id.Slice{1}, "00000000000001"}, + {id.Slice{1, 2}, "00000000000001 00000000000002"}, + } + for i, tc := range testcases { + got := tc.in.String() + if got != tc.exp { + t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got) + } + } +} ADDED domain/meta/collection.go Index: domain/meta/collection.go ================================================================== --- domain/meta/collection.go +++ domain/meta/collection.go @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2022 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 meta + +import "sort" + +// Arrangement stores metadata within its categories. +// Typecally a category might be a tag name, a role name, a syntax value. +type Arrangement map[string][]*Meta + +// CreateArrangement by inspecting a given key and use the found +// value as a category. +func CreateArrangement(metaList []*Meta, key string) Arrangement { + if len(metaList) == 0 { + return nil + } + descr := Type(key) + if descr == nil { + return nil + } + a := make(Arrangement) + if descr.IsSet { + for _, m := range metaList { + if vals, ok := m.GetList(key); ok { + for _, val := range vals { + a[val] = append(a[val], m) + } + } + } + } else { + for _, m := range metaList { + if val, ok := m.Get(key); ok && val != "" { + a[val] = append(a[val], m) + } + } + } + return a +} + +// Counted returns the list of categories, together with the number of +// metadata for each category. +func (a Arrangement) Counted() CountedCategories { + if len(a) == 0 { + return nil + } + result := make(CountedCategories, 0, len(a)) + for cat, metas := range a { + result = append(result, CountedCategory{Name: cat, Count: len(metas)}) + } + return result +} + +// CountedCategory contains of a name and the number how much this name occured +// somewhere. +type CountedCategory struct { + Name string + Count int +} + +// CountedCategories is the list of CountedCategories. +// Every name must occur only once. +type CountedCategories []CountedCategory + +// SortByName sorts the list by the name attribute. +// Since each name must occur only once, two CountedCategories cannot have +// the same name. +func (ccs CountedCategories) SortByName() { + sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name }) +} + +// SortByCount sorts the list by the count attribute, descending. +// If two counts are equal, elements are sorted by name. +func (ccs CountedCategories) SortByCount() { + sort.Slice(ccs, func(i, j int) bool { + iCount, jCount := ccs[i].Count, ccs[j].Count + if iCount > jCount { + return true + } + if iCount == jCount { + return ccs[i].Name < ccs[j].Name + } + return false + }) +} + +// Categories returns just the category names. +func (ccs CountedCategories) Categories() []string { + result := make([]string, len(ccs)) + for i, cc := range ccs { + result[i] = cc.Name + } + return result +} ADDED domain/meta/meta.go Index: domain/meta/meta.go ================================================================== --- domain/meta/meta.go +++ domain/meta/meta.go @@ -0,0 +1,420 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta provides the domain specific type 'meta'. +package meta + +import ( + "bytes" + "regexp" + "sort" + "strings" + "unicode" + "unicode/utf8" + + "zettelstore.de/c/api" + "zettelstore.de/c/maps" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/input" + "zettelstore.de/z/strfun" +) + +type keyUsage int + +const ( + _ keyUsage = iota + usageUser // Key will be manipulated by the user + usageComputed // Key is computed by zettelstore + usageProperty // Key is computed and not stored by zettelstore +) + +// DescriptionKey formally describes each supported metadata key. +type DescriptionKey struct { + Name string + Type *DescriptionType + usage keyUsage + Inverse string +} + +// IsComputed returns true, if metadata is computed and not set by the user. +func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed } + +// IsProperty returns true, if metadata is a computed property. +func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty } + +// IsStoredComputed retruns true, if metadata is computed, but also stored. +func (kd *DescriptionKey) IsStoredComputed() bool { return kd.usage == usageComputed } + +var registeredKeys = make(map[string]*DescriptionKey) + +func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) { + if _, ok := registeredKeys[name]; ok { + panic("Key '" + name + "' already defined") + } + if inverse != "" { + if t != TypeID && t != TypeIDSet { + panic("Inversable key '" + name + "' is not identifier type, but " + t.String()) + } + inv, ok := registeredKeys[inverse] + if !ok { + panic("Inverse Key '" + inverse + "' not found") + } + if !inv.IsComputed() { + panic("Inverse Key '" + inverse + "' is not computed.") + } + if inv.Type != TypeIDSet { + panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String()) + } + } + registeredKeys[name] = &DescriptionKey{name, t, usage, inverse} +} + +// IsComputed returns true, if key denotes a computed metadata key. +func IsComputed(name string) bool { + if kd, ok := registeredKeys[name]; ok { + return kd.IsComputed() + } + return false +} + +// IsProperty returns true, if key denotes a property metadata value. +func IsProperty(name string) bool { + if kd, ok := registeredKeys[name]; ok { + return kd.IsProperty() + } + return false +} + +// IsStoredComputed returns true, if key denotes a computed metadata key that is stored. +func IsStoredComputed(name string) bool { + if kd, ok := registeredKeys[name]; ok { + return kd.IsStoredComputed() + } + return false +} + +// Inverse returns the name of the inverse key. +func Inverse(name string) string { + if kd, ok := registeredKeys[name]; ok { + return kd.Inverse + } + return "" +} + +// GetDescription returns the key description object of the given key name. +func GetDescription(name string) DescriptionKey { + if d, ok := registeredKeys[name]; ok { + return *d + } + return DescriptionKey{Type: Type(name)} +} + +// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name. +func GetSortedKeyDescriptions() []*DescriptionKey { + keys := maps.Keys(registeredKeys) + result := make([]*DescriptionKey, 0, len(keys)) + for _, n := range keys { + result = append(result, registeredKeys[n]) + } + return result +} + +// Supported keys. +func init() { + registerKey(api.KeyID, TypeID, usageComputed, "") + registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "") + registerKey(api.KeyRole, TypeWord, usageUser, "") + registerKey(api.KeyTags, TypeTagSet, usageUser, "") + registerKey(api.KeySyntax, TypeWord, usageUser, "") + + // Properties that are inverse keys + registerKey(api.KeyFolge, TypeIDSet, usageProperty, "") + registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "") + + registerKey(api.KeyAuthor, TypeString, usageUser, "") + registerKey(api.KeyBack, TypeIDSet, usageProperty, "") + registerKey(api.KeyBackward, TypeIDSet, usageProperty, "") + registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "") + registerKey(api.KeyCopyright, TypeString, usageUser, "") + registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "") + registerKey(api.KeyCredential, TypeCredential, usageUser, "") + registerKey(api.KeyDead, TypeIDSet, usageProperty, "") + 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) + registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) + registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") + registerKey(api.KeyReadOnly, TypeWord, usageUser, "") + registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "") + registerKey(api.KeyURL, TypeURL, usageUser, "") + registerKey(api.KeyUselessFiles, TypeString, usageProperty, "") + registerKey(api.KeyUserID, TypeWord, usageUser, "") + registerKey(api.KeyUserRole, TypeWord, usageUser, "") + registerKey(api.KeyVisibility, TypeWord, usageUser, "") +} + +// NewPrefix is the prefix for metadata key in template zettel for creating new zettel. +const NewPrefix = "new-" + +// Meta contains all meta-data of a zettel. +type Meta struct { + Zid id.Zid + pairs map[string]string + YamlSep bool +} + +// New creates a new chunk for storing metadata. +func New(zid id.Zid) *Meta { + return &Meta{Zid: zid, pairs: make(map[string]string, 5)} +} + +// NewWithData creates metadata object with given data. +func NewWithData(zid id.Zid, data map[string]string) *Meta { + pairs := make(map[string]string, len(data)) + for k, v := range data { + pairs[k] = v + } + return &Meta{Zid: zid, pairs: pairs} +} + +// Length returns the number of bytes stored for the metadata. +func (m *Meta) Length() int { + if m == nil { + return 0 + } + result := 6 // storage needed for Zid + for k, v := range m.pairs { + result += len(k) + len(v) + 1 // 1 because separator + } + return result +} + +// Clone returns a new copy of the metadata. +func (m *Meta) Clone() *Meta { + return &Meta{ + Zid: m.Zid, + pairs: m.Map(), + YamlSep: m.YamlSep, + } +} + +// Map returns a copy of the meta data as a string map. +func (m *Meta) Map() map[string]string { + pairs := make(map[string]string, len(m.pairs)) + for k, v := range m.pairs { + pairs[k] = v + } + return pairs +} + +var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$") + +// KeyIsValid returns true, the the key is a valid string. +func KeyIsValid(key string) bool { + return reKey.MatchString(key) +} + +// Pair is one key-value-pair of a Zettel meta. +type Pair struct { + Key string + Value string +} + +var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax} +var firstKeySet strfun.Set + +func init() { + firstKeySet = strfun.NewSet(firstKeys...) +} + +// Set stores the given string value under the given key. +func (m *Meta) Set(key, value string) { + if key != api.KeyID { + m.pairs[key] = trimValue(value) + } +} + +// 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) + } +} + +func trimValue(value string) string { + return strings.TrimFunc(value, input.IsSpace) +} + +// Get retrieves the string value of a given key. The bool value signals, +// whether there was a value stored or not. +func (m *Meta) Get(key string) (string, bool) { + if key == api.KeyID { + return m.Zid.String(), true + } + value, ok := m.pairs[key] + return value, ok +} + +// GetDefault retrieves the string value of the given key. If no value was +// stored, the given default value is returned. +func (m *Meta) GetDefault(key, def string) string { + if value, ok := m.Get(key); ok { + return value + } + return def +} + +// GetTitle returns the title of the metadata. It is the only key that has a +// defined default value: the string representation of the zettel identifier. +func (m *Meta) GetTitle() string { + if title, found := m.Get(api.KeyTitle); found { + return title + } + return m.Zid.String() +} + +// Pairs returns not computed key/values pairs stored, in a specific order. +// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, +// MetaContextKey. Then all other pairs are append to the list, ordered by key. +func (m *Meta) Pairs() []Pair { + return m.doPairs(m.getFirstKeys(), notComputedKey) +} + +// ComputedPairs returns all key/values pairs stored, in a specific order. First come +// the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey, +// MetaContextKey. Then all other pairs are append to the list, ordered by key. +func (m *Meta) ComputedPairs() []Pair { + return m.doPairs(m.getFirstKeys(), anyKey) +} + +// PairsRest returns not computed key/values pairs stored, except the values with +// predefined keys. The pairs are ordered by key. +func (m *Meta) PairsRest() []Pair { + result := make([]Pair, 0, len(m.pairs)) + return m.doPairs(result, notComputedKey) +} + +// ComputedPairsRest returns all key/values pairs stored, except the values with +// predefined keys. The pairs are ordered by key. +func (m *Meta) ComputedPairsRest() []Pair { + result := make([]Pair, 0, len(m.pairs)) + return m.doPairs(result, anyKey) +} + +func notComputedKey(key string) bool { return !IsComputed(key) } +func anyKey(string) bool { return true } + +func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair { + keys := m.getKeysRest(addKeyPred) + for _, k := range keys { + firstKeys = append(firstKeys, Pair{k, m.pairs[k]}) + } + return firstKeys +} + +func (m *Meta) getFirstKeys() []Pair { + result := make([]Pair, 0, len(m.pairs)) + for _, key := range firstKeys { + if value, ok := m.pairs[key]; ok { + result = append(result, Pair{key, value}) + } + } + return result +} + +func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string { + keys := make([]string, 0, len(m.pairs)) + for k := range m.pairs { + if !firstKeySet.Has(k) && addKeyPred(k) { + keys = append(keys, k) + } + } + sort.Strings(keys) + return keys +} + +// Delete removes a key from the data. +func (m *Meta) Delete(key string) { + if key != api.KeyID { + delete(m.pairs, key) + } +} + +// Equal compares to metas for equality. +func (m *Meta) Equal(o *Meta, allowComputed bool) bool { + if m == nil && o == nil { + return true + } + if m == nil || o == nil || m.Zid != o.Zid { + return false + } + tested := make(strfun.Set, len(m.pairs)) + for k, v := range m.pairs { + tested.Set(k) + if !equalValue(k, v, o, allowComputed) { + return false + } + } + for k, v := range o.pairs { + if !tested.Has(k) && !equalValue(k, v, m, allowComputed) { + return false + } + } + return true +} + +func equalValue(key, val string, other *Meta, allowComputed bool) bool { + if allowComputed || !IsComputed(key) { + if valO, ok := other.pairs[key]; !ok || val != valO { + return false + } + } + return true +} + +// Sanitize all metadata keys and values, so that they can be written safely into a file. +func (m *Meta) Sanitize() { + if m == nil { + return + } + for k, v := range m.pairs { + m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v) + } +} + +// RemoveNonGraphic changes the given string not to include non-graphical characters. +// It is needed to sanitize meta data. +func RemoveNonGraphic(s string) string { + if s == "" { + return "" + } + pos := 0 + var buf bytes.Buffer + for pos < len(s) { + nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) }) + if nextPos < 0 { + break + } + buf.WriteString(s[pos:nextPos]) + buf.WriteByte(' ') + _, size := utf8.DecodeRuneInString(s[nextPos:]) + pos = nextPos + size + } + if pos == 0 { + return strings.TrimSpace(s) + } + buf.WriteString(s[pos:]) + return strings.TrimSpace(buf.String()) +} ADDED domain/meta/meta_test.go Index: domain/meta/meta_test.go ================================================================== --- domain/meta/meta_test.go +++ domain/meta/meta_test.go @@ -0,0 +1,262 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta provides the domain specific type 'meta'. +package meta + +import ( + "strings" + "testing" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" +) + +const testID = id.Zid(98765432101234) + +func TestKeyIsValid(t *testing.T) { + t.Parallel() + validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)} + for _, key := range validKeys { + if !KeyIsValid(key) { + t.Errorf("Key %q wrongly identified as invalid key", key) + } + } + invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)} + for _, key := range invalidKeys { + if KeyIsValid(key) { + t.Errorf("Key %q wrongly identified as valid key", key) + } + } +} + +func TestTitleHeader(t *testing.T) { + t.Parallel() + m := New(testID) + if got, ok := m.Get(api.KeyTitle); ok && got != "" { + t.Errorf("Title is not empty, but %q", got) + } + addToMeta(m, api.KeyTitle, " ") + if got, ok := m.Get(api.KeyTitle); ok && got != "" { + t.Errorf("Title is not empty, but %q", got) + } + const st = "A simple text" + addToMeta(m, api.KeyTitle, " "+st+" ") + if got, ok := m.Get(api.KeyTitle); !ok || got != st { + t.Errorf("Title is not %q, but %q", st, got) + } + addToMeta(m, api.KeyTitle, " "+st+"\t") + const exp = st + " " + st + if got, ok := m.Get(api.KeyTitle); !ok || got != exp { + t.Errorf("Title is not %q, but %q", exp, got) + } + + m = New(testID) + const at = "A Title" + addToMeta(m, api.KeyTitle, at) + addToMeta(m, api.KeyTitle, " ") + if got, ok := m.Get(api.KeyTitle); !ok || got != at { + t.Errorf("Title is not %q, but %q", at, got) + } +} + +func checkSet(t *testing.T, exp []string, m *Meta, key string) { + t.Helper() + got, _ := m.GetList(key) + for i, tag := range exp { + if i < len(got) { + if tag != got[i] { + t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i]) + } + } else { + t.Errorf("Expected %q, but is missing", exp[i]) + } + } + if len(exp) < len(got) { + t.Errorf("Extra tags: %q", got[len(exp):]) + } +} + +func TestTagsHeader(t *testing.T) { + t.Parallel() + m := New(testID) + checkSet(t, []string{}, m, api.KeyTags) + + addToMeta(m, api.KeyTags, "") + checkSet(t, []string{}, m, api.KeyTags) + + addToMeta(m, api.KeyTags, " #t1 #t2 #t3 #t4 ") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags) + + addToMeta(m, api.KeyTags, "#t5") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) + + addToMeta(m, api.KeyTags, "t6") + checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags) +} + +func TestSyntax(t *testing.T) { + t.Parallel() + m := New(testID) + if got, ok := m.Get(api.KeySyntax); ok || got != "" { + t.Errorf("Syntax is not %q, but %q", "", got) + } + addToMeta(m, api.KeySyntax, " ") + if got, _ := m.Get(api.KeySyntax); got != "" { + t.Errorf("Syntax is not %q, but %q", "", got) + } + addToMeta(m, api.KeySyntax, "MarkDown") + const exp = "markdown" + if got, ok := m.Get(api.KeySyntax); !ok || got != exp { + t.Errorf("Syntax is not %q, but %q", exp, got) + } + addToMeta(m, api.KeySyntax, " ") + if got, _ := m.Get(api.KeySyntax); got != "" { + t.Errorf("Syntax is not %q, but %q", "", got) + } +} + +func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) { + t.Helper() + got := make(map[string]string, len(gotP)) + for _, p := range gotP { + got[p.Key] = p.Value + if _, ok := exp[p.Key]; !ok { + t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value) + } + } + for k, v := range exp { + if gv, ok := got[k]; !ok || v != gv { + if ok { + t.Errorf("Key %q is not %q, but %q", k, v, got[k]) + } else { + t.Errorf("Key %q missing, should have value %q", k, v) + } + } + } +} + +func TestDefaultHeader(t *testing.T) { + t.Parallel() + m := New(testID) + addToMeta(m, "h1", "d1") + addToMeta(m, "H2", "D2") + addToMeta(m, "H1", "D1.1") + exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"} + checkHeader(t, exp, m.Pairs()) + addToMeta(m, "", "d0") + checkHeader(t, exp, m.Pairs()) + addToMeta(m, "h3", "") + exp["h3"] = "" + checkHeader(t, exp, m.Pairs()) + addToMeta(m, "h3", " ") + checkHeader(t, exp, m.Pairs()) + addToMeta(m, "h4", " ") + exp["h4"] = "" + checkHeader(t, exp, m.Pairs()) +} + +func TestDelete(t *testing.T) { + t.Parallel() + m := New(testID) + m.Set("key", "val") + if got, ok := m.Get("key"); !ok || got != "val" { + t.Errorf("Value != %q, got: %v/%q", "val", ok, got) + } + m.Set("key", "") + if got, ok := m.Get("key"); !ok || got != "" { + t.Errorf("Value != %q, got: %v/%q", "", ok, got) + } + m.Delete("key") + if got, ok := m.Get("key"); ok || got != "" { + t.Errorf("Value != %q, got: %v/%q", "", ok, got) + } +} + +func TestEqual(t *testing.T) { + t.Parallel() + testcases := []struct { + pairs1, pairs2 []string + allowComputed bool + exp bool + }{ + {nil, nil, true, true}, + {nil, nil, false, true}, + {[]string{"a", "a"}, nil, false, false}, + {[]string{"a", "a"}, nil, true, false}, + {[]string{api.KeyFolge, "0"}, nil, true, false}, + {[]string{api.KeyFolge, "0"}, nil, false, true}, + {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true}, + {[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true}, + } + for i, tc := range testcases { + m1 := pairs2meta(tc.pairs1) + m2 := pairs2meta(tc.pairs2) + got := m1.Equal(m2, tc.allowComputed) + if tc.exp != got { + t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) + } + got = m2.Equal(m1, tc.allowComputed) + if tc.exp != got { + t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got) + } + } + + // Pathologic cases + var m1, m2 *Meta + if !m1.Equal(m2, true) { + t.Error("Nil metas should be treated equal") + } + m1 = New(testID) + if m1.Equal(m2, true) { + t.Error("Empty meta should not be equal to nil") + } + if m2.Equal(m1, true) { + t.Error("Nil meta should should not be equal to empty") + } + m2 = New(testID + 1) + if m1.Equal(m2, true) { + t.Error("Different ID should differentiate") + } + if m2.Equal(m1, true) { + t.Error("Different ID should differentiate") + } +} + +func pairs2meta(pairs []string) *Meta { + m := New(testID) + for i := 0; i < len(pairs); i = i + 2 { + m.Set(pairs[i], pairs[i+1]) + } + return m +} + +func TestRemoveNonGraphic(t *testing.T) { + testCases := []struct { + inp string + exp string + }{ + {"", ""}, + {" ", ""}, + {"a", "a"}, + {"a ", "a"}, + {"a b", "a b"}, + {"\n", ""}, + {"a\n", "a"}, + {"a\nb", "a b"}, + {"a\tb", "a b"}, + } + for i, tc := range testCases { + got := RemoveNonGraphic(tc.inp) + if tc.exp != got { + t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got) + } + } +} ADDED domain/meta/parse.go Index: domain/meta/parse.go ================================================================== --- domain/meta/parse.go +++ domain/meta/parse.go @@ -0,0 +1,178 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta provides the domain specific type 'meta'. +package meta + +import ( + "strings" + + "zettelstore.de/c/api" + "zettelstore.de/c/maps" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/input" + "zettelstore.de/z/strfun" +) + +// NewFromInput parses the meta data of a zettel. +func NewFromInput(zid id.Zid, inp *input.Input) *Meta { + if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { + skipToEOL(inp) + inp.EatEOL() + } + meta := New(zid) + for { + skipSpace(inp) + switch inp.Ch { + case '\r': + if inp.Peek() == '\n' { + inp.Next() + } + fallthrough + case '\n': + inp.Next() + return meta + case input.EOS: + return meta + case '%': + skipToEOL(inp) + inp.EatEOL() + continue + } + parseHeader(meta, inp) + if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' { + skipToEOL(inp) + inp.EatEOL() + meta.YamlSep = true + return meta + } + } +} + +func parseHeader(m *Meta, inp *input.Input) { + pos := inp.Pos + for isHeader(inp.Ch) { + inp.Next() + } + key := inp.Src[pos:inp.Pos] + skipSpace(inp) + if inp.Ch == ':' { + inp.Next() + } + var val []byte + for { + skipSpace(inp) + pos = inp.Pos + skipToEOL(inp) + val = append(val, inp.Src[pos:inp.Pos]...) + inp.EatEOL() + if !input.IsSpace(inp.Ch) { + break + } + val = append(val, ' ') + } + addToMeta(m, string(key), string(val)) +} + +func skipSpace(inp *input.Input) { + for input.IsSpace(inp.Ch) { + inp.Next() + } +} + +func skipToEOL(inp *input.Input) { + for { + switch inp.Ch { + case '\n', '\r', input.EOS: + return + } + inp.Next() + } +} + +// Return true iff rune is valid for header key. +func isHeader(ch rune) bool { + return ('a' <= ch && ch <= 'z') || + ('0' <= ch && ch <= '9') || + ch == '-' || + ('A' <= ch && ch <= 'Z') +} + +type predValidElem func(string) bool + +func addToSet(set strfun.Set, elems []string, useElem predValidElem) { + for _, s := range elems { + if len(s) > 0 && useElem(s) { + set.Set(s) + } + } +} + +func addSet(m *Meta, key, val string, useElem predValidElem) { + newElems := strings.Fields(val) + oldElems, ok := m.GetList(key) + if !ok { + oldElems = nil + } + + set := make(strfun.Set, len(newElems)+len(oldElems)) + addToSet(set, newElems, useElem) + if len(set) == 0 { + // Nothing to add. Maybe because of rejected elements. + return + } + addToSet(set, oldElems, useElem) + m.SetList(key, maps.Keys(set)) +} + +func addData(m *Meta, k, v string) { + if o, ok := m.Get(k); !ok || o == "" { + m.Set(k, v) + } else if v != "" { + m.Set(k, o+" "+v) + } +} + +func addToMeta(m *Meta, key, val string) { + v := trimValue(val) + key = strings.ToLower(key) + if !KeyIsValid(key) { + return + } + switch key { + case "", api.KeyID: + // Empty key and 'id' key will be ignored + return + } + + switch Type(key) { + case TypeTagSet: + addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 }) + case TypeWord: + m.Set(key, strings.ToLower(v)) + case TypeWordSet: + addSet(m, key, strings.ToLower(v), func(s string) bool { return true }) + case TypeID: + if _, err := id.Parse(v); err == nil { + m.Set(key, v) + } + case TypeIDSet: + addSet(m, key, v, func(s string) bool { + _, err := id.Parse(s) + return err == nil + }) + case TypeTimestamp: + if _, ok := TimeValue(v); ok { + m.Set(key, v) + } + default: + addData(m, key, v) + } +} ADDED domain/meta/parse_test.go Index: domain/meta/parse_test.go ================================================================== --- domain/meta/parse_test.go +++ domain/meta/parse_test.go @@ -0,0 +1,168 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta_test provides tests for the domain specific type 'meta'. +package meta_test + +import ( + "strings" + "testing" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" +) + +func parseMetaStr(src string) *meta.Meta { + return meta.NewFromInput(testID, input.NewInput([]byte(src))) +} + +func TestEmpty(t *testing.T) { + t.Parallel() + m := parseMetaStr("") + if got, ok := m.Get(api.KeySyntax); ok || got != "" { + t.Errorf("Syntax is not %q, but %q", "", got) + } + if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 { + t.Errorf("Tags are not nil, but %v", got) + } +} + +func TestTitle(t *testing.T) { + t.Parallel() + td := []struct{ s, e string }{ + {api.KeyTitle + ": a title", "a title"}, + {api.KeyTitle + ": a\n\t title", "a title"}, + {api.KeyTitle + ": a\n\t title\r\n x", "a title x"}, + {api.KeyTitle + " AbC", "AbC"}, + {api.KeyTitle + " AbC\n ded", "AbC ded"}, + {api.KeyTitle + ": o\ntitle: p", "o p"}, + {api.KeyTitle + ": O\n\ntitle: P", "O"}, + {api.KeyTitle + ": b\r\ntitle: c", "b c"}, + {api.KeyTitle + ": B\r\n\r\ntitle: C", "B"}, + {api.KeyTitle + ": r\rtitle: q", "r q"}, + {api.KeyTitle + ": R\r\rtitle: Q", "R"}, + } + for i, tc := range td { + m := parseMetaStr(tc.s) + if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e { + t.Log(m) + t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got) + } + } +} + +func TestTags(t *testing.T) { + t.Parallel() + testcases := []struct { + src string + exp string + }{ + {"", ""}, + {api.KeyTags + ":", ""}, + {api.KeyTags + ": c", ""}, + {api.KeyTags + ": #", ""}, + {api.KeyTags + ": #c", "c"}, + {api.KeyTags + ": #c #", "c"}, + {api.KeyTags + ": #c #b", "b c"}, + {api.KeyTags + ": #c # #", "c"}, + {api.KeyTags + ": #c # #b", "b c"}, + } + for i, tc := range testcases { + m := parseMetaStr(tc.src) + tags, found := m.GetTags(api.KeyTags) + if !found { + if tc.exp != "" { + t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags) + } + continue + } + if tc.exp == "" && len(tags) > 0 { + t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags) + continue + } + got := strings.Join(tags, " ") + if tc.exp != got { + t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got) + } + } +} + +func TestNewFromInput(t *testing.T) { + t.Parallel() + testcases := []struct { + input string + exp []meta.Pair + }{ + {"", []meta.Pair{}}, + {" a:b", []meta.Pair{{"a", "b"}}}, + {"%a:b", []meta.Pair{}}, + {"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}}, + {"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}}, + {"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}}, + {"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}}, + {"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}}, + {"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}}, + {"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}}, + {"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}}, + } + for i, tc := range testcases { + meta := parseMetaStr(tc.input) + if got := meta.Pairs(); !equalPairs(tc.exp, got) { + t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got) + } + } + + // Test, whether input position is correct. + inp := input.NewInput([]byte("---\na:b\n---\nX")) + m := meta.NewFromInput(testID, inp) + exp := []meta.Pair{{"a", "b"}} + if got := m.Pairs(); !equalPairs(exp, got) { + t.Errorf("Expected=%v, got=%v", exp, got) + } + expCh := 'X' + if gotCh := inp.Ch; gotCh != expCh { + t.Errorf("Expected=%v, got=%v", expCh, gotCh) + } +} + +func equalPairs(one, two []meta.Pair) bool { + if len(one) != len(two) { + return false + } + for i := 0; i < len(one); i++ { + if one[i].Key != two[i].Key || one[i].Value != two[i].Value { + return false + } + } + return true +} + +func TestPrecursorIDSet(t *testing.T) { + t.Parallel() + var testdata = []struct { + inp string + exp string + }{ + {"", ""}, + {"123", ""}, + {"12345678901234", "12345678901234"}, + {"123 12345678901234", "12345678901234"}, + {"12345678901234 123", "12345678901234"}, + {"01234567890123 123 12345678901234", "01234567890123 12345678901234"}, + {"12345678901234 01234567890123", "01234567890123 12345678901234"}, + } + for i, tc := range testdata { + m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp) + if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got { + t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp) + } + } +} ADDED domain/meta/type.go Index: domain/meta/type.go ================================================================== --- domain/meta/type.go +++ domain/meta/type.go @@ -0,0 +1,208 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta provides the domain specific type 'meta'. +package meta + +import ( + "strconv" + "strings" + "sync" + "time" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" +) + +// DescriptionType is a description of a specific key type. +type DescriptionType struct { + Name string + IsSet bool +} + +// String returns the string representation of the given type +func (t DescriptionType) String() string { return t.Name } + +var registeredTypes = make(map[string]*DescriptionType) + +func registerType(name string, isSet bool) *DescriptionType { + if _, ok := registeredTypes[name]; ok { + panic("Type '" + name + "' already registered") + } + t := &DescriptionType{name, isSet} + registeredTypes[name] = t + return t +} + +// Supported key types. +var ( + TypeCredential = registerType(api.MetaCredential, false) + TypeEmpty = registerType(api.MetaEmpty, false) + TypeID = registerType(api.MetaID, false) + TypeIDSet = registerType(api.MetaIDSet, true) + TypeNumber = registerType(api.MetaNumber, false) + TypeString = registerType(api.MetaString, false) + TypeTagSet = registerType(api.MetaTagSet, true) + TypeTimestamp = registerType(api.MetaTimestamp, false) + TypeURL = registerType(api.MetaURL, false) + TypeWord = registerType(api.MetaWord, false) + TypeWordSet = registerType(api.MetaWordSet, true) + TypeZettelmarkup = registerType(api.MetaZettelmarkup, false) +) + +// Type returns a type hint for the given key. If no type hint is specified, +// TypeUnknown is returned. +func (*Meta) Type(key string) *DescriptionType { + return Type(key) +} + +var ( + cachedTypedKeys = make(map[string]*DescriptionType) + mxTypedKey sync.RWMutex + suffixTypes = map[string]*DescriptionType{ + "-number": TypeNumber, + "-role": TypeWord, + "-set": TypeWordSet, + "-title": TypeZettelmarkup, + "-url": TypeURL, + "-zettel": TypeID, + "-zid": TypeID, + "-zids": TypeIDSet, + } +) + +// Type returns a type hint for the given key. If no type hint is specified, +// TypeEmpty is returned. +func Type(key string) *DescriptionType { + if k, ok := registeredKeys[key]; ok { + return k.Type + } + mxTypedKey.RLock() + k, ok := cachedTypedKeys[key] + mxTypedKey.RUnlock() + if ok { + return k + } + for suffix, t := range suffixTypes { + if strings.HasSuffix(key, suffix) { + mxTypedKey.Lock() + defer mxTypedKey.Unlock() + cachedTypedKeys[key] = t + return t + } + } + return TypeEmpty +} + +// SetList stores the given string list value under the given key. +func (m *Meta) SetList(key string, values []string) { + if key != api.KeyID { + for i, val := range values { + values[i] = trimValue(val) + } + m.pairs[key] = strings.Join(values, " ") + } +} + +// SetNow stores the current timestamp under the given key. +func (m *Meta) SetNow(key string) { + m.Set(key, time.Now().Local().Format(id.ZidLayout)) +} + +// BoolValue returns the value interpreted as a bool. +func BoolValue(value string) bool { + if len(value) > 0 { + switch value[0] { + case '0', 'f', 'F', 'n', 'N': + return false + } + } + return true +} + +// GetBool returns the boolean value of the given key. +func (m *Meta) GetBool(key string) bool { + if value, ok := m.Get(key); ok { + return BoolValue(value) + } + return false +} + +// TimeValue returns the time value of the given value. +func TimeValue(value string) (time.Time, bool) { + if t, err := time.Parse(id.ZidLayout, value); err == nil { + return t, true + } + return time.Time{}, false +} + +// GetTime returns the time value of the given key. +func (m *Meta) GetTime(key string) (time.Time, bool) { + if value, ok := m.Get(key); ok { + return TimeValue(value) + } + return time.Time{}, false +} + +// ListFromValue transforms a string value into a list value. +func ListFromValue(value string) []string { + return strings.Fields(value) +} + +// GetList retrieves the string list value of a given key. The bool value +// signals, whether there was a value stored or not. +func (m *Meta) GetList(key string) ([]string, bool) { + value, ok := m.Get(key) + if !ok { + return nil, false + } + return ListFromValue(value), true +} + +// GetTags returns the list of tags as a string list. Each tag does not begin +// with the '#' character, in contrast to `GetList`. +func (m *Meta) GetTags(key string) ([]string, bool) { + tagsValue, ok := m.Get(key) + if !ok { + return nil, false + } + tags := ListFromValue(strings.ToLower(tagsValue)) + for i, tag := range tags { + tags[i] = CleanTag(tag) + } + return tags, len(tags) > 0 +} + +// CleanTag removes the number character ('#') from a tag value and lowercases it. +func CleanTag(tag string) string { + if len(tag) > 1 && tag[0] == '#' { + return tag[1:] + } + return tag +} + +// GetListOrNil retrieves the string list value of a given key. If there was +// nothing stores, a nil list is returned. +func (m *Meta) GetListOrNil(key string) []string { + if value, ok := m.GetList(key); ok { + return value + } + return nil +} + +// GetNumber retrieves the numeric value of a given key. +func (m *Meta) GetNumber(key string, def int64) int64 { + if value, ok := m.Get(key); ok { + if num, err := strconv.ParseInt(value, 10, 64); err == nil { + return num + } + } + return def +} ADDED domain/meta/type_test.go Index: domain/meta/type_test.go ================================================================== --- domain/meta/type_test.go +++ domain/meta/type_test.go @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2021 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 meta_test provides tests for the domain specific type 'meta'. +package meta_test + +import ( + "strconv" + "testing" + "time" + + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +func TestNow(t *testing.T) { + t.Parallel() + m := meta.New(id.Invalid) + m.SetNow("key") + val, ok := m.Get("key") + if !ok { + t.Error("Unable to get value of key") + } + if len(val) != 14 { + t.Errorf("Value is not 14 digits long: %q", val) + } + if _, err := strconv.ParseInt(val, 10, 64); err != nil { + t.Errorf("Unable to parse %q as an int64: %v", val, err) + } + if _, ok = m.GetTime("key"); !ok { + t.Errorf("Unable to get time from value %q", val) + } +} + +func TestGetTime(t *testing.T) { + t.Parallel() + testCases := []struct { + value string + valid bool + exp time.Time + }{ + {"", false, time.Time{}}, + {"1", false, time.Time{}}, + {"00000000000000", false, time.Time{}}, + {"98765432109876", false, time.Time{}}, + {"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)}, + } + for i, tc := range testCases { + got, ok := meta.TimeValue(tc.value) + if ok != tc.valid { + t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok) + continue + } + if got != tc.exp { + t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got) + } + } +} ADDED domain/meta/values.go Index: domain/meta/values.go ================================================================== --- domain/meta/values.go +++ domain/meta/values.go @@ -0,0 +1,89 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta + +import ( + "fmt" + + "zettelstore.de/c/api" +) + +// Visibility enumerates the variations of the 'visibility' meta key. +type Visibility int + +// Supported values for visibility. +const ( + _ Visibility = iota + VisibilityUnknown + VisibilityPublic + VisibilityCreator + VisibilityLogin + VisibilityOwner + VisibilityExpert +) + +var visMap = map[string]Visibility{ + api.ValueVisibilityPublic: VisibilityPublic, + api.ValueVisibilityCreator: VisibilityCreator, + api.ValueVisibilityLogin: VisibilityLogin, + api.ValueVisibilityOwner: VisibilityOwner, + api.ValueVisibilityExpert: VisibilityExpert, +} +var revVisMap = map[Visibility]string{} + +func init() { + for k, v := range visMap { + revVisMap[v] = k + } +} + +// GetVisibility returns the visibility value of the given string +func GetVisibility(val string) Visibility { + if vis, ok := visMap[val]; ok { + return vis + } + return VisibilityUnknown +} + +func (v Visibility) String() string { + if s, ok := revVisMap[v]; ok { + return s + } + return fmt.Sprintf("Unknown (%d)", v) +} + +// UserRole enumerates the supported values of meta key 'user-role'. +type UserRole int + +// Supported values for user roles. +const ( + _ UserRole = iota + UserRoleUnknown + UserRoleCreator + UserRoleReader + UserRoleWriter + UserRoleOwner +) + +var urMap = map[string]UserRole{ + api.ValueUserRoleCreator: UserRoleCreator, + api.ValueUserRoleReader: UserRoleReader, + api.ValueUserRoleWriter: UserRoleWriter, + api.ValueUserRoleOwner: UserRoleOwner, +} + +// GetUserRole role returns the user role of the given string. +func GetUserRole(val string) UserRole { + if ur, ok := urMap[val]; ok { + return ur + } + return UserRoleUnknown +} ADDED domain/meta/write.go Index: domain/meta/write.go ================================================================== --- domain/meta/write.go +++ domain/meta/write.go @@ -0,0 +1,58 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta provides the domain specific type 'meta'. +package meta + +import "io" + +// Write writes metadata to a writer, excluding computed and propery values. +func (m *Meta) Write(w io.Writer) (int, error) { + return m.doWrite(w, IsComputed) +} + +// WriteComputed writes metadata to a writer, including computed values, +// but excluding property values. +func (m *Meta) WriteComputed(w io.Writer) (int, error) { + return m.doWrite(w, IsProperty) +} + +func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) { + for _, p := range m.ComputedPairs() { + key := p.Key + if ignoreKeyPred(key) { + continue + } + if err != nil { + break + } + var l int + l, err = io.WriteString(w, key) + length += l + if err == nil { + l, err = w.Write(colonSpace) + length += l + } + if err == nil { + l, err = io.WriteString(w, p.Value) + length += l + } + if err == nil { + l, err = w.Write(newline) + length += l + } + } + return length, err +} + +var ( + colonSpace = []byte{':', ' '} + newline = []byte{'\n'} +) ADDED domain/meta/write_test.go Index: domain/meta/write_test.go ================================================================== --- domain/meta/write_test.go +++ domain/meta/write_test.go @@ -0,0 +1,59 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 meta_test provides tests for the domain specific type 'meta'. +package meta_test + +import ( + "bytes" + "strings" + "testing" + + "zettelstore.de/c/api" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" +) + +const testID = id.Zid(98765432101234) + +func newMeta(title string, tags []string, syntax string) *meta.Meta { + m := meta.New(testID) + if title != "" { + m.Set(api.KeyTitle, title) + } + if tags != nil { + m.Set(api.KeyTags, strings.Join(tags, " ")) + } + if syntax != "" { + m.Set(api.KeySyntax, syntax) + } + return m +} +func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) { + t.Helper() + var buf bytes.Buffer + m.Write(&buf) + if got := buf.String(); got != expected { + t.Errorf("\nExp: %q\ngot: %q", expected, got) + } +} + +func TestWriteMeta(t *testing.T) { + t.Parallel() + assertWriteMeta(t, newMeta("", nil, ""), "") + + m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax") + assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n") + + m = newMeta("TITLE", nil, "") + m.Set("user", "zettel") + m.Set("auth", "basic") + assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n") +} ADDED domain/zettel.go Index: domain/zettel.go ================================================================== --- domain/zettel.go +++ domain/zettel.go @@ -0,0 +1,29 @@ +//----------------------------------------------------------------------------- +// Copyright (c) 2020-2022 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 domain provides domain specific types, constants, and functions. +package domain + +import "zettelstore.de/z/domain/meta" + +// Zettel is the main data object of a zettelstore. +type Zettel struct { + Meta *meta.Meta // Some additional meta-data. + Content Content // The content of the zettel itself. +} + +// Length returns the number of bytes to store the zettel (in a domain view, +// not in a technical view). +func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() } + +// Equal compares two zettel for equality. +func (z Zettel) Equal(o Zettel, allowComputed bool) bool { + return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content) +} Index: encoder/encoder.go ================================================================== --- encoder/encoder.go +++ encoder/encoder.go @@ -1,16 +1,13 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2020-present Detlef Stern +// Copyright (c) 2020-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2020-present Detlef Stern //----------------------------------------------------------------------------- // Package encoder provides a generic interface to encode the abstract syntax // tree into some text form. package encoder @@ -18,13 +15,13 @@ import ( "errors" "fmt" "io" - "t73f.de/r/zsc/api" + "zettelstore.de/c/api" "zettelstore.de/z/ast" - "zettelstore.de/z/zettel/meta" + "zettelstore.de/z/domain/meta" ) // Encoder is an interface that allows to encode different parts of a zettel. type Encoder interface { WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error) @@ -46,24 +43,19 @@ ErrNoWriteBlocks = errors.New("method WriteBlocks is not implemented") ErrNoWriteInlines = errors.New("method WriteInlines is not implemented") ) // Create builds a new encoder with the given options. -func Create(enc api.EncodingEnum, params *CreateParameter) Encoder { +func Create(enc api.EncodingEnum) Encoder { if create, ok := registry[enc]; ok { - return create(params) + return create() } return nil } // CreateFunc produces a new encoder. -type CreateFunc func(*CreateParameter) Encoder - -// CreateParameter contains values that are needed to create an encoder. -type CreateParameter struct { - Lang string // default language -} +type CreateFunc func() Encoder var registry = map[api.EncodingEnum]CreateFunc{} // Register the encoder for later retrieval. func Register(enc api.EncodingEnum, create CreateFunc) { @@ -79,5 +71,13 @@ for enc := range registry { result = append(result, enc) } return result } + +// GetDefaultEncoding returns the encoding that should be used as default. +func GetDefaultEncoding() api.EncodingEnum { + if _, ok := registry[api.EncoderZJSON]; ok { + return api.EncoderZJSON + } + panic("No ZJSON encoding registered") +} Index: encoder/encoder_blob_test.go ================================================================== --- encoder/encoder_blob_test.go +++ encoder/encoder_blob_test.go @@ -1,29 +1,26 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test import ( "testing" - "t73f.de/r/zsc/api" - "t73f.de/r/zsc/input" + "zettelstore.de/c/api" "zettelstore.de/z/config" + "zettelstore.de/z/domain/id" + "zettelstore.de/z/domain/meta" + "zettelstore.de/z/input" "zettelstore.de/z/parser" - "zettelstore.de/z/zettel/id" - "zettelstore.de/z/zettel/meta" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. ) type blobTestCase struct { @@ -41,15 +38,15 @@ 0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00, 0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, }, expect: expectMap{ - encoderHTML: `

PNG

`, - encoderSz: `(BLOCK (BLOB ((TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, - encoderSHTML: `((p (img (@ (alt . "PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`, + encoderZJSON: `[{"":"BLOB","q":"PNG","s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`, + encoderHTML: `

`, + encoderSexpr: `((BLOB "PNG" "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`, encoderText: "", - encoderZmk: `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`, + encoderZmk: `%% Unable to display BLOB with title 'PNG' and syntax 'png'.`, }, }, } func TestBlob(t *testing.T) { Index: encoder/encoder_block_test.go ================================================================== --- encoder/encoder_block_test.go +++ encoder/encoder_block_test.go @@ -1,197 +1,157 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsBlock = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing", zmk: "", expect: expectMap{ + encoderZJSON: `[]`, encoderHTML: "", - encoderMD: "", - encoderSz: `(BLOCK)`, - encoderSHTML: `()`, + encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world", zmk: "Hello, world", expect: expectMap{ + encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]}]`, encoderHTML: "

Hello, world

", - encoderMD: "Hello, world", - encoderSz: `(BLOCK (PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`, - encoderSHTML: `((p "Hello," " " "world"))`, + encoderSexpr: `((PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { descr: "Simple block comment", zmk: "%%%\nNo\nrender\n%%%", expect: expectMap{ + encoderZJSON: `[{"":"CommentBlock","s":"No\nrender"}]`, encoderHTML: ``, - encoderMD: "", - encoderSz: `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`, - encoderSHTML: `(())`, + encoderSexpr: `((VERBATIM-COMMENT () "No\nrender"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Rendered block comment", zmk: "%%%{-}\nRender\n%%%", expect: expectMap{ - encoderHTML: "\n", - encoderMD: "", - encoderSz: `(BLOCK (VERBATIM-COMMENT (("-" . "")) "Render"))`, - encoderSHTML: "((@@@ \"Render\"))", + encoderZJSON: `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`, + encoderHTML: "", + encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "Render"))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "Simple Heading", - zmk: `=== Top Job`, + zmk: `=== Top`, expect: expectMap{ - encoderHTML: "

Top Job

", - encoderMD: "# Top Job", - encoderSz: `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top") (SPACE) (TEXT "Job")))`, - encoderSHTML: `((h2 (@ (id . "top-job")) "Top" " " "Job"))`, - encoderText: `Top Job`, + encoderZJSON: `[{"":"Heading","n":1,"s":"top","i":[{"":"Text","s":"Top"}]}]`, + encoderHTML: "

Top

", + encoderSexpr: `((HEADING 1 () "top" "top" (TEXT "Top")))`, + encoderText: `Top`, encoderZmk: useZmk, }, }, { descr: "Simple List", zmk: "* A\n* B\n* C", expect: expectMap{ + encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"A"}]}],[{"":"Para","i":[{"":"Text","s":"B"}]}],[{"":"Para","i":[{"":"Text","s":"C"}]}]]}]`, encoderHTML: "
  • A
  • B
  • C
", - encoderMD: "* A\n* B\n* C", - encoderSz: `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`, - encoderSHTML: `((ul (li "A") (li "B") (li "C")))`, + encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((TEXT "C"))))`, encoderText: "A\nB\nC", encoderZmk: useZmk, }, }, { descr: "Nested List", zmk: "* T1\n** T2\n* T3\n** T4\n** T5\n* T6", expect: expectMap{ + encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T1"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T2"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T3"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T4"}]}],[{"":"Para","i":[{"":"Text","s":"T5"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T6"}]}]]}]`, encoderHTML: `
  • T1

    • T2
  • T3

    • T4
    • T5
  • T6

`, - encoderMD: "* T1\n * T2\n* T3\n * T4\n * T5\n* T6", - encoderSz: `(BLOCK (UNORDERED (BLOCK (PARA (TEXT "T1")) (UNORDERED (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`, - encoderSHTML: `((ul (li (p "T1") (ul (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`, + encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((PARA (TEXT "T6")))))`, encoderText: "T1\nT2\nT3\nT4\nT5\nT6", encoderZmk: useZmk, }, }, { descr: "Sequence of two lists", zmk: "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2", expect: expectMap{ + encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"Item1.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.2"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.3"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.2"}]}]]}]`, encoderHTML: "
  • Item1.1
  • Item1.2
  • Item1.3
  • Item2.1
  • Item2.2
", - encoderMD: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", - encoderSz: `(BLOCK (UNORDERED (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`, - encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`, + encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((TEXT "Item2.2"))))`, encoderText: "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2", encoderZmk: "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2", }, }, { descr: "Simple horizontal rule", zmk: `---`, expect: expectMap{ + encoderZJSON: `[{"":"Thematic"}]`, encoderHTML: "
", - encoderMD: "---", - encoderSz: `(BLOCK (THEMATIC ()))`, - encoderSHTML: `((hr))`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Thematic break with attribute", - zmk: `---{lang="zmk"}`, - expect: expectMap{ - encoderHTML: `
`, - encoderMD: "---", - encoderSz: `(BLOCK (THEMATIC (("lang" . "zmk"))))`, - encoderSHTML: `((hr (@ (lang . "zmk"))))`, + encoderSexpr: `((THEMATIC ()))`, encoderText: ``, encoderZmk: useZmk, }, }, { descr: "No list after paragraph", zmk: "Text\n*abc", expect: expectMap{ + encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Soft"},{"":"Text","s":"*abc"}]}]`, encoderHTML: "

Text *abc

", - encoderMD: "Text\n*abc", - encoderSz: `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`, - encoderSHTML: `((p "Text" " " "*abc"))`, + encoderSexpr: `((PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`, encoderText: `Text *abc`, encoderZmk: useZmk, }, }, { descr: "A list after paragraph", zmk: "Text\n# abc", expect: expectMap{ + encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"}]},{"":"Ordered","c":[[{"":"Para","i":[{"":"Text","s":"abc"}]}]]}]`, encoderHTML: "

Text

  1. abc
", - encoderMD: "Text\n\n1. abc", - encoderSz: `(BLOCK (PARA (TEXT "Text")) (ORDERED (INLINE (TEXT "abc"))))`, - encoderSHTML: `((p "Text") (ol (li "abc")))`, + encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((TEXT "abc"))))`, encoderText: "Text\nabc", encoderZmk: useZmk, }, }, { - descr: "Simple List Quote", - zmk: "> ToBeOrNotToBe", - expect: expectMap{ - encoderHTML: "
ToBeOrNotToBe
", - encoderMD: "> ToBeOrNotToBe", - encoderSz: `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`, - encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`, - encoderText: "ToBeOrNotToBe", - encoderZmk: useZmk, - }, - }, - { descr: "Simple Quote Block", - zmk: "<<<\nToBeOrNotToBe\n<<< Romeo Julia", + zmk: "<<<\nToBeOrNotToBe\n<<< Romeo", expect: expectMap{ - encoderHTML: "

ToBeOrNotToBe

Romeo Julia
", - encoderMD: "> ToBeOrNotToBe", - encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo") (SPACE) (TEXT "Julia")))`, - encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo" " " "Julia")))`, - encoderText: "ToBeOrNotToBe\nRomeo Julia", + encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`, + encoderHTML: "

ToBeOrNotToBe

Romeo
", + encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((TEXT "Romeo"))))`, + encoderText: "ToBeOrNotToBe\nRomeo", encoderZmk: useZmk, }, }, { descr: "Quote Block with multiple paragraphs", zmk: "<<<\nToBeOr\n\nNotToBe\n<<< Romeo", expect: expectMap{ + encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOr"}]},{"":"Para","i":[{"":"Text","s":"NotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`, encoderHTML: "

ToBeOr

NotToBe

Romeo
", - encoderMD: "> ToBeOr\n\n> NotToBe", - encoderSz: `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`, - encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`, + encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((TEXT "Romeo"))))`, encoderText: "ToBeOr\nNotToBe\nRomeo", encoderZmk: useZmk, }, }, { @@ -204,14 +164,13 @@ Paragraph Spacy Para """ Author`, expect: expectMap{ + encoderZJSON: "[{\"\":\"Poem\",\"b\":[{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"A\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"another\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Text\",\"s\":\"Back\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"Paragraph\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Space\",\"s\":\"\u00a0\u00a0\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Spacy\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Para\"}]}],\"i\":[{\"\":\"Text\",\"s\":\"Author\"}]}]", encoderHTML: "

A\u00a0line
\u00a0\u00a0another\u00a0line
Back

Paragraph

\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para

Author
", - encoderMD: "", - encoderSz: "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) (TEXT \"Author\")))", - encoderSHTML: "((div (p \"A\" \"\u00a0\" \"line\" (br) \"\u00a0\u00a0\" \"another\" \"\u00a0\" \"line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0\" \"Spacy\" \"\u00a0\u00a0\" \"Para\") (cite \"Author\")))", + encoderSexpr: "((REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) ((TEXT \"Author\"))))", encoderText: "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor", encoderZmk: "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author", }, }, { @@ -220,110 +179,79 @@ A simple span and much more :::`, expect: expectMap{ + encoderZJSON: `[{"":"Block","b":[{"":"Para","i":[{"":"Text","s":"A"},{"":"Space"},{"":"Text","s":"simple"},{"":"Soft"},{"":"Space"},{"":"Text","s":"span"},{"":"Soft"},{"":"Text","s":"and"},{"":"Space"},{"":"Text","s":"much"},{"":"Space"},{"":"Text","s":"more"}]}]}]`, encoderHTML: "

A simple span and much more

", - encoderMD: "", - encoderSz: `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more")))))`, - encoderSHTML: `((div (p "A" " " "simple" " " " " "span" " " "and" " " "much" " " "more")))`, + encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`, encoderText: `A simple span and much more`, encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code", zmk: "```\nHello\nWorld\n```", expect: expectMap{ + encoderZJSON: `[{"":"CodeBlock","s":"Hello\nWorld"}]`, encoderHTML: "
Hello\nWorld
", - encoderMD: " Hello\n World", - encoderSz: `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`, - encoderSHTML: `((pre (code "Hello\nWorld")))`, + encoderSexpr: `((VERBATIM-CODE () "Hello\nWorld"))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Code with visible spaces", zmk: "```{-}\nHello World\n```", expect: expectMap{ + encoderZJSON: `[{"":"CodeBlock","a":{"-":""},"s":"Hello World"}]`, encoderHTML: "
Hello\u2423World
", - encoderMD: " Hello World", - encoderSz: `(BLOCK (VERBATIM-CODE (("-" . "")) "Hello World"))`, - encoderSHTML: "((pre (code \"Hello\u2423World\")))", + encoderSexpr: `((VERBATIM-CODE (("-" "")) "Hello World"))`, encoderText: "Hello World", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Eval", zmk: "~~~\nHello\nWorld\n~~~", expect: expectMap{ + encoderZJSON: `[{"":"EvalBlock","s":"Hello\nWorld"}]`, encoderHTML: "
Hello\nWorld
", - encoderMD: "", - encoderSz: `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`, - encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))", + encoderSexpr: `((VERBATIM-EVAL () "Hello\nWorld"))`, encoderText: "Hello\nWorld", encoderZmk: useZmk, }, }, { descr: "Simple Verbatim Math", zmk: "$$$\nHello\n\\LaTeX\n$$$", expect: expectMap{ + encoderZJSON: `[{"":"MathBlock","s":"Hello\n\\LaTeX"}]`, encoderHTML: "
Hello\n\\LaTeX
", - encoderMD: "", - encoderSz: `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`, - encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))", + encoderSexpr: `((VERBATIM-MATH () "Hello\n\\LaTeX"))`, encoderText: "Hello\n\\LaTeX", encoderZmk: useZmk, }, }, { descr: "Simple Description List", zmk: "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box", expect: expectMap{ - encoderHTML: "
Zettel

Paper

Note

Zettelkasten

Slip box

", - encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, - encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, - encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", - encoderZmk: useZmk, - }, - }, - { - descr: "Description List with paragraphs as item", - zmk: "; Zettel\n: Paper\n\n Note\n; Zettelkasten\n: Slip box", - expect: expectMap{ - encoderHTML: "
Zettel

Paper

Note

Zettelkasten

Slip box

", - encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip") (SPACE) (TEXT "box"))))))`, - encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip" " " "box"))))`, - encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", - encoderZmk: useZmk, - }, - }, - { - descr: "Description List with keys, but no descriptions", - zmk: "; K1\n: D11\n: D12\n; K2\n; K3\n: D31", - expect: expectMap{ - encoderHTML: "
K1

D11

D12

K2
K3

D31

", - encoderMD: "", - encoderSz: `(BLOCK (DESCRIPTION ((TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) ((TEXT "K2")) (BLOCK) ((TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`, - encoderSHTML: `((dl (dt "K1") (dd (p "D11")) (dd (p "D12")) (dt "K2") (dt "K3") (dd (p "D31"))))`, - encoderText: "K1\nD11\nD12\nK2\nK3\nD31", + encoderZJSON: `[{"":"Description","d":[{"i":[{"":"Text","s":"Zettel"}],"e":[[{"":"Para","i":[{"":"Text","s":"Paper"}]}],[{"":"Para","i":[{"":"Text","s":"Note"}]}]]},{"i":[{"":"Text","s":"Zettelkasten"}],"e":[[{"":"Para","i":[{"":"Text","s":"Slip"},{"":"Space"},{"":"Text","s":"box"}]}]]}]}]`, + encoderHTML: "
Zettel
Paper
Note
Zettelkasten
Slip box
", + encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((TEXT "Slip") (SPACE) (TEXT "box")))))`, + encoderText: "Zettel\nPaper\nNote\nZettelkasten\nSlip box", encoderZmk: useZmk, }, }, { descr: "Simple Table", zmk: "|c1|c2|c3\n|d1||d3", expect: expectMap{ + encoderZJSON: `[{"":"Table","p":[[],[[{"i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"i":[{"":"Text","s":"c3"}]}],[{"i":[{"":"Text","s":"d1"}]},{"i":[]},{"i":[{"":"Text","s":"d3"}]}]]]}]`, encoderHTML: `
c1c2c3
d1d3
`, - encoderMD: "", - encoderSz: `(BLOCK (TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, - encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`, + encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`, encoderText: "c1 c2 c3\nd1 d3", encoderZmk: useZmk, }, }, { @@ -331,78 +259,52 @@ zmk: `|h1>|=h2|h3:| |%--+---+---+ |","i":[{"":"Text","s":"h1"}]},{"i":[{"":"Text","s":"h2"}]},{"s":":","i":[{"":"Text","s":"h3"}]}],[[{"s":"<","i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"s":":","i":[{"":"Text","s":"c3"}]}],[{"s":">","i":[{"":"Text","s":"f1"}]},{"i":[{"":"Text","s":"f2"}]},{"s":":","i":[{"":"Text","s":"=f3"}]}]]]}]`, encoderHTML: `
h1h2h3
c1c2c3
f1f2=f3
`, - encoderMD: "", - encoderSz: `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, - encoderSHTML: `((table (thead (tr (td (@ (class . "right")) "h1") (td "h2") (td (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`, + encoderSexpr: `((TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`, encoderText: "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3", encoderZmk: `|=h1>|=h2|=h3: |Text1

  1. Endnote \u21a9\ufe0e
", - encoderMD: "Text", - encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`, - encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", - encoderText: "Text Endnote", - encoderZmk: useZmk, - }, - }, - { - descr: "Nested Endnotes", - zmk: `Text[^Endnote[^Nested]]`, - expect: expectMap{ - encoderHTML: "

Text1

  1. Endnote2 \u21a9\ufe0e
  2. Nested \u21a9\ufe0e
", - encoderMD: "Text", - encoderSz: `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`, - encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))", - encoderText: "Text Endnote Nested", + descr: "Simple Endnotes", + zmk: `Text[^Footnote]`, + expect: expectMap{ + encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`, + encoderHTML: `

Text1

  1. Footnote ↩︎
`, + encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`, + encoderText: "Text Footnote", encoderZmk: useZmk, }, }, { descr: "Transclusion", zmk: `{{{http://example.com/image}}}{width="100px"}`, expect: expectMap{ + encoderZJSON: `[{"":"Transclude","a":{"width":"100px"},"q":"external","s":"http://example.com/image"}]`, encoderHTML: `

`, - encoderMD: "", - encoderSz: `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`, - encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`, + encoderSexpr: `((TRANSCLUDE (("width" "100px")) (EXTERNAL "http://example.com/image")))`, encoderText: "", encoderZmk: useZmk, }, }, { - descr: "A paragraph with a inline comment only should be empty in HTML", - zmk: `%% Comment`, - expect: expectMap{ - // encoderHTML: ``, - encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`, - // encoderSHTML: ``, - encoderText: "", - encoderZmk: useZmk, - }, - }, - { descr: "", zmk: ``, expect: expectMap{ + encoderZJSON: `[]`, encoderHTML: ``, - encoderSz: `(BLOCK)`, - encoderSHTML: `()`, + encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, } // func TestEncoderBlock(t *testing.T) { // executeTestCases(t, tcsBlock) // } Index: encoder/encoder_inline_test.go ================================================================== --- encoder/encoder_inline_test.go +++ encoder/encoder_inline_test.go @@ -1,676 +1,509 @@ //----------------------------------------------------------------------------- -// Copyright (c) 2021-present Detlef Stern +// Copyright (c) 2021-2022 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. -// -// SPDX-License-Identifier: EUPL-1.2 -// SPDX-FileCopyrightText: 2021-present Detlef Stern //----------------------------------------------------------------------------- package encoder_test var tcsInline = []zmkTestCase{ { descr: "Empty Zettelmarkup should produce near nothing (inline)", zmk: "", expect: expectMap{ + encoderZJSON: `[]`, encoderHTML: "", - encoderMD: "", - encoderSz: `(INLINE)`, - encoderSHTML: `()`, + encoderSexpr: `()`, encoderText: "", encoderZmk: useZmk, }, }, { descr: "Simple text: Hello, world (inline)", zmk: `Hello, world`, expect: expectMap{ + encoderZJSON: `[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]`, encoderHTML: "Hello, world", - encoderMD: "Hello, world", - encoderSz: `(INLINE (TEXT "Hello,") (SPACE) (TEXT "world"))`, - encoderSHTML: `("Hello," " " "world")`, + encoderSexpr: `((TEXT "Hello,") (SPACE) (TEXT "world"))`, encoderText: "Hello, world", encoderZmk: useZmk, }, }, { - descr: "Soft Break", - zmk: "soft\nbreak", - expect: expectMap{ - encoderHTML: "soft break", - encoderMD: "soft\nbreak", - encoderSz: `(INLINE (TEXT "soft") (SOFT) (TEXT "break"))`, - encoderSHTML: `("soft" " " "break")`, - encoderText: "soft break", - encoderZmk: useZmk, - }, - }, - { - descr: "Hard Break", - zmk: "hard\\\nbreak", - expect: expectMap{ - encoderHTML: "hard
break", - encoderMD: "hard\\\nbreak", - encoderSz: `(INLINE (TEXT "hard") (HARD) (TEXT "break"))`, - encoderSHTML: `("hard" (br) "break")`, - encoderText: "hard\nbreak", - encoderZmk: useZmk, - }, - }, - { descr: "Emphasized formatting", zmk: "__emph__", expect: expectMap{ + encoderZJSON: `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`, encoderHTML: "emph", - encoderMD: "*emph*", - encoderSz: `(INLINE (FORMAT-EMPH () (TEXT "emph")))`, - encoderSHTML: `((em "emph"))`, + encoderSexpr: `((FORMAT-EMPH () (TEXT "emph")))`, encoderText: "emph", encoderZmk: useZmk, }, }, { descr: "Strong formatting", zmk: "**strong**", expect: expectMap{ + encoderZJSON: `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`, encoderHTML: "strong", - encoderMD: "__strong__", - encoderSz: `(INLINE (FORMAT-STRONG () (TEXT "strong")))`, - encoderSHTML: `((strong "strong"))`, + encoderSexpr: `((FORMAT-STRONG () (TEXT "strong")))`, encoderText: "strong", encoderZmk: useZmk, }, }, { descr: "Insert formatting", zmk: ">>insert>>", expect: expectMap{ + encoderZJSON: `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`, encoderHTML: "insert", - encoderMD: "insert", - encoderSz: `(INLINE (FORMAT-INSERT () (TEXT "insert")))`, - encoderSHTML: `((ins "insert"))`, + encoderSexpr: `((FORMAT-INSERT () (TEXT "insert")))`, encoderText: "insert", encoderZmk: useZmk, }, }, { descr: "Delete formatting", zmk: "~~delete~~", expect: expectMap{ + encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`, encoderHTML: "delete", - encoderMD: "delete", - encoderSz: `(INLINE (FORMAT-DELETE () (TEXT "delete")))`, - encoderSHTML: `((del "delete"))`, + encoderSexpr: `((FORMAT-DELETE () (TEXT "delete")))`, encoderText: "delete", encoderZmk: useZmk, }, }, { descr: "Update formatting", zmk: "~~old~~>>new>>", expect: expectMap{ + encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"old"}]},{"":"Insert","i":[{"":"Text","s":"new"}]}]`, encoderHTML: "oldnew", - encoderMD: "oldnew", - encoderSz: `(INLINE (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`, - encoderSHTML: `((del "old") (ins "new"))`, + encoderSexpr: `((FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`, encoderText: "oldnew", encoderZmk: useZmk, }, }, { descr: "Superscript formatting", zmk: "^^superscript^^", expect: expectMap{ + encoderZJSON: `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`, encoderHTML: `superscript`, - encoderMD: "superscript", - encoderSz: `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`, - encoderSHTML: `((sup "superscript"))`, + encoderSexpr: `((FORMAT-SUPER () (TEXT "superscript")))`, encoderText: `superscript`, encoderZmk: useZmk, }, }, { descr: "Subscript formatting", zmk: ",,subscript,,", expect: expectMap{ + encoderZJSON: `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`, encoderHTML: `subscript`, - encoderMD: "subscript", - encoderSz: `(INLINE (FORMAT-SUB () (TEXT "subscript")))`, - encoderSHTML: `((sub "subscript"))`, + encoderSexpr: `((FORMAT-SUB () (TEXT "subscript")))`, encoderText: `subscript`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting", zmk: `""quotes""`, expect: expectMap{ - encoderHTML: "“quotes”", - encoderMD: "quotes", - encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`, - encoderSHTML: `((@L (@H "“") "quotes" (@H "”")))`, + encoderZJSON: `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`, + encoderHTML: "quotes", + encoderSexpr: `((FORMAT-QUOTE () (TEXT "quotes")))`, encoderText: `quotes`, encoderZmk: useZmk, }, }, { descr: "Quotes formatting (german)", zmk: `""quotes""{lang=de}`, expect: expectMap{ - encoderHTML: `„quotes“`, - encoderMD: "quotes", - encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`, - encoderSHTML: `((span (@ (lang . "de")) (@H "„") "quotes" (@H "“")))`, + encoderZJSON: `[{"":"Quote","a":{"lang":"de"},"i":[{"":"Text","s":"quotes"}]}]`, + encoderHTML: `quotes`, + encoderSexpr: `((FORMAT-QUOTE (("lang" "de")) (TEXT "quotes")))`, encoderText: `quotes`, encoderZmk: `""quotes""{lang="de"}`, }, }, { - descr: "Empty quotes (default)", - zmk: `""""`, - expect: expectMap{ - encoderHTML: `“”`, - encoderMD: "", - encoderSz: `(INLINE (FORMAT-QUOTE ()))`, - encoderSHTML: `((@L (@H "“" "”")))`, - encoderText: ``, - encoderZmk: useZmk, - }, - }, - { - descr: "Empty quotes (unknown)", - zmk: `""""{lang=unknown}`, - expect: expectMap{ - encoderHTML: `""`, - encoderMD: "", - encoderSz: `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`, - encoderSHTML: `((span (@ (lang . "unknown")) (@H """ """)))`, - encoderText: ``, - encoderZmk: `""""{lang="unknown"}`, - }, - }, - { - descr: "Nested quotes (default)", - zmk: `""say: ::""yes, ::""or?""::""::""`, - expect: expectMap{ - encoderHTML: `“say: ‘yes, “or?””`, - encoderMD: "say: yes, or?", - encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "say:") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes,") (SPACE) (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`, - encoderSHTML: `((@L (@H "“") "say:" " " (span (@L (@H "‘") "yes," " " (span (@L (@H "“") "or?" (@H "”"))) (@H "’"))) (@H "”")))`, - encoderText: `say: yes, or?`, - encoderZmk: useZmk, - }, - }, - { - descr: "Two quotes", - zmk: `""yes"" or ""no""`, - expect: expectMap{ - encoderHTML: `“yes” or “no”`, - encoderMD: "yes or no", - encoderSz: `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (SPACE) (TEXT "or") (SPACE) (FORMAT-QUOTE () (TEXT "no")))`, - encoderSHTML: `((@L (@H "“") "yes" (@H "”")) " " "or" " " (@L (@H "“") "no" (@H "”")))`, - encoderText: `yes or no`, - encoderZmk: useZmk, - }, - }, - { - descr: "Mark formatting", - zmk: `##marked##`, - expect: expectMap{ - encoderHTML: `marked`, - encoderMD: "marked", - encoderSz: `(INLINE (FORMAT-MARK () (TEXT "marked")))`, - encoderSHTML: `((mark "marked"))`, - encoderText: `marked`, - encoderZmk: useZmk, - }, - }, - { descr: "Span formatting", zmk: `::span::`, expect: expectMap{ + encoderZJSON: `[{"":"Span","i":[{"":"Text","s":"span"}]}]`, encoderHTML: `span`, - encoderMD: "span", - encoderSz: `(INLINE (FORMAT-SPAN () (TEXT "span")))`, - encoderSHTML: `((span "span"))`, + encoderSexpr: `((FORMAT-SPAN () (TEXT "span")))`, encoderText: `span`, encoderZmk: useZmk, }, }, { descr: "Code formatting", zmk: "``code``", expect: expectMap{ + encoderZJSON: `[{"":"Code","s":"code"}]`, encoderHTML: `code`, - encoderMD: "`code`", - encoderSz: `(INLINE (LITERAL-CODE () "code"))`, - encoderSHTML: `((code "code"))`, + encoderSexpr: `((LITERAL-CODE () "code"))`, encoderText: `code`, encoderZmk: useZmk, }, }, { descr: "Code formatting with visible space", zmk: "``x y``{-}", expect: expectMap{ + encoderZJSON: `[{"":"Code","a":{"-":""},"s":"x y"}]`, encoderHTML: "x\u2423y", - encoderMD: "`x y`", - encoderSz: `(INLINE (LITERAL-CODE (("-" . "")) "x y"))`, - encoderSHTML: "((code \"x\u2423y\"))", + encoderSexpr: `((LITERAL-CODE (("-" "")) "x y"))`, encoderText: `x y`, encoderZmk: useZmk, }, }, { descr: "HTML in Code formatting", zmk: "``