Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Difference From v0.12.0 To v0.13.0
|
2023-08-07
| ||
| 13:56 | Typo on home page ... (check-in: d2fe74163e user: stern tags: trunk) | |
| 13:53 | Version 0.13.0 ... (check-in: 37fed58a18 user: stern tags: trunk, release, v0.13.0) | |
| 10:49 | Update changelog ... (check-in: 8da4351a55 user: stern tags: trunk) | |
|
2023-06-08
| ||
| 12:13 | Increase version to 0.13.0-dev to begin next development cycle ... (check-in: 761b8bd088 user: t73fde tags: trunk) | |
|
2023-06-05
| ||
| 12:12 | Version 0.12.0 ... (check-in: 0a29539266 user: stern tags: trunk, release, v0.12.0) | |
|
2023-06-04
| ||
| 12:58 | Fix: empty tags field on edit ... (check-in: 9d331fd7f9 user: t73fde tags: trunk) | |
Changes to README.md.
| ︙ | ︙ | |||
19 20 21 22 23 24 25 | 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). | | | 19 20 21 22 23 24 25 26 | 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) … |
Changes to VERSION.
|
| | | 1 | 0.13.0 |
Changes to ast/block.go.
1 2 3 4 5 6 7 8 9 10 11 12 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package ast | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package ast
import "zettelstore.de/client.fossil/attrs"
// Definition of Block nodes.
// BlockSlice is a slice of BlockNodes.
type BlockSlice []BlockNode
func (*BlockSlice) blockNode() { /* Just a marker */ }
|
| ︙ | ︙ |
Changes to ast/inline.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package ast import ( "unicode/utf8" "zettelstore.de/client.fossil/attrs" ) // Definitions of inline nodes. // InlineSlice is a list of BlockNodes. type InlineSlice []InlineNode |
| ︙ | ︙ |
Changes to ast/walk_test.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package ast_test import ( "testing" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package ast_test
import (
"testing"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/z/ast"
)
func BenchmarkWalk(b *testing.B) {
root := ast.BlockSlice{
&ast.HeadingNode{
Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"),
|
| ︙ | ︙ |
Added auth/impl/digest.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package impl
import (
"bytes"
"crypto"
"crypto/hmac"
"encoding/base64"
"zettelstore.de/sx.fossil"
"zettelstore.de/sx.fossil/sxreader"
)
var encoding = base64.RawURLEncoding
const digestAlg = crypto.SHA384
func sign(claim sx.Object, secret []byte) ([]byte, error) {
var buf bytes.Buffer
sx.Print(&buf, claim)
token := make([]byte, encoding.EncodedLen(buf.Len()))
encoding.Encode(token, buf.Bytes())
digest := hmac.New(digestAlg.New, secret)
_, err := digest.Write(buf.Bytes())
if err != nil {
return nil, err
}
dig := digest.Sum(nil)
encDig := make([]byte, encoding.EncodedLen(len(dig)))
encoding.Encode(encDig, dig)
token = append(token, '.')
token = append(token, encDig...)
return token, nil
}
func check(token []byte, secret []byte) (sx.Object, error) {
i := bytes.IndexByte(token, '.')
if i <= 0 || 1024 < i {
return nil, ErrMalformedToken
}
buf := make([]byte, len(token))
n, err := encoding.Decode(buf, token[:i])
if err != nil {
return nil, err
}
rdr := sxreader.MakeReader(bytes.NewReader(buf[:n]))
obj, err := rdr.Read()
if err != nil {
return nil, err
}
var objBuf bytes.Buffer
_, err = sx.Print(&objBuf, obj)
if err != nil {
return nil, err
}
digest := hmac.New(digestAlg.New, secret)
_, err = digest.Write(objBuf.Bytes())
if err != nil {
return nil, err
}
n, err = encoding.Decode(buf, token[i+1:])
if err != nil {
return nil, err
}
if !hmac.Equal(buf[:n], digest.Sum(nil)) {
return nil, ErrMalformedToken
}
return obj, nil
}
|
Changes to auth/impl/impl.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "errors" "hash/fnv" "io" "time" | | | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "errors" "hash/fnv" "io" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/sexp" "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/auth/policy" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ | |||
63 64 65 66 67 68 69 |
}
return h.Sum(nil)
}
// IsReadonly returns true, if the systems is configured to run in read-only-mode.
func (a *myAuth) IsReadonly() bool { return a.readonly }
| | > | | | > | < < < | < < < < < > | < < | > | > | | > > > > > > > | < < < | > | | | | < < < < | < | | < < | < > > | | | > > > > > | | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
}
return h.Sum(nil)
}
// 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")
// 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.
var ErrOtherKind = errors.New("auth: wrong token kind")
// ErrNoZid signals that the 'zid' key is missing.
var ErrNoZid = errors.New("auth: missing zettel id")
// GetToken returns a token to be used for authentification.
func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) {
subject, ok := ident.Get(api.KeyUserID)
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)
}
// 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)
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 = ident.String()
tokenData.Issued = issued
tokenData.Now = now
tokenData.Expires = expires
tokenData.Zid = zid
return nil
}
func (a *myAuth) Owner() id.Zid { return a.owner }
func (a *myAuth) IsOwner(zid id.Zid) bool {
return zid.IsValid() && zid == a.owner
}
|
| ︙ | ︙ |
Changes to auth/policy/box.go.
| ︙ | ︙ | |||
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
}
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) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
m, err := pp.box.GetMeta(ctx, zid)
if err != nil {
return nil, err
}
user := server.GetUser(ctx)
if pp.policy.CanRead(user, m) {
return m, nil
}
return nil, box.NewErrNotAllowed("GetMeta", user, zid)
}
| > > > > < < < < < < < < | | | | | | | | | 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
}
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)
}
func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
m, err := pp.box.GetMeta(ctx, zid)
if err != nil {
return nil, err
}
user := server.GetUser(ctx)
if pp.policy.CanRead(user, m) {
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) {
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)
}
func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool {
return pp.box.CanUpdateZettel(ctx, zettel)
}
func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
zid := zettel.Meta.Zid
user := server.GetUser(ctx)
if !zid.IsValid() {
return &box.ErrInvalidID{Zid: zid}
}
// Write existing zettel
oldZettel, err := pp.box.GetZettel(ctx, zid)
if err != nil {
return err
}
if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
return pp.box.UpdateZettel(ctx, zettel)
}
return box.NewErrNotAllowed("Write", user, zid)
}
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)
if err != nil {
return err
}
user := server.GetUser(ctx)
if pp.policy.CanRename(user, z.Meta) {
return pp.box.RenameZettel(ctx, curZid, newZid)
}
return box.NewErrNotAllowed("Rename", user, curZid)
}
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)
if err != nil {
return err
}
user := server.GetUser(ctx)
if pp.policy.CanDelete(user, z.Meta) {
return pp.box.DeleteZettel(ctx, zid)
}
return box.NewErrNotAllowed("Delete", user, zid)
}
func (pp *polBox) Refresh(ctx context.Context) error {
user := server.GetUser(ctx)
|
| ︙ | ︙ |
Changes to auth/policy/default.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package policy
import (
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/zettel/meta"
)
type defaultPolicy struct {
manager auth.AuthzManager
}
|
| ︙ | ︙ |
Changes to auth/policy/owner.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package policy import ( | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package policy
import (
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/config"
"zettelstore.de/z/zettel/meta"
)
type ownerPolicy struct {
manager auth.AuthzManager
|
| ︙ | ︙ |
Changes to auth/policy/policy_test.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package policy import ( "fmt" "testing" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package policy
import (
"fmt"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func TestPolicies(t *testing.T) {
t.Parallel()
|
| ︙ | ︙ |
Changes to box/box.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "context" "errors" "fmt" "io" "time" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "context" "errors" "fmt" "io" "time" "zettelstore.de/client.fossil/api" "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. |
| ︙ | ︙ | |||
37 38 39 40 41 42 43 | // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) | < < < | 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // CreateZettel creates a new zettel. // Returns the new zettel id (and an error indication). CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) // GetZettel retrieves a specific zettel. GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) // 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 // AllowRenameZettel returns true, if box will not disallow renaming the zettel. |
| ︙ | ︙ | |||
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// MetaFunc is a function that processes metadata of a zettel.
type MetaFunc func(*meta.Meta)
// ManagedBox is the interface of managed boxes.
type ManagedBox interface {
BaseBox
// 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
| > > > | 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
// MetaFunc is a function that processes metadata of a zettel.
type MetaFunc func(*meta.Meta)
// ManagedBox is the interface of managed boxes.
type ManagedBox interface {
BaseBox
// HasZettel returns true, if box conains zettel with given identifier.
HasZettel(context.Context, id.Zid) bool
// Apply identifier of every zettel to the given function, if predicate returns true.
ApplyZid(context.Context, ZidFunc, query.RetrievePredicate) error
// Apply metadata of every zettel to the given function, if predicate returns true.
ApplyMeta(context.Context, MetaFunc, query.RetrievePredicate) error
|
| ︙ | ︙ | |||
128 129 130 131 132 133 134 135 136 |
// Box is to be used outside the box package and its descendants.
type Box interface {
BaseBox
// FetchZids returns the set of all zettel identifer managed by the box.
FetchZids(ctx context.Context) (id.Set, error)
// SelectMeta returns a list of metadata that comply to the given selection criteria.
| > > > > | < < < | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
// Box is to be used outside the box package and its descendants.
type Box interface {
BaseBox
// FetchZids returns the set of all zettel identifer managed by the box.
FetchZids(ctx context.Context) (id.Set, error)
// GetMeta returns the metadata of the zettel with the given identifier.
GetMeta(context.Context, id.Zid) (*meta.Meta, error)
// SelectMeta returns a list of metadata that comply to the given selection criteria.
// If `metaSeq` is `nil`, the box assumes metadata of all available zettel.
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
// GetAllZettel retrieves a specific zettel from all managed boxes.
GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
// Refresh the data from the box and from its managed sub-boxes.
Refresh(context.Context) error
}
// Stats record stattistics about a box.
type Stats struct {
// ReadOnly indicates that boxes cannot be modified.
|
| ︙ | ︙ |
Changes to box/compbox/compbox.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package compbox provides zettel that have computed content. package compbox import ( "context" "net/url" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" |
| ︙ | ︙ | |||
78 79 80 81 82 83 84 |
}
func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
if gen, ok := myZettel[zid]; ok && gen.meta != nil {
if m := gen.meta(zid); m != nil {
updateMeta(m)
if genContent := gen.content; genContent != nil {
| | | | | < < < < | < < < < < | | 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
}
func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
if gen, ok := myZettel[zid]; ok && gen.meta != nil {
if m := gen.meta(zid); m != nil {
updateMeta(m)
if genContent := gen.content; genContent != nil {
cb.log.Trace().Msg("GetZettel/Content")
return zettel.Zettel{
Meta: m,
Content: zettel.NewContent(genContent(m)),
}, nil
}
cb.log.Trace().Msg("GetZettel/NoContent")
return zettel.Zettel{Meta: m}, nil
}
}
cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err")
return zettel.Zettel{}, box.ErrNotFound
}
func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool {
_, found := myZettel[zid]
return found
}
func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid")
for zid, gen := range myZettel {
if !constraint(zid) {
continue
}
if genMeta := gen.meta; genMeta != nil {
if genMeta(zid) != nil {
handle(zid)
|
| ︙ | ︙ |
Changes to box/compbox/config.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package compbox import ( "bytes" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package compbox
import (
"bytes"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/kernel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func genConfigZettelM(zid id.Zid) *meta.Meta {
if myConfig == nil {
|
| ︙ | ︙ |
Changes to box/compbox/keys.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package compbox import ( "bytes" "fmt" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
package compbox
import (
"bytes"
"fmt"
"zettelstore.de/client.fossil/api"
"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")
m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
return m
}
func genKeysC(*meta.Meta) []byte {
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())
}
return buf.Bytes()
}
|
Changes to box/compbox/log.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package compbox import ( "bytes" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package compbox
import (
"bytes"
"zettelstore.de/client.fossil/api"
"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)
|
| ︙ | ︙ |
Changes to box/compbox/manager.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package compbox import ( "bytes" "fmt" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package compbox
import (
"bytes"
"fmt"
"zettelstore.de/client.fossil/api"
"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)
|
| ︙ | ︙ |
Changes to box/compbox/parser.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "bytes" "fmt" "sort" "strings" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import (
"bytes"
"fmt"
"sort"
"strings"
"zettelstore.de/client.fossil/api"
"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 {
|
| ︙ | ︙ |
Changes to box/compbox/version.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package compbox import ( | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package compbox
import (
"zettelstore.de/client.fossil/api"
"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)
|
| ︙ | ︙ |
Changes to box/constbox/base.sxn.
| ︙ | ︙ | |||
32 33 34 35 36 37 38 |
(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"))
| | | 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
(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)
)))
)
(form (@ (action ,search-url))
(input (@ (type "text") (placeholder "Search..") (name ,query-key-query))))
)
(main (@ (class "content")) ,DETAIL)
,@(if FOOTER `((footer (hr) ,@FOOTER)))
|
| ︙ | ︙ |
Changes to box/constbox/constbox.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package constbox import ( "context" _ "embed" // Allow to embed file content "net/url" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" |
| ︙ | ︙ | |||
69 70 71 72 73 74 75 |
cb.log.Trace().Msg("GetZettel")
return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
}
cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel")
return zettel.Zettel{}, box.ErrNotFound
}
| | | < < < < | | 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
cb.log.Trace().Msg("GetZettel")
return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
}
cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel")
return zettel.Zettel{}, box.ErrNotFound
}
func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool {
_, found := cb.zettel[zid]
return found
}
func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid")
for zid := range cb.zettel {
if constraint(zid) {
handle(zid)
|
| ︙ | ︙ | |||
219 220 221 222 223 224 225 |
zettel.NewContent(contentZettelSxn)},
id.InfoTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Info HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20200804111624",
| | | | | | > > > > > > > > > | 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
zettel.NewContent(contentZettelSxn)},
id.InfoTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Info HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20200804111624",
api.KeyModified: "20230621131500",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentInfoSxn)},
id.FormTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Form HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20200804111624",
api.KeyModified: "20230621132600",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentFormSxn)},
id.RenameTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Rename Form HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20200804111624",
api.KeyModified: "20230707190246",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentRenameSxn)},
id.DeleteTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Delete HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20200804111624",
api.KeyModified: "20230621133100",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentDeleteSxn)},
id.ListTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore List Zettel HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20230704122100",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentListZettelSxn)},
id.ErrorTemplateZid: {
constHeader{
api.KeyTitle: "Zettelstore Error HTML Template",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20210305133215",
api.KeyModified: "20230527224800",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentErrorSxn)},
id.TemplateSxnZid: {
constHeader{
api.KeyTitle: "Zettelstore Sxn Code for Templates",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxSxn,
api.KeyCreated: "20230619132800",
api.KeyVisibility: api.ValueVisibilityExpert,
},
zettel.NewContent(contentTemplateCodeSxn)},
id.MustParse(api.ZidBaseCSS): {
constHeader{
api.KeyTitle: "Zettelstore Base CSS",
api.KeyRole: api.ValueRoleConfiguration,
api.KeySyntax: meta.SyntaxCSS,
api.KeyCreated: "20200804111624",
api.KeyVisibility: api.ValueVisibilityPublic,
|
| ︙ | ︙ | |||
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 | //go:embed listzettel.sxn var contentListZettelSxn []byte //go:embed error.sxn var contentErrorSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte //go:embed home.zettel var contentHomeZettel []byte | > > > | 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | //go:embed listzettel.sxn var contentListZettelSxn []byte //go:embed error.sxn var contentErrorSxn []byte //go:embed wuicode.sxn var contentTemplateCodeSxn []byte //go:embed base.css var contentBaseCSS []byte //go:embed emoji_spin.gif var contentEmoji []byte //go:embed newtoc.zettel var contentNewTOCZettel []byte //go:embed home.zettel var contentHomeZettel []byte |
Changes to box/constbox/delete.sxn.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
`(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.")
| | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
`(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"))))
)
|
Changes to box/constbox/dependencies.zettel.
| ︙ | ︙ | |||
97 98 99 100 101 102 103 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 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. ``` | < < < < < < < < < < < < < | 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 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. ``` === yuin/goldmark ; URL & Source : [[https://github.com/yuin/goldmark]] ; License : MIT License ``` MIT License |
| ︙ | ︙ | |||
139 140 141 142 143 144 145 | 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. ``` | | | | < < | 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | 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. ``` === sx, zettelstore-client These are companion projects, written by the current main developer of Zettelstore. They are published under the same license, [[EUPL v1.2, or later|00000000000004]]. ; URL & Source sx : [[https://zettelstore.de/sx]] ; URL & Source zettelstore-client : [[https://zettelstore.de/client/]] ; License: : European Union Public License, version 1.2 (EUPL v1.2), or later. |
Changes to box/constbox/form.sxn.
1 2 3 4 5 6 7 8 9 10 11 |
`(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") (placeholder "Title..") (value ,meta-title) (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") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role)
,@(if role-data '((list "zs-role-data")))
))
| < | < < < < | < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
`(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") (placeholder "Title..") (value ,meta-title) (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") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role)
,@(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") (placeholder "#tag") (value ,meta-tags))))
(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") (placeholder "metakey: metavalue")) ,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") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax)
,@(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") (placeholder "Zettel content..")) ,content)
))
)
|
| ︙ | ︙ |
Changes to box/constbox/info.sxn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
`(article
(header (h1 "Information for Zettel " ,zid)
(p
(a (@ (href ,web-url)) "Web")
(@H " · ") (a (@ (href ,context-url)) "Context")
,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit")))
,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))
,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))
,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))
,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename")))
,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete")))
)
)
(h2 "Interpreted Metadata")
| > | | < < < < | < < < < | < < < < | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
`(article
(header (h1 "Information for Zettel " ,zid)
(p
(a (@ (href ,web-url)) "Web")
(@H " · ") (a (@ (href ,context-url)) "Context")
,@(if (bound? 'edit-url) `((@H " · ") (a (@ (href ,edit-url)) "Edit")))
,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))
,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))
,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))
,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))
,@(if (bound? 'rename-url) `((@H " · ") (a (@ (href ,rename-url)) "Rename")))
,@(if (bound? 'delete-url) `((@H " · ") (a (@ (href ,delete-url)) "Delete")))
)
)
(h2 "Interpreted Metadata")
(table ,@(map wui-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))
)
)
)
|
Changes to box/constbox/listzettel.sxn.
1 2 3 4 5 6 |
`(article
(header (h1 ,heading))
(form (@ (action ,search-url))
(input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value))))
,@content
,@endnotes
| > > > > > | < | | | < > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
`(article
(header (h1 ,heading))
(form (@ (action ,search-url))
(input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value))))
,@content
,@endnotes
(form (@ (action ,(if (bound? 'create-url) create-url)))
"Other encodings: "
(a (@ (href ,data-url)) "data")
", "
(a (@ (href ,plain-url)) "plain")
,@(if (bound? 'create-url)
`((input (@ (type "hidden") (name ,query-key-query) (value ,query-value)))
(input (@ (type "hidden") (name ,query-key-seed) (value ,seed)))
(input (@ (class "zs-primary") (type "submit") (value "Save As Zettel")))
)
)
)
)
|
Changes to box/constbox/login.sxn.
1 | `(article | | | 1 2 3 4 5 6 7 8 9 |
`(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:")
|
| ︙ | ︙ |
Changes to box/constbox/rename.sxn.
1 2 3 4 5 6 7 |
`(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.")
| | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
`(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") (id "newzid") (name "newzid") (placeholder "ZID..") (value ,zid) (autofocus))))
(div (input (@ (class "zs-primary") (type "submit") (value "Rename"))))
)
,(wui-meta-desc metapairs)
)
|
Added box/constbox/wuicode.sxn.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;----------------------------------------------------------------------------
;; wui-list-item returns the argument as a HTML list item.
(define (wui-item s) `(li ,s))
;; wui-table-row takes a pair and translates it into a HTML table row with
;; two columns.
(define (wui-table-row p)
`(tr (td ,(car p)) (td ,(cdr p))))
;; wui-valid-link translates a local link into a HTML link. A link is a pair
;; (valid . url). If valid is not truish, only the invalid url is returned.
(define (wui-valid-link l)
(if (car l)
`(li (a (@ (href ,(cdr l))) ,(cdr l)))
`(li ,(cdr l))))
;; wui-link taks an url and returns a HTML link inside.
(define (wui-link q)
`(a (@ (href ,(cdr q))) ,(car q)))
;; wui-item-link taks a pair (text . url) and returns a HTML link inside
;; a list item.
(define (wui-item-link q) `(li ,(wui-link q)))
;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside
;; a table data item.
(define (wui-tdata-link q) `(td ,(wui-link q)))
;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open
;; a new tab / window.
(define (wui-item-popup-link e)
`(li (a (@ (href ,e) (target "_blank") (rel "noopener noreferrer")) ,e)))
;; wui-option-value returns a value for an HTML option element.
(define (wui-option-value v) `(option (@ (value ,v))))
;; wui-datalist returns a HTML datalist with the given HTML identifier and a
;; list of values.
(define (wui-datalist id lst)
(if lst
`((datalist (@ (id ,id)) ,@(map wui-option-value lst)))))
;; wui-pair-desc-item takes a pair '(term . text) and returns a list with
;; a HTML description term and a HTML description data.
(define (wui-pair-desc-item p) `((dt ,(car p)) (dd ,(cdr p))))
;; wui-meta-desc returns a HTML description list made from the list of pairs
;; given.
(define (wui-meta-desc l)
`(dl ,@(apply append (map wui-pair-desc-item l))))
;; wui-enc-matrix returns the HTML table of all encodings and parts.
(define (wui-enc-matrix matrix)
`(table
,@(map
(lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row))))
matrix)))
|
Changes to box/constbox/zettel.sxn.
1 2 3 4 5 6 7 |
`(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 " · ")
| | > > > > | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
`(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))
,@(if (bound? 'copy-url) `((@H " · ") (a (@ (href ,copy-url)) "Copy")))
,@(if (bound? 'version-url) `((@H " · ") (a (@ (href ,version-url)) "Version")))
,@(if (bound? 'child-url) `((@H " · ") (a (@ (href ,child-url)) "Child")))
,@(if (bound? 'folge-url) `((@H " · ") (a (@ (href ,folge-url)) "Folge")))
,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
,@(if superior-refs `((br) "Superior: " ,superior-refs))
,@(if (bound? 'meta-url) `((br) "URL: " ,(url-to-html meta-url)))
,@(let (author (and (bound? 'meta-author) meta-author)) (if author `((br) "By " ,author)))
)
)
,@content
,endnotes
,@(if (or folge-links subordinate-links back-links successor-links)
`((nav
,@(if folge-links `((details (@ (open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
,@(if subordinate-links `((details (@ (open)) (summary "Subordinates") (ul ,@(map wui-item-link subordinate-links)))))
,@(if back-links `((details (@ (open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
,@(if successor-links `((details (@ (open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
))
)
)
|
Changes to box/dirbox/dirbox.go.
| ︙ | ︙ | |||
256 257 258 259 260 261 262 |
return zettel.Zettel{}, err
}
zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)}
dp.log.Trace().Zid(zid).Msg("GetZettel")
return zettel, nil
}
| | < < < < < | < < < < < < < < | 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
return zettel.Zettel{}, err
}
zettel := zettel.Zettel{Meta: m, Content: zettel.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) 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")
for _, entry := range entries {
handle(entry.Zid)
|
| ︙ | ︙ | |||
349 350 351 352 353 354 355 |
return box.ErrNotFound
}
if dp.readonly {
return box.ErrReadOnly
}
// Check whether zettel with new ID already exists in this box.
| | | 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 |
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.ErrInvalidID{Zid: newZid}
}
oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid)
if err != nil {
return err
}
|
| ︙ | ︙ |
Changes to box/filebox/filebox.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "errors" "net/url" "path/filepath" "strings" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "errors" "net/url" "path/filepath" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to box/filebox/zipbox.go.
| ︙ | ︙ | |||
136 137 138 139 140 141 142 |
}
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
}
| | | < < < < < < < < < < < | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
}
CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles)
zb.log.Trace().Zid(zid).Msg("GetZettel")
return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil
}
func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool {
return zb.dirSrv.GetDirEntry(zid).IsValid()
}
func (zb *zipBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
entries := zb.dirSrv.GetDirEntries(constraint)
zb.log.Trace().Int("entries", int64(len(entries))).Msg("ApplyZid")
for _, entry := range entries {
handle(entry.Zid)
|
| ︙ | ︙ |
Changes to box/manager/box.go.
| ︙ | ︙ | |||
90 91 92 93 94 95 96 | mgr.Enrich(ctx, z.Meta, i+1) result = append(result, z) } } return result, nil } | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > > > > > > > > > > > > > > | > > > > | > > > > > > > > > | < | < | < < < | | 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
mgr.Enrich(ctx, z.Meta, i+1)
result = append(result, z)
}
}
return result, nil
}
// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) {
mgr.mgrLog.Debug().Msg("FetchZids")
if mgr.State() != box.StartStateStarted {
return nil, box.ErrStopped
}
result := id.Set{}
mgr.mgrMx.RLock()
defer mgr.mgrMx.RUnlock()
for _, p := range mgr.boxes {
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{}
for _, term := range compSearch.Terms {
rejected := id.Set{}
handleMeta := func(m *meta.Meta) {
zid := m.Zid
if rejected.Contains(zid) {
mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
return
|
| ︙ | ︙ |
Changes to box/manager/enrich.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package manager import ( "context" "strconv" | | > | > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
package manager
import (
"context"
"strconv"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/box"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Enrich computes additional properties and updates the given metadata.
func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) {
// Calculate computed, but stored values.
if _, ok := m.Get(api.KeyCreated); !ok {
m.Set(api.KeyCreated, computeCreated(m.Zid))
}
if box.DoNotEnrich(ctx) {
// 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))
}
mgr.idxStore.Enrich(ctx, m)
}
func computeCreated(zid id.Zid) string {
if zid <= 10101000000 {
// A year 0000 is not allowed and therefore an artificaial Zid.
// In the year 0001, the month must be > 0.
|
| ︙ | ︙ |
Changes to box/manager/indexer.go.
| ︙ | ︙ | |||
159 160 161 162 163 164 165 |
func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
var cData collectData
cData.initialize()
collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)
m := zettel.Meta
| | | 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
var cData collectData
cData.initialize()
collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)
m := zettel.Meta
zi := store.NewZettelIndex(m)
mgr.idxCollectFromMeta(ctx, m, zi, &cData)
mgr.idxProcessData(ctx, zi, &cData)
toCheck := mgr.idxStore.UpdateReferences(ctx, zi)
mgr.idxCheckZettel(toCheck)
}
func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
|
| ︙ | ︙ | |||
196 197 198 199 200 201 202 |
}
}
}
}
func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
for ref := range cData.refs {
| | | | | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
}
}
}
}
func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
for ref := range cData.refs {
if mgr.HasZettel(ctx, ref) {
zi.AddBackRef(ref)
} else {
zi.AddDeadRef(ref)
}
}
zi.SetWords(cData.words)
zi.SetUrls(cData.urls)
}
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) {
zi.AddDeadRef(zid)
return
}
if inverseKey == "" {
zi.AddBackRef(zid)
return
}
zi.AddInverseRef(inverseKey, 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)
}
}
|
Changes to box/manager/manager.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "context" "io" "net/url" "sync" "time" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import ( "context" "io" "net/url" "sync" "time" "zettelstore.de/client.fossil/maps" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/box/manager/memstore" "zettelstore.de/z/box/manager/store" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" |
| ︙ | ︙ |
Changes to box/manager/memstore/memstore.go.
| ︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "fmt" "io" "sort" "strings" "sync" | | > | | | > | | | | | | < < < < < < < | > | | | | > > | | | | > > > > > > > > > | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
"context"
"fmt"
"io"
"sort"
"strings"
"sync"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/box"
"zettelstore.de/z/box/manager/store"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
type bidiRefs struct {
forward id.Slice
backward id.Slice
}
type zettelData struct {
meta *meta.Meta
dead id.Slice
forward id.Slice
backward id.Slice
otherRefs map[string]bidiRefs
words []string
urls []string
}
type stringRefs map[string]id.Slice
type memStore struct {
mx sync.RWMutex
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 {
return zi.meta.Clone(), nil
}
return nil, box.ErrNotFound
}
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]
|
| ︙ | ︙ | |||
96 97 98 99 100 101 102 |
updated = true
}
if len(zi.forward) > 0 {
m.Set(api.KeyForward, zi.forward.String())
back = remRefs(back, zi.forward)
updated = true
}
| | | 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
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 {
|
| ︙ | ︙ | |||
230 231 232 233 234 235 236 | continue } result.AddSlice(refs) } return result } | | | | 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
continue
}
result.AddSlice(refs)
}
return result
}
func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) {
// Must only be called if ms.mx is read-locked!
result.Zid(zid)
result.AddSlice(zi.backward)
for _, mref := range zi.otherRefs {
result.AddSlice(mref.backward)
}
}
func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
for _, p := range m.PairsRest() {
switch meta.Type(p.Key) {
|
| ︙ | ︙ | |||
258 259 260 261 262 263 264 265 266 267 268 |
}
}
}
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 {
| > | > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | | | | | < | 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
}
}
}
return back
}
func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set {
m := ms.makeMeta(zidx)
ms.mx.Lock()
defer ms.mx.Unlock()
zi, ziExist := ms.idx[zidx.Zid]
if !ziExist || zi == nil {
zi = &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)
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 {
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, "-role")
}
func (ms *memStore) internString(s string) string {
if is, found := ms.intern[s]; found {
return is
}
ms.intern[s] = s
return s
}
func (ms *memStore) makeMeta(zidx *store.ZettelIndex) *meta.Meta {
origM := zidx.GetMeta()
copyM := meta.New(origM.Zid)
for _, p := range origM.Pairs() {
key := ms.internString(p.Key)
if isInternableValue(key) {
copyM.Set(key, ms.internString(p.Value))
} else if key == api.KeyBoxNumber || !meta.IsComputed(key) {
copyM.Set(key, p.Value)
}
}
return copyM
}
func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) {
// Must only be called if ms.mx is write-locked!
drefs := zidx.GetDeadRefs()
newRefs, remRefs := refsDiff(drefs, zi.dead)
zi.dead = drefs
for _, ref := range remRefs {
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) {
// 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 *zettelData) {
// 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)
}
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.getEntry(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
}
ms.removeInverseMeta(zidx.Zid, key, remRefs)
}
}
func updateWordSet(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}
|
| ︙ | ︙ | |||
372 373 374 375 376 377 378 | continue } srefs[word] = refs2 } return next.Words() } | | | | | | | | 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 |
continue
}
srefs[word] = refs2
}
return next.Words()
}
func (ms *memStore) getEntry(zid id.Zid) *zettelData {
// Must only be called if ms.mx is write-locked!
if zi, ok := ms.idx[zid]; ok {
return zi
}
zi := &zettelData{}
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.otherRefs) > 0 {
for key, mrefs := range zi.otherRefs {
ms.removeInverseMeta(zid, key, mrefs.forward)
}
}
ms.deleteWords(zid, zi.words)
delete(ms.idx, zid)
return toCheck
}
func (ms *memStore) deleteDeadSources(zid id.Zid, zi *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!
var toCheck id.Set
for _, ref := range zi.forward {
if fzi, ok := ms.idx[ref]; ok {
fzi.backward = remRef(fzi.backward, zid)
}
}
|
| ︙ | ︙ | |||
441 442 443 444 445 446 447 |
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]
| | | | | | | | 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 |
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 (ms *memStore) deleteWords(zid id.Zid, words []string) {
// Must only be called if ms.mx is write-locked!
|
| ︙ | ︙ | |||
479 480 481 482 483 484 485 |
ms.words[word] = refs2
}
}
func (ms *memStore) ReadStats(st *store.Stats) {
ms.mx.RLock()
st.Zettel = len(ms.idx)
| < > > > | 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 |
ms.words[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")
|
| ︙ | ︙ | |||
514 515 516 517 518 519 520 |
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)
| | | 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 |
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)
}
|
| ︙ | ︙ |
Changes to box/manager/store/store.go.
| ︙ | ︙ | |||
35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
Urls uint64
}
// Store all relevant zettel data. There may be multiple implementations, i.e.
// memory-based, file-based, based on SQLite, ...
type Store interface {
query.Searcher
// 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
| > > > | 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
Urls uint64
}
// 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
|
| ︙ | ︙ |
Changes to box/manager/store/zettel.go.
1 2 3 4 5 6 7 8 9 10 11 12 | //----------------------------------------------------------------------------- // Copyright (c) 2021-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package store | > | > > | > | | | | | | | > | | | | | | | | < | > > | < | < | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package store
import (
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
Zid id.Zid // zid of the indexed zettel
meta *meta.Meta // full metadata
backrefs id.Set // set of back references
inverseRefs map[string]id.Set // references of inverse keys
deadrefs id.Set // set of dead references
words WordSet
urls WordSet
}
// NewZettelIndex creates a new zettel index.
func NewZettelIndex(m *meta.Meta) *ZettelIndex {
return &ZettelIndex{
Zid: m.Zid,
meta: m,
backrefs: id.NewSet(),
inverseRefs: make(map[string]id.Set),
deadrefs: id.NewSet(),
}
}
// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
zi.backrefs.Zid(zid)
}
// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
if zids, ok := zi.inverseRefs[key]; ok {
zids.Zid(zid)
return
}
zi.inverseRefs[key] = id.NewSet(zid)
}
// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
zi.deadrefs.Zid(zid)
}
// 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 }
// GetBackRefs returns all back references as a sorted list.
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 {
return nil
}
result := make(map[string]id.Slice, len(zi.inverseRefs))
for key, refs := range zi.inverseRefs {
result[key] = refs.Sorted()
}
return result
}
// GetWords returns a reference to the set of words. It must not be modified.
func (zi *ZettelIndex) GetWords() WordSet { return zi.words }
// GetUrls returns a reference to the set of URLs. It must not be modified.
func (zi *ZettelIndex) GetUrls() WordSet { return zi.urls }
|
Changes to box/membox/membox.go.
| ︙ | ︙ | |||
19 20 21 22 23 24 25 | "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/query" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" | < | 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
"zettelstore.de/z/box"
"zettelstore.de/z/box/manager"
"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",
func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
return &memBox{
|
| ︙ | ︙ | |||
126 127 128 129 130 131 132 |
return zettel.Zettel{}, box.ErrNotFound
}
z.Meta = z.Meta.Clone()
mb.log.Trace().Msg("GetZettel")
return z, nil
}
| | | < | < < < | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
return zettel.Zettel{}, box.ErrNotFound
}
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
}
func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
mb.mx.RLock()
defer mb.mx.RUnlock()
mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid")
for zid := range mb.zettel {
|
| ︙ | ︙ |
Changes to box/notify/entry.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package notify import ( "path/filepath" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package notify import ( "path/filepath" "zettelstore.de/client.fossil/api" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) const ( |
| ︙ | ︙ |
Changes to cmd/cmd_file.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "context" "flag" "fmt" "io" "os" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "context" "flag" "fmt" "io" "os" "zettelstore.de/client.fossil/api" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to cmd/cmd_password.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "flag" "fmt" "os" "golang.org/x/term" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import (
"flag"
"fmt"
"os"
"golang.org/x/term"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth/cred"
"zettelstore.de/z/zettel/id"
)
// ---------- Subcommand: password -------------------------------------------
func cmdPassword(fs *flag.FlagSet) (int, error) {
|
| ︙ | ︙ |
Changes to cmd/cmd_run.go.
| ︙ | ︙ | |||
52 53 54 55 56 57 58 | 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) | > | | < | | > < | | | | | | < < < | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
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)
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)
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.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.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.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))
webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(&ucEvaluate, ucGetZettel))
webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))
// API
webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
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))
webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel, ucParseZettel, ucEvaluate))
if !authManager.IsReadonly() {
webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
}
|
| ︙ | ︙ |
Changes to cmd/command.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package cmd import ( "flag" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package cmd
import (
"flag"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/logger"
)
// Command stores information about commands / sub-commands.
type Command struct {
Name string // command name as it appears on the command line
Func CommandFunc // function that executes a command
|
| ︙ | ︙ |
Changes to cmd/main.go.
| ︙ | ︙ | |||
18 19 20 21 22 23 24 | "net/url" "os" "runtime/debug" "strconv" "strings" "time" | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "net/url" "os" "runtime/debug" "strconv" "strings" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/auth/impl" "zettelstore.de/z/box" "zettelstore.de/z/box/compbox" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/input" |
| ︙ | ︙ | |||
84 85 86 87 88 89 90 |
})
RegisterCommand(Command{
Name: "password",
Func: cmdPassword,
})
}
| | | | | | | | | | | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
})
RegisterCommand(Command{
Name: "password",
Func: cmdPassword,
})
}
func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) {
if configFlag := fs.Lookup("c"); configFlag != nil {
if filename := configFlag.Value.String(); filename != "" {
content, err := readConfiguration(filename)
return filename, createConfiguration(content, err)
}
}
filename, content, err := searchAndReadConfiguration()
return filename, createConfiguration(content, err)
}
func createConfiguration(content []byte, err error) *meta.Meta {
if err != nil {
return meta.New(id.Invalid)
}
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"} {
if content, err := readConfiguration(filename); err == nil {
return filename, content, nil
}
}
return "", nil, os.ErrNotExist
}
func getConfig(fs *flag.FlagSet) (string, *meta.Meta) {
filename, cfg := fetchStartupConfiguration(fs)
fs.Visit(func(flg *flag.Flag) {
switch flg.Name {
case "p":
cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
case "a":
cfg.Set(keyAdminPort, flg.Value.String())
case "d":
|
| ︙ | ︙ | |||
140 141 142 143 144 145 146 | cfg.Set(keyDebug, flg.Value.String()) case "r": cfg.Set(keyReadOnly, flg.Value.String()) case "v": cfg.Set(keyVerbose, flg.Value.String()) } }) | | | 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
cfg.Set(keyDebug, flg.Value.String())
case "r":
cfg.Set(keyReadOnly, flg.Value.String())
case "v":
cfg.Set(keyVerbose, flg.Value.String())
}
})
return filename, cfg
}
func deleteConfiguredBoxes(cfg *meta.Meta) {
for _, p := range cfg.PairsRest() {
if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) {
cfg.Delete(key)
}
|
| ︙ | ︙ | |||
247 248 249 250 251 252 253 |
return 1
}
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
}
| | | 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
return 1
}
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)
if !setServiceConfig(cfg) {
fs.Usage()
return 2
}
kern := kernel.Main
var createManager kernel.CreateBoxManagerFunc
|
| ︙ | ︙ | |||
286 287 288 289 290 291 292 |
return nil
},
)
if command.Simple {
kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true")
}
| | | | 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 |
return nil
},
)
if command.Simple {
kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true")
}
kern.Start(command.Header, command.LineServer, filename)
exitCode, err := command.Func(fs)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
}
kern.Shutdown(true)
return exitCode
}
// 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 {
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)
return 1
}
|
| ︙ | ︙ |
Changes to docs/manual/00001002000000.zettel.
1 2 3 4 5 6 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 | | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | id: 00001002000000 title: Design goals for the Zettelstore role: manual tags: #design #goal #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20230624171152 Zettelstore supports the following design goals: ; Longevity of stored notes / zettel : Every zettel you create should be readable without the help of any tool, even without Zettelstore. : It should be not hard to write other software that works with your zettel. : Normal zettel should be stored in a single file. If this is not possible: at most in two files: one for the metadata, one for the content. The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable. : There is no additional database. ; Single user : All zettel belong to you, only to you. Zettelstore provides its services only to one person: you. If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel. : If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel. ; Ease of installation : If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working. |
| ︙ | ︙ | |||
31 32 33 34 35 36 37 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : 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. | > > | 35 36 37 38 39 40 41 42 43 | ; Simple service : The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them. : 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. |
Changes to docs/manual/00001005090000.zettel.
1 2 3 4 5 6 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001005090000 title: List of predefined zettel role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20230619133707 The following table lists all predefined zettel with their purpose. |= Identifier :|= Title | Purpose | [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore | [[00000000000002]] | Zettelstore Host | Contains the name of the computer running the Zettelstore | [[00000000000003]] | Zettelstore Operating System | Contains the operating system and CPU architecture of the computer running the Zettelstore |
| ︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel | [[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 | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. | > > | 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | | [[00000000010200]] | Zettelstore Login Form HTML Template | Layout of the login form, when authentication is [[enabled|00001010040100]] | [[00000000010300]] | Zettelstore List Zettel HTML Template | Used when displaying a list of zettel | [[00000000010401]] | Zettelstore Detail HTML Template | Layout for the HTML detail view of one zettel | [[00000000010402]] | Zettelstore Info HTML Template | Layout for the information view of a specific zettel | [[00000000010403]] | Zettelstore Form HTML Template | Form that is used to create a new or to change an existing zettel that contains text | [[00000000010404]] | Zettelstore Rename Form HTML Template | View that is displayed to change the [[zettel identifier|00001006050000]] | [[00000000010405]] | Zettelstore Delete HTML Template | View to confirm the deletion of a zettel | [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message | [[00000000019100]] | Zettelstore Sxn Code for Templates | Some helper functions to build the templates | [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]] | [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file | [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid | [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu | [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]"" | [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]] | [[00010000000000]] | Home | Default home zettel, contains some welcome information If a zettel is not linked, it is not accessible for the current user. **Important:** All identifier may change until a stable version of the software is released. |
Changes to docs/manual/00001006020000.zettel.
1 2 3 4 5 6 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006020000 title: Supported Metadata Keys role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20230704161159 Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore. See the [[computed list of supported metadata keys|00000000000090]] for details. Most keys conform to a [[type|00001006030000]]. ; [!author|''author''] |
| ︙ | ︙ | |||
49 50 51 52 53 54 55 56 57 58 59 60 61 62 | 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. ; [!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. ; [!lang|''lang''] : Language for the zettel. | > > | 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | 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. ; [!lang|''lang''] : Language for the zettel. |
| ︙ | ︙ |
Changes to docs/manual/00001006030000.zettel.
1 2 3 4 5 6 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001006030000 title: Supported Key Types role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210126175322 modified: 20230612183742 All [[supported metadata keys|00001006020000]] conform to a type. User-defined metadata keys conform also to a type, based on the suffix of the key. |=Suffix|Type | ''-date'' | [[Timestamp|00001006034500]] |
| ︙ | ︙ | |||
22 23 24 25 26 27 28 | | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] | 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. | | | 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | | ''-zid'' | [[Identifier|00001006032000]] | ''-zids'' | [[IdentifierSet|00001006032500]] | 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. And there is a rule how values compare for sorting. * [[Credential|00001006031000]] * [[EString|00001006031500]] * [[Identifier|00001006032000]] * [[IdentifierSet|00001006032500]] * [[Number|00001006033000]] |
| ︙ | ︙ |
Changes to docs/manual/00001006032000.zettel.
1 2 3 4 5 6 | id: 00001006032000 title: Identifier Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | > | > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
id: 00001006032000
title: Identifier Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230612183459
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.
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.
|
Changes to docs/manual/00001006033000.zettel.
1 2 3 4 5 6 | id: 00001006033000 title: Number Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | > > > > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
id: 00001006033000
title: Number Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230612183900
Values of this type denote a numeric integer value.
=== Allowed values
Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character.
=== Query comparison
[[Search operators|00001007705000]] for equality (""equal"" or ""not equal"", ""has"" or ""not has""), for lesser values (""less"" or ""not less""), or for greater values (""greater"" or ""not greater"") are executed by converting both the [[search value|00001007706000]] and the metadata value into integer values and then comparing them numerically.
Integer values must be in the range -9223372036854775808 … 9223372036854775807.
Comparisons with metadata values outside this range always returns a negative match.
Comparisons with search values outside this range will be executed as a comparison of the string representation values.
All other comparisons (""match"", ""not match"", ""prefix"", ""not prefix"", ""suffix"", and ""not suffix"") are done on the given string representation of the number.
In this case, the number ""+12"" will be treated as different to the number ""12"".
=== Sorting
Sorting is done by comparing the numeric values.
|
Changes to docs/manual/00001006034500.zettel.
1 2 3 4 5 6 | id: 00001006034500 title: Timestamp Key Type role: manual tags: #manual #meta #reference #zettel #zettelstore syntax: zmk created: 20210212135017 | | > > > > > | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230612183509
Values of this type denote a point in time.
=== Allowed values
Must be a sequence of 14 digits (""0""--""9"") (same as an [[Identifier|00001006032000]]), with the restriction that is conforms to the pattern ""YYYYMMDDhhmmss"".
* YYYY is the year,
* MM is the month,
* DD is the day,
* hh is the hour,
* mm is the minute,
* ss is the second.
=== 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.
=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.
If both values are timestamp values, this works well because both have the same length.
|
Changes to docs/manual/00001007700000.zettel.
1 | id: 00001007700000 | | | | | < | < | < < < < < | < | < < < < < < < | < < | | < < < < < < < < < < < < < | | | > | < < < < < < < < < < < < < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
id: 00001007700000
title: Query Expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230731161954
A query expression allows you to search for specific zettel and to perform some actions on them.
You may select zettel based on a list of [[zettel identifier|00001006050000]], based on a query directive, based on a full-text search, based on specific metadata values, or some or all of them.
A query expression consists of an optional __[[zettel identifier list|00001007710000]]__, zero or more __[[query directives|00001007720000]]__, an optional __[[search expression|00001007701000]]__, and an optional __[[action list|00001007770000]]__.
The latter two are separated by a vertical bar character (""''|''"", U+007C).
A query expression follows a [[formal syntax|00001007780000]].
* [[List of zettel identifier|00001007710000]]
* [[Query directives|00001007720000]]
** [[Context directive|00001007720300]]
** [[Ident directive|00001007720600]]
** [[Items directive|00001007720900]]
** [[Unlinked directive|00001007721200]]
* [[Search expression|00001007701000]]
** [[Search term|00001007702000]]
** [[Search operator|00001007705000]]
** [[Search value|00001007706000]]
* [[Action list|00001007770000]]
Here are [[some examples|00001007790000]], which can be used to manage a Zettelstore:
{{{00001007790000}}}
|
Added docs/manual/00001007701000.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
id: 00001007701000
title: Query: Search Expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707205043
modified: 20230707210039
In its simplest form, a search expression just contains a string to be search for with the help of a full-text search.
For example, the string ''syntax'' will search for all zettel containing the word ""syntax"".
If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''.
""title"" denotes the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]].
The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match.
""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"".
A search expression may contain more than one search term, such as ''title:syntax''.
Search terms must be separated by one or more space characters, for example ''title:syntax title:search''.
All terms of a select expression must be true so that a zettel is selected.
Above sequence of search expressions may be combined by specifying the keyword ''OR''.
At most one of those sequences must be true so that a zettel is selected.
* [[Search term|00001007702000]]
* [[Search operator|00001007705000]]
* [[Search value|00001007706000]]
|
Changes to docs/manual/00001007702000.zettel.
1 2 3 4 5 6 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | id: 00001007702000 title: Search term role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 modified: 20230612180954 A search term allows you to specify one search restriction. The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions. A search term can be one of the following (the first three term are collectively called __search literals__): * A metadata-based search, by specifying the name of a [[metadata key|00001006010000]], followed by a [[search operator|00001007705000]], followed by an optional [[search value|00001007706000]]. All zettel containing the given metadata key with a allowed value (depending on the search operator) are selected. 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 ""''!?''"". |
| ︙ | ︙ |
Changes to docs/manual/00001007705000.zettel.
1 2 3 4 5 6 | id: 00001007705000 title: Search operator role: manual tags: #manual #search #zettelstore syntax: zmk created: 20220805150154 | | | | | | | > > | > | | | | | > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
id: 00001007705000
title: Search operator
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230612180539
A search operator specifies how the comparison of a search value and a zettel should be executed.
Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters.
The following are allowed search operator characters:
* The exclamation mark character (""''!''"", U+0021) negates the meaning.
* The equal sign character (""''=''"", U+003D) compares on equal content (""equals operator"").
* The tilde character (""''~''"", U+007E) compares on matching (""match operator"").
* The 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:
# ""''!''"": 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 metadata contains the given key.
# ""''!?''"": is successful if the metadata does not contain the given key.
# ""''''"": a missing search operator can only occur for a full-text search.
It is equal to the ""''~''"" operator.
|
Added docs/manual/00001007710000.zettel.
> > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | id: 20230707202600 title: Query: List of Zettel Identifier role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707202652 A query may start with a list of [[zettel identifier|00001006050000]], where the identifier are separated by one ore more space characters. If you specify at least one query directive, this list acts as a input for the first query directive. Otherwise, only the zettel of the given list are used to evaluated the search expression or the action list (if no search expression was given). Some examples: * [[query:00001007700000 CONTEXT]] returns the context of this zettel. * [[query:00001007700000 00001007031140 man]] searches the given two zettel for a string ""man"". * [[query:00001007700000 00001007031140 | tags]] return a tag cloud with tags from those two zettel. * [[query:00001007700000 00001007031140]] returns a list with the two zettel. |
Added docs/manual/00001007720000.zettel.
> > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | id: 00001007720000 title: Query Directives role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707203135 modified: 20230731162002 A query directive transforms a list of zettel identifier into a list of zettel identifiert. It is only valid if a list of zettel identifier is specified at the beginning of the query expression. Otherwise the text of the directive is interpreted as a search expression. For example, ''CONTEXT'' is interpreted as a full-text search for the word ""context"". Every query directive therefore consumes a list of zettel, and it produces a list of zettel according to the specific directive. * [[Context directive|00001007720300]] * [[Ident directive|00001007720600]] * [[Items directive|00001007720900]] * [[Unlinked directive|00001007721200]] |
Added docs/manual/00001007720300.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | id: 00001007720300 title: Query: Context Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707204706 modified: 20230724153832 A context directive calculates the __context__ of a list of zettel identifier. It starts with the keyword ''CONTEXT''. Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters. These are: * ''BACKWARD'': search for context only though backward links, * ''FORWARD'': search for context only through forward links, * ''COST'', one or more space characters, and a positive integer: set the maximum __cost__ (default: 17), * ''MAX'', one or more space characters, and a positive integer: set the maximum number of context zettel (default: 200). If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links. The cost of a context zettel is calculated iteratively: * Each of the specified zettel hast a cost of one. * A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus one. * A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus two. * A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus three. * A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the three choices above and multiplied with roughly a logarithmic value based on the size of the set. * A zettel with the same tag, has the cost of the originating zettel, plus the number of zettel with the same tag (if it is less than eight), or the cost of the originating zettel plus two, multiplied by number of zettel with the same tag divided by four. The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel. This ensures that initial zettel that have only a highly used tag, will also produce some context zettel. Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel. It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list. This directive may be specified only once as a query directive. A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]]. In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving power. |
Added docs/manual/00001007720600.zettel.
> > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 | id: 00001007720600 title: Query: Ident Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230724153018 modified: 20230724154015 An ident directive is needed if you want to specify just a list of zettel identifier. It starts with / consists of the keyword ''IDENT''. When not using the ident directive, zettel identifier are interpreted as a [[search expression|00001007701000]]. |
Added docs/manual/00001007720900.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | id: 00001007720900 title: Query: Items Directive role: manual tags: #manual #search #zettelstore syntax: zmk created: 00010101000000 modified: 20230729120755 The items directive works on zettel that act as a ""table of contents"" for other zettel. The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another. Every zettel with a certain internal structure can act as the ""table of contents"" for others. What is a ""table of contents""? Basically, it is just a list of references to other zettel. To retrieve the items of a zettel, the software looks at first level [[list items|00001007030200]]. If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the items list, in the ""table of contents"". This applies only to first level list items (ordered or unordered list), but not to deeper levels. Only the first reference to a valid zettel is collected for the table of contents. Following references to zettel within such an list item are ignored. ```` # curl 'http://127.0.0.1:23123/z?q=00001000000000+ITEMS' 00001001000000 Introduction to the Zettelstore 00001002000000 Design goals for the Zettelstore 00001003000000 Installation of the Zettelstore software 00001004000000 Configuration of Zettelstore 00001005000000 Structure of Zettelstore 00001006000000 Layout of a Zettel 00001007000000 Zettelmarkup 00001008000000 Other Markup Languages 00001010000000 Security 00001012000000 API 00001014000000 Web user interface 00001017000000 Tips and Tricks 00001018000000 Troubleshooting ```` |
Added docs/manual/00001007721200.zettel.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
id: 00001007721200
title: Query: Unlinked Directive
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20230731163343
The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.
__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel.
To retrieve unlinked references to an existing zettel, use the query ''{ID} UNLINKED''.
````
# curl 'http://127.0.0.1:23123/z?q=00001012000000+UNLINKED'
00001012921200 API: Encoding of Zettel Access Rights
````
This returns all zettel (in this case: only one) that references the title of the given Zettel, but does not references it directly.
In addition you may add __phrases__ if you do not want to scan for the title of the given zettel.
```
# curl 'http://localhost:23123/z?q=00001012054400+UNLINKED+PHRASE+API'
00001012050600 API: Provide an access token
00001012921200 API: Encoding of Zettel Access Rights
00001012080200 API: Check for authentication
00001012080500 API: Refresh internal data
00001012050200 API: Authenticate a client
00001010040700 Access token
```
This finds all zettel that does contain the phrase ""API"" but does not directly reference the given zettel.
The directive searches within all zettel whether the title of the specified zettel occurs there.
The other zettel must not link to the specified zettel.
The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting.
The match must be exact, but is case-insensitive.
For example ""API"" does not match ""API:"".
|
Added docs/manual/00001007770000.zettel.
> > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001007770000 title: Query: Action List role: manual tags: #manual #search #zettelstore syntax: zmk created: 20230707205246 modified: 20230707205532 With a [[list of zettel identifier|00001007710000]], a [[query directives|00001007720000]], or a [[search expression|00001007701000]], a list of zettel is selected. __Actions__ allow to modify this list to a certain degree. Which actions are allowed depends on the context. However, actions are further separated into __parameter action__ and __aggregate actions__. A parameter action just sets a parameter for an aggregate action. An aggregate action transforms the list of selected zettel into a different, aggregate form. Only the first aggregate form is executed, following aggregate actions are ignored. In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]]. |
Changes to docs/manual/00001007780000.zettel.
1 2 3 4 5 6 | id: 00001007780000 title: Formal syntax of query expressions role: manual tags: #manual #reference #search #zettelstore syntax: zmk created: 20220810144539 | | | > > > > > > > | > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20230731160413
```
QueryExpression := ZettelList? QueryDirective* SearchExpression ActionExpression?
ZettelList := (ZID (SPACE+ ZID)*).
ZID := '0'+ ('1' .. '9'') DIGIT*
| ('1' .. '9') DIGIT*.
QueryDirective := ContextDirective
| IdentDirective
| ItemsDirective
| UnlinkedDirective.
ContextDirective := "CONTEXT" (SPACE+ ContextDetail)*.
ContextDetail := "BACKWARD"
| "FORWARD"
| "COST" SPACE+ PosInt
| "MAX" SPACE+ PosInt.
IdentDirective := IDENT.
ItemsDirective := ITEMS.
UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*.
SearchExpression := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm := SearchOperator? SearchValue
| SearchKey SearchOperator SearchValue?
| SearchKey ExistOperator
| "OR"
| "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)*)?
Word := NO-SPACE NO-SPACE*
```
|
Changes to docs/manual/00001007790000.zettel.
1 2 3 4 5 6 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | id: 00001007790000 title: Useful query expressions role: manual tags: #example #manual #search #zettelstore syntax: zmk created: 20220810144539 modified: 20230706155134 |= Query Expression |= Meaning | [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore | [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel | [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel | [[query:PICK 40]] | 40 random zettel, ordered by zettel identifier | [[query:dead?]] | Zettel with invalid / dead links | [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel | [[query:tags!?]] | Zettel without tags | [[query:expire? ORDER expire]] | Zettel with an expire date, ordered from the nearest to the latest | [[query:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]] |
Changes to docs/manual/00001008000000.zettel.
| ︙ | ︙ | |||
45 46 47 48 49 50 51 | : 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. ; [!svg|''svg''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!sxn|''sxn''] | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | : 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. ; [!svg|''svg''] : [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]]. ; [!sxn|''sxn''] : S-Expressions, as implemented by [[sx|https://zettelstore.de/sx]]. Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]). ; [!text|''text''], [!plain|''plain''], [!txt|''txt''] : Plain text that must not be interpreted further. ; [!zmk|''zmk''] : [[Zettelmarkup|00001007000000]]. The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]]. |
| ︙ | ︙ |
Changes to docs/manual/00001012000000.zettel.
1 2 3 4 5 6 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012000000 title: API role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20230731162018 The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore. Most integration with other systems and services is done through the API. The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore. === Background 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. |
| ︙ | ︙ | |||
27 28 29 30 31 32 33 | === 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]] | < < | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | === 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]] * [[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]] |
Changes to docs/manual/00001012051200.zettel.
1 2 3 4 5 6 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | id: 00001012051200 title: API: List all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210126175322 modified: 20230703180113 To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header]. Always use the endpoint ''/z'' to work with a list of zettel. Without further specifications, a plain text document is returned, with one line per zettel. Each line contains in the first 14 characters the [[zettel identifier|00001006050000]]. Separated by a space character, the title of the zettel follows: |
| ︙ | ︙ | |||
23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
...
```
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.
Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'':
```sh
# curl 'http://127.0.0.1:23123/z?enc=json'
{"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]}
```
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
...
```
The list is **not** sorted, even in the these examples where it appears to be sorted.
If you want to have it ordered, you must specify it with the help of a [[query expression|00001007700000]] / [[search term|00001007702000]].
See [[Query the list of all zettel|00001012051400]] how to do it.
=== Data output
Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':
```sh
# curl 'http://127.0.0.1:23123/z?enc=data'
(meta-list (query "") (human "") (list (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ...
```
Pretty-printed, this results in:
```
(meta-list (query "")
(human "")
(list (zettel (id "00001012921200")
(meta (title "API: Encoding of Zettel Access Rights")
(role "manual")
(tags "#api #manual #reference #zettelstore")
(syntax "zmk")
(back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
(backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
(box-number "1")
(created "00010101000000")
(forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300")
(modified "20220201171959")
(published "20220201171959"))
(rights 62))
(zettel (id "00001007030100")
```
* The result is a list, starting with the symbol ''meta-list''.
* Then, some key/value pairs are following, also nested.
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
* ''list'' starts a list of zettel.
* ''zettel'' itself start, well, a zettel.
* ''id'' denotes the zettel identifier, encoded as a string.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
=== JSON output (deprecated)
Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'':
```sh
# curl 'http://127.0.0.1:23123/z?enc=json'
{"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]}
```
|
| ︙ | ︙ |
Changes to docs/manual/00001012051400.zettel.
1 2 3 4 5 6 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 | | > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | id: 00001012051400 title: API: Query the list of all zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20220912111111 modified: 20230731162234 precursor: 00001012051200 The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions. A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below). An empty search expression will select all zettel. An empty list of action, or no valid action, returns the list of all selected zettel metadata. |
| ︙ | ︙ | |||
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: JSON structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```
If you want to retrieve a JSON document:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=json'
{"query":"title:API ORDER REVERSE id OFFSET 1","human":"title HAS API ORDER REVERSE id OFFSET 1","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...]
```
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.
| > > > > > > > > > > > > > > > > > > > > > < < < > | 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: JSON 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.
where its value is a list of zettel JSON objects.
The symbol ''list'' starts the list of zettel data.
Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value.
Leading zeroes are removed.
Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value.
Metadata keys are encoded as a symbol, metadata values as a string.
''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel.
If you want to retrieve a JSON document:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=json'
{"query":"title:API ORDER REVERSE id OFFSET 1","human":"title HAS API ORDER REVERSE id OFFSET 1","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...]
```
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.
=== 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.
|
| ︙ | ︙ | |||
65 66 67 68 69 70 71 72 73 74 | 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 JSON object: ```sh | > > > > > > > > > > > > > > > > > > > > > > > > | < | | 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
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.
Of course, this list can also be returned as a JSON object:
```sh
# curl 'http://127.0.0.1:23123/z?q=|role&enc=json'
{"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 ''/z?q=|tags''.
If successful, the output is a JSON object:
```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=json'
{"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.
=== Actions
There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
__n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
|
| ︙ | ︙ |
Changes to docs/manual/00001012053200.zettel.
1 2 3 4 5 6 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 | | > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | id: 00001012053200 title: API: Create a new zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 modified: 20230807124310 A zettel is created by adding it to the [[list of zettel|00001012000000]]. Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request. 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. === JSON input (deprecated) Alternatively, the body of the POST request may contain a JSON object that specifies metadata and content of the zettel to be created. To do this, you must add the query parameter ''enc=json''. The following keys of the JSON object are used: ; ''"meta"'' : References an embedded JSON object with only string values. 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). |
| ︙ | ︙ |
Changes to docs/manual/00001012053300.zettel.
1 2 3 4 5 6 | id: 00001012053300 title: API: Retrieve metadata and content of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20211004093206 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807123533
The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
|
| ︙ | ︙ | |||
34 35 36 37 38 39 40 |
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
...
````
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
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
...
````
=== Data output
Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':
```sh
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=data&part=zettel'
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400") (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000") (box-number "1") (created "20211004093206") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500") (modified "20230703174152") (published "20230703174152") (role "manual") (syntax "zmk") (tags "#api #manual #zettelstore") (title "API: Retrieve metadata and content of an existing zettel")) (rights 62) (encoding "") (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```
If you print the result a little bit nicer, you will see its structure:
```
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400")
(backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000")
(box-number "1")
(created "20211004093206")
(forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500")
(modified "20230703174152")
(published "20230703174152")
(role "manual")
(syntax "zmk")
(tags "#api #manual #zettelstore")
(title "API: Retrieve metadata and content of an existing zettel"))
(rights 62)
(encoding "")
(content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```
* The result is a list, starting with the symbol ''zettel''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
* The zettel contents is stored as a value of the key ''content''.
Typically, text content is not encoded, and binary content is encoded via Base64.
=== JSON output (deprecated)
You may also retrieve the zettel as a JSON object by providing the query parameter ''enc=json'':
```sh
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=json&part=zettel'
{"id":"00001012053300","meta":{"back":"00001012000000 00001012054400","backward":"00001012000000 00001012054400 00001012920000","box-number":"1","created":"20211004093206","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200","modified":"20221219160211","published":"20221219160211","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata and content of an existing zettel"},"encoding":"","content":"The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```
Pretty-printed, this results in:
```
|
| ︙ | ︙ | |||
75 76 77 78 79 80 81 |
; ''"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]].
| < | 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
; ''"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]].
; ''"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.
=== HTTP Status codes
|
| ︙ | ︙ |
Changes to docs/manual/00001012053400.zettel.
1 2 3 4 5 6 | id: 00001012053400 title: API: Retrieve metadata of an existing zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210726174524 | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230703175153
The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''
````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.
=== JSON output (deprecated)
To return a JSON object, use also the query parameter ''enc=json''.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=json'
{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20230703174515","published":"20230703174515","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62}
```
Pretty-printed, this results in:
```
{
"meta": {
"back": "00001012000000 00001012053300",
|
| ︙ | ︙ |
Deleted docs/manual/00001012053900.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted docs/manual/00001012054000.zettel.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to docs/manual/00001012054200.zettel.
1 2 3 4 5 6 | id: 00001012054200 title: API: Update a zettel role: manual tags: #api #manual #zettelstore syntax: zmk created: 20210713150005 | | > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807124354
Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]].
In both cases you must provide the data for the new or updated zettel in the body of the HTTP request.
One difference is the endpoint.
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 POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```
=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].
The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.
=== JSON input (deprecated)
Alternatively, you can use the JSON encoding by using the query parameter ''enc=json''.
```
# curl -X PUT --data '{}' 'http://127.0.0.1:23123/z/00001012054200?enc=json'
```
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.
|
| ︙ | ︙ |
Changes to docs/manual/00001012070500.zettel.
1 2 3 4 5 | id: 00001012070500 title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk | > | | | < | | > | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | id: 00001012070500 title: Retrieve administrative data role: manual tags: #api #manual #zettelstore syntax: zmk created: 00010101000000 modified: 20230701160903 The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data. Currently, you can only request Zettelstore version data. ```` # curl 'http://127.0.0.1:23123/x' (0 13 0 "dev" "f781dc384b-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. === HTTP Status codes ; ''200'' : Retrieval was successful, the body contains an appropriate object. |
Changes to docs/manual/00001012920000.zettel.
1 2 3 4 5 6 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 | | < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | id: 00001012920000 title: Endpoints used by the API role: manual tags: #api #manual #reference #zettelstore syntax: zmk created: 20210126175322 modified: 20230731162343 All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where: ; ''PREFIX'' : is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]], ; ''LETTER'' : is a single letter that specifies the resource type, ; ''ZETTEL-ID'' : is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]]. 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 | | 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]] 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/"". |
Changes to docs/manual/00001012930500.zettel.
1 2 3 4 5 6 | id: 00001012930500 title: Syntax of Symbolic Expressions role: manual tags: #manual #reference #zettelstore syntax: zmk created: 20230403151127 | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
id: 00001012930500
title: Syntax of Symbolic Expressions
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403151127
modified: 20230703174218
=== Syntax of lists
A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Internally, lists are composed of __cells__.
A cell allows to store two values.
|
| ︙ | ︙ | |||
23 24 25 26 27 28 29 | ~~~draw +---+---+ +---+---+ +---+---+ | V | N +-->| V | N +--> -->| V | | +-+-+---+ +-+-+---+ +-+-+---+ | | | v v v | | | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | ~~~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 |
| ︙ | ︙ | |||
67 68 69 70 71 72 73 | 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 | | | 67 68 69 70 71 72 73 74 75 76 77 |
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://zettelstore.de/sx]] (""Symbolic eXPression Framework"") to implement symbolic expression.
The project page might contain additional information about the full syntax.
Zettelstore only uses lists, numbers, string, and symbols to represent zettel.
|
Changes to docs/readmezip.txt.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is 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. | | < | 13 14 15 16 17 18 19 20 | https://zettelstore.de/manual/. It is a live example of the zettelstore software, running in read-only mode. You can download it separately and it is 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. |
Changes to encoder/encoder.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | package encoder import ( "errors" "fmt" "io" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package encoder
import (
"errors"
"fmt"
"io"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/zettel/meta"
)
// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)
|
| ︙ | ︙ |
Changes to encoder/encoder_blob_test.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package encoder_test import ( "testing" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package encoder_test import ( "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" _ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser. |
| ︙ | ︙ |
Changes to encoder/encoder_test.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package encoder_test import ( "fmt" "strings" "testing" | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package encoder_test import ( "fmt" "strings" "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ | |||
107 108 109 110 111 112 113 |
t.Helper()
encdr := encoder.Create(encoderSz)
exp, err := pe.encode(encdr)
if err != nil {
t.Error(err)
return
}
| | | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
t.Helper()
encdr := encoder.Create(encoderSz)
exp, err := pe.encode(encdr)
if err != nil {
t.Error(err)
return
}
val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
if err != nil {
t.Error(err)
return
}
got := val.Repr()
if exp != got {
prefix := fmt.Sprintf("Test #%d", testNum)
|
| ︙ | ︙ |
Changes to encoder/htmlenc/htmlenc.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. package htmlenc import ( "io" "strings" | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // Package htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client. package htmlenc import ( "io" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/shtml" "zettelstore.de/sx.fossil" "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/szenc" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ | |||
52 53 54 55 56 57 58 |
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
hm, err := he.th.Transform(he.tx.GetMeta(zn.InhMeta, evalMeta))
if err != nil {
return 0, err
}
var isTitle ast.InlineSlice
| | | | | | | | | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
hm, err := he.th.Transform(he.tx.GetMeta(zn.InhMeta, evalMeta))
if err != nil {
return 0, err
}
var isTitle ast.InlineSlice
var htitle *sx.Pair
plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
if hasTitle {
isTitle = parser.ParseSpacedText(plainTitle)
xtitle := he.tx.GetSz(&isTitle)
htitle, err = he.th.Transform(xtitle)
if err != nil {
return 0, err
}
}
xast := he.tx.GetSz(&zn.Ast)
hast, err := he.th.Transform(xast)
if err != nil {
return 0, err
}
hen := he.th.Endnotes()
sf := he.th.SymbolFactory()
symAttr := sf.MustMake(sxhtml.NameSymAttr)
head := sx.MakeList(sf.MustMake("head"))
curr := head
curr = curr.AppendBang(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sf.MustMake("charset"), sx.MakeString("utf-8"))).Cons(symAttr)).Cons(sf.MustMake("meta")))
for elem := hm; elem != nil; elem = elem.Tail() {
curr = curr.AppendBang(elem.Car())
}
var sb strings.Builder
if hasTitle {
he.textEnc.WriteInlines(&sb, &isTitle)
} else {
sb.Write(zn.Meta.Zid.Bytes())
}
_ = curr.AppendBang(sx.Nil().Cons(sx.MakeString(sb.String())).Cons(sf.MustMake("title")))
body := sx.MakeList(sf.MustMake("body"))
curr = body
if hasTitle {
curr = curr.AppendBang(htitle.Cons(sf.MustMake("h1")))
}
for elem := hast; elem != nil; elem = elem.Tail() {
curr = curr.AppendBang(elem.Car())
}
if hen != nil {
curr = curr.AppendBang(sx.Nil().Cons(sf.MustMake("hr")))
_ = curr.AppendBang(hen)
}
doc := sx.MakeList(
sf.MustMake(sxhtml.NameSymDoctype),
sx.MakeList(sf.MustMake("html"), head, body),
)
gen := sxhtml.NewGenerator(sf, sxhtml.WithNewline)
return gen.WriteHTML(w, doc)
}
// WriteMeta encodes meta data as HTML5.
|
| ︙ | ︙ | |||
144 145 146 147 148 149 150 |
return 0, err
}
// WriteInlines writes an inline slice to the writer
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
hobj, err := he.th.Transform(he.tx.GetSz(is))
if err == nil {
| | | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
return 0, err
}
// WriteInlines writes an inline slice to the writer
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
hobj, err := he.th.Transform(he.tx.GetSz(is))
if err == nil {
gen := sxhtml.NewGenerator(sx.FindSymbolFactory(hobj))
length, err2 := gen.WriteListHTML(w, hobj)
if err2 != nil {
return length, err2
}
return length, nil
}
return 0, err
}
|
Changes to encoder/mdenc/mdenc.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package mdenc encodes the abstract syntax tree back into Markdown. package mdenc import ( "io" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Package mdenc encodes the abstract syntax tree back into Markdown.
package mdenc
import (
"io"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/zettel/meta"
)
func init() {
encoder.Register(api.EncoderMD, func() encoder.Encoder { return Create() })
|
| ︙ | ︙ |
Changes to encoder/shtmlenc/shtmlenc.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML. package shtmlenc import ( "io" | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// Package shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
package shtmlenc
import (
"io"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/shtml"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/encoder/szenc"
"zettelstore.de/z/zettel/meta"
)
func init() {
|
| ︙ | ︙ | |||
48 49 50 51 52 53 54 |
if err != nil {
return 0, err
}
contentSHTML, err := enc.th.Transform(enc.tx.GetSz(&zn.Ast))
if err != nil {
return 0, err
}
| | | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
if err != nil {
return 0, err
}
contentSHTML, err := enc.th.Transform(enc.tx.GetSz(&zn.Ast))
if err != nil {
return 0, err
}
result := sx.Cons(metaSHTML, contentSHTML)
return result.Print(w)
}
// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
metaSHTML, err := enc.th.Transform(enc.tx.GetMeta(m, evalMeta))
if err != nil {
|
| ︙ | ︙ |
Changes to encoder/szenc/szenc.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package szenc encodes the abstract syntax tree into a s-expr for zettel. package szenc import ( "io" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Package szenc encodes the abstract syntax tree into a s-expr for zettel.
package szenc
import (
"io"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/zettel/meta"
)
func init() {
encoder.Register(api.EncoderSz, func() encoder.Encoder { return Create() })
|
| ︙ | ︙ | |||
36 37 38 39 40 41 42 |
trans *Transformer
}
// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
content := enc.trans.GetSz(&zn.Ast)
meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
| | | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
trans *Transformer
}
// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
content := enc.trans.GetSz(&zn.Ast)
meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
return sx.MakeList(meta, content).Print(w)
}
// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
return enc.trans.GetMeta(m, evalMeta).Print(w)
}
|
| ︙ | ︙ |
Changes to encoder/szenc/transform.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package szenc import ( "encoding/base64" "fmt" "strings" | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
package szenc
import (
"encoding/base64"
"fmt"
"strings"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/client.fossil/sz"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/zettel/meta"
)
// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
sf := sx.MakeMappedFactory()
t := Transformer{sf: sf}
t.zetSyms.InitializeZettelSymbols(sf)
t.mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
ast.VerbatimZettel: t.zetSyms.SymVerbatimZettel,
ast.VerbatimProg: t.zetSyms.SymVerbatimProg,
ast.VerbatimEval: t.zetSyms.SymVerbatimEval,
ast.VerbatimMath: t.zetSyms.SymVerbatimMath,
ast.VerbatimComment: t.zetSyms.SymVerbatimComment,
ast.VerbatimHTML: t.zetSyms.SymVerbatimHTML,
}
t.mapRegionKindS = map[ast.RegionKind]*sx.Symbol{
ast.RegionSpan: t.zetSyms.SymRegionBlock,
ast.RegionQuote: t.zetSyms.SymRegionQuote,
ast.RegionVerse: t.zetSyms.SymRegionVerse,
}
t.mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{
ast.NestedListOrdered: t.zetSyms.SymListOrdered,
ast.NestedListUnordered: t.zetSyms.SymListUnordered,
ast.NestedListQuote: t.zetSyms.SymListQuote,
}
t.alignmentSymbolS = map[ast.Alignment]*sx.Symbol{
ast.AlignDefault: t.zetSyms.SymCell,
ast.AlignLeft: t.zetSyms.SymCellLeft,
ast.AlignCenter: t.zetSyms.SymCellCenter,
ast.AlignRight: t.zetSyms.SymCellRight,
}
t.mapRefStateLink = map[ast.RefState]*sx.Symbol{
ast.RefStateInvalid: t.zetSyms.SymLinkInvalid,
ast.RefStateZettel: t.zetSyms.SymLinkZettel,
ast.RefStateSelf: t.zetSyms.SymLinkSelf,
ast.RefStateFound: t.zetSyms.SymLinkFound,
ast.RefStateBroken: t.zetSyms.SymLinkBroken,
ast.RefStateHosted: t.zetSyms.SymLinkHosted,
ast.RefStateBased: t.zetSyms.SymLinkBased,
ast.RefStateQuery: t.zetSyms.SymLinkQuery,
ast.RefStateExternal: t.zetSyms.SymLinkExternal,
}
t.mapFormatKindS = map[ast.FormatKind]*sx.Symbol{
ast.FormatEmph: t.zetSyms.SymFormatEmph,
ast.FormatStrong: t.zetSyms.SymFormatStrong,
ast.FormatDelete: t.zetSyms.SymFormatDelete,
ast.FormatInsert: t.zetSyms.SymFormatInsert,
ast.FormatSuper: t.zetSyms.SymFormatSuper,
ast.FormatSub: t.zetSyms.SymFormatSub,
ast.FormatQuote: t.zetSyms.SymFormatQuote,
ast.FormatSpan: t.zetSyms.SymFormatSpan,
}
t.mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
ast.LiteralZettel: t.zetSyms.SymLiteralZettel,
ast.LiteralProg: t.zetSyms.SymLiteralProg,
ast.LiteralInput: t.zetSyms.SymLiteralInput,
ast.LiteralOutput: t.zetSyms.SymLiteralOutput,
ast.LiteralComment: t.zetSyms.SymLiteralComment,
ast.LiteralHTML: t.zetSyms.SymLiteralHTML,
ast.LiteralMath: t.zetSyms.SymLiteralMath,
}
t.mapRefStateS = map[ast.RefState]*sx.Symbol{
ast.RefStateInvalid: t.zetSyms.SymRefStateInvalid,
ast.RefStateZettel: t.zetSyms.SymRefStateZettel,
ast.RefStateSelf: t.zetSyms.SymRefStateSelf,
ast.RefStateFound: t.zetSyms.SymRefStateFound,
ast.RefStateBroken: t.zetSyms.SymRefStateBroken,
ast.RefStateHosted: t.zetSyms.SymRefStateHosted,
ast.RefStateBased: t.zetSyms.SymRefStateBased,
ast.RefStateQuery: t.zetSyms.SymRefStateQuery,
ast.RefStateExternal: t.zetSyms.SymRefStateExternal,
}
t.mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
meta.TypeCredential: t.zetSyms.SymTypeCredential,
meta.TypeEmpty: t.zetSyms.SymTypeEmpty,
meta.TypeID: t.zetSyms.SymTypeID,
meta.TypeIDSet: t.zetSyms.SymTypeIDSet,
meta.TypeNumber: t.zetSyms.SymTypeNumber,
meta.TypeString: t.zetSyms.SymTypeString,
meta.TypeTagSet: t.zetSyms.SymTypeTagSet,
meta.TypeTimestamp: t.zetSyms.SymTypeTimestamp,
meta.TypeURL: t.zetSyms.SymTypeURL,
meta.TypeWord: t.zetSyms.SymTypeWord,
meta.TypeWordSet: t.zetSyms.SymTypeWordSet,
meta.TypeZettelmarkup: t.zetSyms.SymTypeZettelmarkup,
}
return &t
}
type Transformer struct {
sf sx.SymbolFactory
zetSyms sz.ZettelSymbols
mapVerbatimKindS map[ast.VerbatimKind]*sx.Symbol
mapRegionKindS map[ast.RegionKind]*sx.Symbol
mapNestedListKindS map[ast.NestedListKind]*sx.Symbol
alignmentSymbolS map[ast.Alignment]*sx.Symbol
mapRefStateLink map[ast.RefState]*sx.Symbol
mapFormatKindS map[ast.FormatKind]*sx.Symbol
mapLiteralKindS map[ast.LiteralKind]*sx.Symbol
mapRefStateS map[ast.RefState]*sx.Symbol
mapMetaTypeS map[*meta.DescriptionType]*sx.Symbol
inVerse bool
}
func (t *Transformer) GetSz(node ast.Node) *sx.Pair {
switch n := node.(type) {
case *ast.BlockSlice:
return t.getBlockSlice(n)
case *ast.InlineSlice:
return t.getInlineSlice(*n)
case *ast.ParaNode:
return t.getInlineSlice(n.Inlines).Tail().Cons(t.zetSyms.SymPara)
case *ast.VerbatimNode:
return sx.MakeList(
mapGetS(t, t.mapVerbatimKindS, n.Kind),
t.getAttributes(n.Attrs),
sx.MakeString(string(n.Content)),
)
case *ast.RegionNode:
return t.getRegion(n)
case *ast.HeadingNode:
return sx.MakeList(
t.zetSyms.SymHeading,
sx.Int64(int64(n.Level)),
t.getAttributes(n.Attrs),
sx.MakeString(n.Slug),
sx.MakeString(n.Fragment),
t.getInlineSlice(n.Inlines),
)
case *ast.HRuleNode:
return sx.MakeList(t.zetSyms.SymThematic, t.getAttributes(n.Attrs))
case *ast.NestedListNode:
return t.getNestedList(n)
case *ast.DescriptionListNode:
return t.getDescriptionList(n)
case *ast.TableNode:
return t.getTable(n)
case *ast.TranscludeNode:
return sx.MakeList(t.zetSyms.SymTransclude, t.getAttributes(n.Attrs), t.getReference(n.Ref))
case *ast.BLOBNode:
return t.getBLOB(n)
case *ast.TextNode:
return sx.MakeList(t.zetSyms.SymText, sx.MakeString(n.Text))
case *ast.SpaceNode:
if t.inVerse {
return sx.MakeList(t.zetSyms.SymSpace, sx.MakeString(n.Lexeme))
}
return sx.MakeList(t.zetSyms.SymSpace)
case *ast.BreakNode:
if n.Hard {
return sx.MakeList(t.zetSyms.SymHard)
}
return sx.MakeList(t.zetSyms.SymSoft)
case *ast.LinkNode:
return t.getLink(n)
case *ast.EmbedRefNode:
return t.getInlineSlice(n.Inlines).Tail().
Cons(sx.MakeString(n.Syntax)).
Cons(t.getReference(n.Ref)).
Cons(t.getAttributes(n.Attrs)).
Cons(t.zetSyms.SymEmbed)
case *ast.EmbedBLOBNode:
return t.getEmbedBLOB(n)
case *ast.CiteNode:
return t.getInlineSlice(n.Inlines).Tail().
Cons(sx.MakeString(n.Key)).
Cons(t.getAttributes(n.Attrs)).
Cons(t.zetSyms.SymCite)
case *ast.FootnoteNode:
text := sx.Nil().Cons(sx.Nil().Cons(t.getInlineSlice(n.Inlines)).Cons(t.zetSyms.SymQuote))
return text.Cons(t.getAttributes(n.Attrs)).Cons(t.zetSyms.SymEndnote)
case *ast.MarkNode:
return t.getInlineSlice(n.Inlines).Tail().
Cons(sx.MakeString(n.Fragment)).
Cons(sx.MakeString(n.Slug)).
Cons(sx.MakeString(n.Mark)).
Cons(t.zetSyms.SymMark)
case *ast.FormatNode:
return t.getInlineSlice(n.Inlines).Tail().
Cons(t.getAttributes(n.Attrs)).
Cons(mapGetS(t, t.mapFormatKindS, n.Kind))
case *ast.LiteralNode:
return sx.MakeList(
mapGetS(t, t.mapLiteralKindS, n.Kind),
t.getAttributes(n.Attrs),
sx.MakeString(string(n.Content)),
)
}
return sx.MakeList(t.zetSyms.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}
func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair {
saveInVerse := t.inVerse
if rn.Kind == ast.RegionVerse {
t.inVerse = true
}
symBlocks := t.GetSz(&rn.Blocks)
t.inVerse = saveInVerse
return sx.MakeList(
mapGetS(t, t.mapRegionKindS, rn.Kind),
t.getAttributes(rn.Attrs),
symBlocks,
t.GetSz(&rn.Inlines),
)
}
func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair {
nlistObjs := make([]sx.Object, len(ln.Items)+1)
nlistObjs[0] = mapGetS(t, t.mapNestedListKindS, ln.Kind)
isCompact := isCompactList(ln.Items)
for i, item := range ln.Items {
if isCompact && len(item) > 0 {
paragraph := t.GetSz(item[0])
nlistObjs[i+1] = paragraph.Tail().Cons(t.zetSyms.SymInline)
continue
}
itemObjs := make([]sx.Object, len(item))
for j, in := range item {
itemObjs[j] = t.GetSz(in)
}
if isCompact {
nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymInline)
} else {
nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(t.zetSyms.SymBlock)
}
}
return sx.MakeList(nlistObjs...)
}
func isCompactList(itemSlice []ast.ItemSlice) bool {
for _, items := range itemSlice {
if len(items) > 1 {
return false
}
if len(items) == 1 {
if _, ok := items[0].(*ast.ParaNode); !ok {
return false
}
}
}
return true
}
func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair {
dlObjs := make([]sx.Object, 2*len(dn.Descriptions)+1)
dlObjs[0] = t.zetSyms.SymDescription
for i, def := range dn.Descriptions {
dlObjs[2*i+1] = t.getInlineSlice(def.Term)
descObjs := make([]sx.Object, len(def.Descriptions))
for j, b := range def.Descriptions {
dVal := make([]sx.Object, len(b))
for k, dn := range b {
dVal[k] = t.GetSz(dn)
}
descObjs[j] = sx.MakeList(dVal...).Cons(t.zetSyms.SymBlock)
}
dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(t.zetSyms.SymBlock)
}
return sx.MakeList(dlObjs...)
}
func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair {
tObjs := make([]sx.Object, len(tn.Rows)+2)
tObjs[0] = t.zetSyms.SymTable
tObjs[1] = t.getHeader(tn.Header)
for i, row := range tn.Rows {
tObjs[i+2] = t.getRow(row)
}
return sx.MakeList(tObjs...)
}
func (t *Transformer) getHeader(header ast.TableRow) *sx.Pair {
if len(header) == 0 {
return nil
}
return t.getRow(header)
}
func (t *Transformer) getRow(row ast.TableRow) *sx.Pair {
rObjs := make([]sx.Object, len(row))
for i, cell := range row {
rObjs[i] = t.getCell(cell)
}
return sx.MakeList(rObjs...).Cons(t.zetSyms.SymList)
}
func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
return t.getInlineSlice(cell.Inlines).Tail().Cons(mapGetS(t, t.alignmentSymbolS, cell.Align))
}
func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
var lastObj sx.Object
if bn.Syntax == meta.SyntaxSVG {
lastObj = sx.MakeString(string(bn.Blob))
} else {
lastObj = getBase64String(bn.Blob)
}
return sx.MakeList(
t.zetSyms.SymBLOB,
t.getInlineSlice(bn.Description),
sx.MakeString(bn.Syntax),
lastObj,
)
}
func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
return t.getInlineSlice(ln.Inlines).Tail().
Cons(sx.MakeString(ln.Ref.Value)).
Cons(t.getAttributes(ln.Attrs)).
Cons(mapGetS(t, t.mapRefStateLink, ln.Ref.State))
}
func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
tail := t.getInlineSlice(en.Inlines).Tail()
if en.Syntax == meta.SyntaxSVG {
tail = tail.Cons(sx.MakeString(string(en.Blob)))
} else {
tail = tail.Cons(getBase64String(en.Blob))
}
return tail.Cons(sx.MakeString(en.Syntax)).Cons(t.getAttributes(en.Attrs)).Cons(t.zetSyms.SymEmbedBLOB)
}
func (t *Transformer) getBlockSlice(bs *ast.BlockSlice) *sx.Pair {
objs := make([]sx.Object, len(*bs))
for i, n := range *bs {
objs[i] = t.GetSz(n)
}
return sx.MakeList(objs...).Cons(t.zetSyms.SymBlock)
}
func (t *Transformer) getInlineSlice(is ast.InlineSlice) *sx.Pair {
objs := make([]sx.Object, len(is))
for i, n := range is {
objs[i] = t.GetSz(n)
}
return sx.MakeList(objs...).Cons(t.zetSyms.SymInline)
}
func (t *Transformer) getAttributes(a attrs.Attributes) sx.Object {
if a.IsEmpty() {
return sx.Nil()
}
keys := a.Keys()
objs := make([]sx.Object, 0, len(keys))
for _, k := range keys {
objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
}
return sx.Nil().Cons(sx.MakeList(objs...)).Cons(t.zetSyms.SymQuote)
}
func (t *Transformer) getReference(ref *ast.Reference) *sx.Pair {
return sx.MakeList(
t.zetSyms.SymQuote,
sx.MakeList(
mapGetS(t, t.mapRefStateS, ref.State),
sx.MakeString(ref.Value),
),
)
}
func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
pairs := m.ComputedPairs()
objs := make([]sx.Object, 0, len(pairs))
for _, p := range pairs {
key := p.Key
ty := m.Type(key)
symType := mapGetS(t, t.mapMetaTypeS, ty)
var obj sx.Object
if ty.IsSet {
setList := meta.ListFromValue(p.Value)
setObjs := make([]sx.Object, len(setList))
for i, val := range setList {
setObjs[i] = sx.MakeString(val)
}
obj = sx.MakeList(setObjs...).Cons(t.zetSyms.SymList)
} else if ty == meta.TypeZettelmarkup {
is := evalMeta(p.Value)
obj = t.GetSz(&is)
} else {
obj = sx.MakeString(p.Value)
}
symKey := sx.MakeList(t.zetSyms.SymQuote, t.sf.MustMake(key))
objs = append(objs, sx.Nil().Cons(obj).Cons(symKey).Cons(symType))
}
return sx.MakeList(objs...).Cons(t.zetSyms.SymMeta)
}
func mapGetS[T comparable](t *Transformer, m map[T]*sx.Symbol, k T) *sx.Symbol {
if result, found := m[k]; found {
return result
}
return t.sf.MustMake(fmt.Sprintf("**%v:NOT-FOUND**", k))
}
func getBase64String(data []byte) sx.String {
var sb strings.Builder
encoder := base64.NewEncoder(base64.StdEncoding, &sb)
_, err := encoder.Write(data)
if err == nil {
err = encoder.Close()
}
if err == nil {
return sx.MakeString(sb.String())
}
return sx.MakeString("")
}
|
Changes to encoder/textenc/textenc.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package textenc encodes the abstract syntax tree into its text. package textenc import ( "io" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Package textenc encodes the abstract syntax tree into its text.
package textenc
import (
"io"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/zettel/meta"
)
func init() {
encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() })
|
| ︙ | ︙ |
Changes to encoder/zmkenc/zmkenc.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package zmkenc import ( "fmt" "io" "strings" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package zmkenc import ( "fmt" "io" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/encoder" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to encoding/atom/atom.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package atom provides an Atom encoding. package atom import ( "bytes" "time" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Package atom provides an Atom encoding. package atom import ( "bytes" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" |
| ︙ | ︙ |
Changes to encoding/encoding.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | // Package encoding provides helper functions for encodings. package encoding import ( "time" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// Package encoding provides helper functions for encodings.
package encoding
import (
"time"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/parser"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
|
| ︙ | ︙ |
Changes to encoding/rss/rss.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package rss import ( "bytes" "context" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package rss import ( "bytes" "context" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/encoding" "zettelstore.de/z/encoding/xml" "zettelstore.de/z/kernel" "zettelstore.de/z/query" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" |
| ︙ | ︙ |
Changes to evaluator/evaluator.go.
| ︙ | ︙ | |||
15 16 17 18 19 20 21 | "context" "errors" "fmt" "path" "strconv" "strings" | | | < | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
"context"
"errors"
"fmt"
"path"
"strconv"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/config"
"zettelstore.de/z/input"
"zettelstore.de/z/parser"
"zettelstore.de/z/parser/cleaner"
"zettelstore.de/z/parser/draw"
"zettelstore.de/z/query"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}
// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
if zn.Syntax == meta.SyntaxNone {
// AST is empty, evaluate to a description list of metadata.
|
| ︙ | ︙ | |||
251 252 253 254 255 256 257 |
e.transcludeCount += cost.ec
}
return &zn.Ast
}
func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
q := query.Parse(expr)
| | | 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
e.transcludeCount += cost.ec
}
return &zn.Ast
}
func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
q := query.Parse(expr)
ml, err := e.port.QueryMeta(e.ctx, q)
if err != nil {
if errors.Is(err, &box.ErrNotAllowed{}) {
return nil
}
return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
}
result := QueryAction(e.ctx, q, ml, e.rtConfig)
|
| ︙ | ︙ | |||
341 342 343 344 345 346 347 |
}
ref := ln.Ref
if ref == nil || ref.State != ast.RefStateZettel {
return ln
}
zid := mustParseZid(ref)
| | | 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 |
}
ref := ln.Ref
if ref == nil || ref.State != ast.RefStateZettel {
return ln
}
zid := mustParseZid(ref)
_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
if errors.Is(err, &box.ErrNotAllowed{}) {
return &ast.FormatNode{
Kind: ast.FormatSpan,
Attrs: ln.Attrs,
Inlines: getLinkInline(ln),
}
} else if err != nil {
|
| ︙ | ︙ |
Changes to evaluator/list.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "bytes" "context" "math" "sort" "strconv" "strings" | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "bytes" "context" "math" "sort" "strconv" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoding/atom" "zettelstore.de/z/encoding/rss" "zettelstore.de/z/parser" "zettelstore.de/z/query" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ |
Changes to go.mod.
1 2 | module zettelstore.de/z | | < < < | | | | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | module zettelstore.de/z go 1.20 require ( github.com/fsnotify/fsnotify v1.6.0 github.com/yuin/goldmark v1.5.5 golang.org/x/crypto v0.12.0 golang.org/x/term v0.11.0 golang.org/x/text v0.12.0 zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841 zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 ) require golang.org/x/sys v0.11.0 // indirect |
Changes to go.sum.
|
| < < < < < < | | | | | | | | | | | | > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841 h1:QlOIlT2cURqM2nYEvL7zOrjiE5/ZA3iAAfslcd6u2PY= zettelstore.de/client.fossil v0.0.0-20230807134407-92d8dd7df841/go.mod h1:MaVH7f0eHaWB5bK0GHNUiPJxKIYGyBk9amBUSbDZM0g= zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284 h1:26xwZWEjdyL3wObczc/PKugv0EY6mgSH5NUe5kYFCt4= zettelstore.de/sx.fossil v0.0.0-20230727172325-adec5a7ba284/go.mod h1:nsWXVrQG8RNKtoXzEMrWXNMdnpfIDU6Hb0pk56KpVKE= |
Changes to kernel/impl/cfg.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "context" "errors" "fmt" "strconv" "strings" "sync" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "context" "errors" "fmt" "strconv" "strings" "sync" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ | |||
143 144 145 146 147 148 149 |
func (cs *configService) setBox(mgr box.Manager) {
mgr.RegisterObserver(cs.observe)
cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}
func (cs *configService) doUpdate(p box.BaseBox) error {
| | > | 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
func (cs *configService) setBox(mgr box.Manager) {
mgr.RegisterObserver(cs.observe)
cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}
func (cs *configService) doUpdate(p box.BaseBox) error {
z, err := p.GetZettel(context.Background(), cs.orig.Zid)
cs.logger.Trace().Err(err).Msg("got config meta")
if err != nil {
return err
}
m := z.Meta
cs.mxService.Lock()
for _, pair := range cs.orig.Pairs() {
key := pair.Key
if val, ok := m.Get(key); ok {
cs.SetConfig(key, val)
} else if defVal, defFound := cs.orig.Get(key); defFound {
cs.SetConfig(key, defVal)
|
| ︙ | ︙ |
Changes to kernel/impl/cmd.go.
| ︙ | ︙ | |||
15 16 17 18 19 20 21 | "io" "os" "runtime/metrics" "sort" "strconv" "strings" | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
"io"
"os"
"runtime/metrics"
"sort"
"strconv"
"strings"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
"zettelstore.de/z/strfun"
)
type cmdSession struct {
w io.Writer
|
| ︙ | ︙ |
Changes to kernel/impl/config.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "errors" "fmt" "sort" "strconv" "strings" "sync" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
"zettelstore.de/z/zettel/id"
)
type parseFunc func(string) (any, error)
type configDescription struct {
|
| ︙ | ︙ |
Changes to kernel/impl/core.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "fmt" "net" "os" "runtime" "sync" "time" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
"fmt"
"net"
"os"
"runtime"
"sync"
"time"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/kernel"
"zettelstore.de/z/logger"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/id"
)
type coreService struct {
|
| ︙ | ︙ |
Changes to kernel/impl/impl.go.
| ︙ | ︙ | |||
125 126 127 128 129 130 131 |
func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout))
}
| | | 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout))
}
func (kern *myKernel) Start(headline, lineServer bool, configFilename string) {
for _, srvD := range kern.srvs {
srvD.srv.Freeze()
}
if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {
kern.SetLogLevel(defaultSimpleLogLevel.String())
}
kern.wg.Add(1)
|
| ︙ | ︙ | |||
156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
kern.core.GetCurConfig(kernel.CoreProgname),
kern.core.GetCurConfig(kernel.CoreVersion),
kern.core.GetCurConfig(kernel.CoreGoVersion),
kern.core.GetCurConfig(kernel.CoreGoOS),
kern.core.GetCurConfig(kernel.CoreGoArch),
))
logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)")
if kern.core.GetCurConfig(kernel.CoreDebug).(bool) {
logger.Warn().Msg("----------------------------------------")
logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
logger.Warn().Msg("----------------------------------------")
}
if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) {
logger.Info().Msg("Read-only mode")
| > > > > > | 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
kern.core.GetCurConfig(kernel.CoreProgname),
kern.core.GetCurConfig(kernel.CoreVersion),
kern.core.GetCurConfig(kernel.CoreGoVersion),
kern.core.GetCurConfig(kernel.CoreGoOS),
kern.core.GetCurConfig(kernel.CoreGoArch),
))
logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)")
if configFilename != "" {
logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found")
} else {
logger.Mandatory().Msg("No configuration file found / used")
}
if kern.core.GetCurConfig(kernel.CoreDebug).(bool) {
logger.Warn().Msg("----------------------------------------")
logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
logger.Warn().Msg("----------------------------------------")
}
if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) {
logger.Info().Msg("Read-only mode")
|
| ︙ | ︙ |
Changes to kernel/kernel.go.
| ︙ | ︙ | |||
27 28 29 30 31 32 33 |
// Kernel is the main internal service.
type Kernel interface {
// Setup sets the most basic data of a software: its name, its version,
// and when the version was created.
Setup(progname, version string, versionTime time.Time)
// Start the service.
| | | 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// Kernel is the main internal service.
type Kernel interface {
// Setup sets the most basic data of a software: its name, its version,
// and when the version was created.
Setup(progname, version string, versionTime time.Time)
// Start the service.
Start(headline bool, lineServer bool, configFile string)
// WaitForShutdown blocks the call until Shutdown is called.
WaitForShutdown()
// Shutdown the service. Waits for all concurrent activities to stop.
Shutdown(silent bool)
|
| ︙ | ︙ |
Changes to logger/message.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "context" "net/http" "strconv" "sync" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import (
"context"
"net/http"
"strconv"
"sync"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
)
// Message presents a message to log.
type Message struct {
logger *Logger
level Level
|
| ︙ | ︙ |
Changes to parser/draw/draw.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | // It is not a parser registered by the general parser framework (directed by // metadata "syntax" of a zettel). It will be used when a zettel is evaluated. package draw import ( "strconv" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// It is not a parser registered by the general parser framework (directed by
// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw
import (
"strconv"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/z/ast"
"zettelstore.de/z/input"
"zettelstore.de/z/parser"
"zettelstore.de/z/zettel/meta"
)
func init() {
|
| ︙ | ︙ |
Changes to parser/markdown/markdown.go.
| ︙ | ︙ | |||
17 18 19 20 21 22 23 | "strconv" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "strconv" "strings" gm "github.com/yuin/goldmark" gmAst "github.com/yuin/goldmark/ast" gmText "github.com/yuin/goldmark/text" "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/encoder/textenc" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to parser/parser.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package parser import ( "context" "fmt" "strings" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package parser import ( "context" "fmt" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser/cleaner" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to parser/plain/plain.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package plain provides a parser for plain text data. package plain import ( "bytes" "strings" | > | | < | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Package plain provides a parser for plain text data.
package plain
import (
"bytes"
"strings"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/sx.fossil/sxbuiltins/pprint"
"zettelstore.de/sx.fossil/sxbuiltins/quote"
"zettelstore.de/sx.fossil/sxreader"
"zettelstore.de/z/ast"
"zettelstore.de/z/input"
"zettelstore.de/z/parser"
"zettelstore.de/z/zettel/meta"
)
func init() {
|
| ︙ | ︙ | |||
132 133 134 135 136 137 138 |
return ""
}
// TODO: check proper end </svg>
return svgSrc
}
func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
| | | 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
return ""
}
// TODO: check proper end </svg>
return svgSrc
}
func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
sf := rd.SymbolFactory()
quote.InstallQuoteReader(rd, sf.MustMake("quote"), '\'')
quote.InstallQuasiQuoteReader(rd,
sf.MustMake("quasiquote"), '`',
sf.MustMake("unquote"), ',',
sf.MustMake("unquote-splicing"), '@')
objs, err := rd.ReadAll()
|
| ︙ | ︙ |
Changes to parser/zettelmark/inline.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package zettelmark import ( "bytes" "fmt" "strings" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package zettelmark
import (
"bytes"
"fmt"
"strings"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/z/ast"
"zettelstore.de/z/input"
"zettelstore.de/z/zettel/meta"
)
// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
|
| ︙ | ︙ |
Changes to parser/zettelmark/zettelmark.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | // Package zettelmark provides a parser for zettelmarkup. package zettelmark import ( "strings" "unicode" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// Package zettelmark provides a parser for zettelmarkup.
package zettelmark
import (
"strings"
"unicode"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/z/ast"
"zettelstore.de/z/input"
"zettelstore.de/z/parser"
"zettelstore.de/z/zettel/meta"
)
func init() {
|
| ︙ | ︙ |
Changes to parser/zettelmark/zettelmark_test.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package zettelmark_test import ( "fmt" "strings" "testing" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package zettelmark_test import ( "fmt" "strings" "testing" "zettelstore.de/client.fossil/attrs" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/input" "zettelstore.de/z/parser" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to query/compiled.go.
| ︙ | ︙ | |||
23 24 25 26 27 28 29 | hasQuery bool seed int pick int order []sortOrder offset int // <= 0: no offset limit int // <= 0: no limit | | | | | 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
hasQuery bool
seed int
pick int
order []sortOrder
offset int // <= 0: no offset
limit int // <= 0: no limit
startMeta []*meta.Meta
PreMatch MetaMatchFunc // Precondition for Match and Retrieve
Terms []CompiledTerm
}
// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
type MetaMatchFunc func(*meta.Meta) bool
func matchAlways(*meta.Meta) bool { return true }
func matchNever(*meta.Meta) bool { return false }
|
| ︙ | ︙ | |||
47 48 49 50 51 52 53 |
// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool
func (c *Compiled) isDeterministic() bool { return c.seed > 0 }
// Result returns a result of the compiled search, that is achievable without iterating through a box.
func (c *Compiled) Result() []*meta.Meta {
| | | | | | | | 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool
func (c *Compiled) isDeterministic() bool { return c.seed > 0 }
// Result returns a result of the compiled search, that is achievable without iterating through a box.
func (c *Compiled) Result() []*meta.Meta {
if len(c.startMeta) == 0 {
// nil -> no directive
// empty slice -> nothing found
return c.startMeta
}
result := make([]*meta.Meta, 0, len(c.startMeta))
for _, m := range c.startMeta {
for _, term := range c.Terms {
if term.Match(m) && term.Retrieve(m.Zid) {
result = append(result, m)
break
}
}
}
result = c.pickElements(result)
result = c.sortElements(result)
|
| ︙ | ︙ |
Changes to query/context.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package query import ( "context" | | > > > > > > > > | | | | < > > | < < > | < < > > > | > > > > > > < < > > | > > | | | | | | < | < | | | | | | | < < < < < < < | < | | | | | | > > > > > > > > > > > > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
//-----------------------------------------------------------------------------
package query
import (
"context"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
Direction ContextDirection
MaxCost int
MaxCount int
}
// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8
const (
ContextDirBoth ContextDirection = iota
ContextDirForward
ContextDirBackward
)
// ContextPort is the collection of box methods needed by this directive.
type ContextPort interface {
GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error)
}
func (spec *ContextSpec) Print(pe *PrintEnv) {
pe.printSpace()
pe.writeString(api.ContextDirective)
switch spec.Direction {
case ContextDirBackward:
pe.printSpace()
pe.writeString(api.BackwardDirective)
case ContextDirForward:
pe.printSpace()
pe.writeString(api.ForwardDirective)
}
pe.printPosInt(api.CostDirective, spec.MaxCost)
pe.printPosInt(api.MaxDirective, spec.MaxCount)
}
func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta {
maxCost := spec.MaxCost
if maxCost <= 0 {
maxCost = 17
}
maxCount := spec.MaxCount
if maxCount <= 0 {
maxCount = 200
}
tasks := newQueue(startSeq, maxCost, maxCount, port)
isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward
isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward
result := []*meta.Meta{}
for {
m, cost := tasks.next()
if m == nil {
break
}
result = append(result, m)
for _, p := range m.ComputedPairsRest() {
tasks.addPair(ctx, p.Key, p.Value, cost, isBackward, isForward)
}
if tags, found := m.GetList(api.KeyTags); found {
for _, tag := range tags {
tasks.addSameTag(ctx, tag, cost)
}
}
}
return result
}
type ztlCtxTask struct {
next *ztlCtxTask
prev *ztlCtxTask
meta *meta.Meta
cost int
}
type contextQueue struct {
port ContextPort
seen id.Set
first *ztlCtxTask
last *ztlCtxTask
maxCost int
limit int
tagCost map[string][]*meta.Meta
}
func newQueue(startSeq []*meta.Meta, maxCost, limit int, port ContextPort) *contextQueue {
result := &contextQueue{
port: port,
seen: id.NewSet(),
first: nil,
last: nil,
maxCost: maxCost,
limit: limit,
tagCost: make(map[string][]*meta.Meta, 1024),
}
var prev *ztlCtxTask
for _, m := range startSeq {
task := &ztlCtxTask{next: nil, prev: prev, meta: m, cost: 1}
if prev == nil {
result.first = task
} else {
prev.next = task
}
result.last = task
prev = task
}
return result
}
func (zc *contextQueue) addPair(ctx context.Context, key, value string, curCost int, isBackward, isForward bool) {
if key == api.KeyBack {
return
|
| ︙ | ︙ | |||
149 150 151 152 153 154 155 |
return 3
}
func (zc *contextQueue) addID(ctx context.Context, newCost int, value string) {
if zc.costMaxed(newCost) {
return
}
| < | < < < | < | | < < | 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
return 3
}
func (zc *contextQueue) addID(ctx context.Context, newCost int, value string) {
if zc.costMaxed(newCost) {
return
}
if zid, errParse := id.Parse(value); errParse == nil {
if m, errGetMeta := zc.port.GetMeta(ctx, zid); errGetMeta == nil {
zc.addMeta(m, newCost)
}
}
}
func (zc *contextQueue) addMeta(m *meta.Meta, newCost int) {
task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost}
if zc.first == nil {
zc.first = task
zc.last = task
|
| ︙ | ︙ | |||
194 195 196 197 198 199 200 |
// We have not found a task, therefore the new task is the first one
task.next = zc.first
zc.first.prev = task
zc.first = task
}
func (zc *contextQueue) costMaxed(newCost int) bool {
| > > > | | 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
// We have not found a task, therefore the new task is the first one
task.next = zc.first
zc.first.prev = task
zc.first = task
}
func (zc *contextQueue) costMaxed(newCost int) bool {
// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
// other zettel that are directly reachable, without taking the cost into account.
// Of course, the limit ist still relevant.
return (len(zc.seen) > 1 && zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit()
}
func (zc *contextQueue) addIDSet(ctx context.Context, newCost int, value string) {
elems := meta.ListFromValue(value)
refCost := referenceCost(newCost, len(elems))
for _, val := range elems {
zc.addID(ctx, refCost, val)
|
| ︙ | ︙ | |||
228 229 230 231 232 233 234 |
return baseCost * numReferences / 8
}
func (zc *contextQueue) addSameTag(ctx context.Context, tag string, baseCost int) {
tagMetas, found := zc.tagCost[tag]
if !found {
q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
| | < | < | 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
return baseCost * numReferences / 8
}
func (zc *contextQueue) addSameTag(ctx context.Context, tag string, baseCost int) {
tagMetas, found := zc.tagCost[tag]
if !found {
q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
ml, err := zc.port.SelectMeta(ctx, nil, q)
if err != nil {
return
}
tagMetas = ml
zc.tagCost[tag] = ml
}
cost := tagCost(baseCost, len(tagMetas))
if zc.costMaxed(cost) {
return
}
for _, m := range tagMetas {
zc.addMeta(m, cost)
}
}
func tagCost(baseCost, numTags int) int {
if numTags < 8 {
return baseCost + numTags/2
}
|
| ︙ | ︙ | |||
279 280 281 282 283 284 285 |
return m, task.cost
}
return nil, -1
}
func (zc *contextQueue) hasLimit() bool {
limit := zc.limit
| | | 292 293 294 295 296 297 298 299 300 |
return m, task.cost
}
return nil, -1
}
func (zc *contextQueue) hasLimit() bool {
limit := zc.limit
return limit > 0 && len(zc.seen) >= limit
}
|
Changes to query/parser.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package query import ( "strconv" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package query
import (
"strconv"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/input"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Parse the query specification and return a Query object.
func Parse(spec string) (q *Query) { return q.Parse(spec) }
|
| ︙ | ︙ | |||
52 53 54 55 56 57 58 | ps.skipSpace() return true } return false } const ( | | | | | | | | | | < < | < < < < < < < < < > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | < < < | < < < | < | | | | | | | | | | < | | < | > > | | | | | | | | | > | > > | | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > | | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 |
ps.skipSpace()
return true
}
return false
}
const (
actionSeparatorChar = '|'
existOperatorChar = '?'
searchOperatorNotChar = '!'
searchOperatorEqualChar = '='
searchOperatorHasChar = ':'
searchOperatorPrefixChar = '['
searchOperatorSuffixChar = ']'
searchOperatorMatchChar = '~'
searchOperatorLessChar = '<'
searchOperatorGreaterChar = '>'
)
func (ps *parserState) parse(q *Query) *Query {
ps.skipSpace()
if ps.mustStop() {
return q
}
inp := ps.inp
firstPos := inp.Pos
zidSet := id.NewSet()
for {
pos := inp.Pos
zid, found := ps.scanZid()
if !found {
inp.SetPos(pos)
break
}
if !zidSet.Contains(zid) {
zidSet.Zid(zid)
q = createIfNeeded(q)
q.zids = append(q.zids, zid)
}
ps.skipSpace()
if ps.mustStop() {
q.zids = nil
break
}
}
hasContext := false
for {
ps.skipSpace()
if ps.mustStop() {
break
}
pos := inp.Pos
if ps.acceptSingleKw(api.ContextDirective) {
if hasContext {
inp.SetPos(pos)
break
}
q = ps.parseContext(q, pos)
hasContext = true
continue
}
inp.SetPos(pos)
if q == nil || len(q.zids) == 0 {
break
}
if ps.acceptSingleKw(api.IdentDirective) {
q.directives = append(q.directives, &IdentSpec{})
continue
}
inp.SetPos(pos)
if ps.acceptSingleKw(api.ItemsDirective) {
q.directives = append(q.directives, &ItemsSpec{})
continue
}
inp.SetPos(pos)
if ps.acceptSingleKw(api.UnlinkedDirective) {
q = ps.parseUnlinked(q)
continue
}
inp.SetPos(pos)
break
}
if q != nil && len(q.directives) == 0 {
inp.SetPos(firstPos) // No directive -> restart at beginning
q.zids = nil
}
for {
ps.skipSpace()
if ps.mustStop() {
break
}
pos := inp.Pos
if ps.acceptSingleKw(api.OrDirective) {
q = createIfNeeded(q)
if !q.terms[len(q.terms)-1].isEmpty() {
q.terms = append(q.terms, conjTerms{})
}
continue
}
inp.SetPos(pos)
if ps.acceptSingleKw(api.RandomDirective) {
q = createIfNeeded(q)
if len(q.order) == 0 {
q.order = []sortOrder{{"", false}}
}
continue
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.PickDirective) {
if s, ok := ps.parsePick(q); ok {
q = s
continue
}
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.OrderDirective) {
if s, ok := ps.parseOrder(q); ok {
q = s
continue
}
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.OffsetDirective) {
if s, ok := ps.parseOffset(q); ok {
q = s
continue
}
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.LimitDirective) {
if s, ok := ps.parseLimit(q); ok {
q = s
continue
}
}
inp.SetPos(pos)
if isActionSep(inp.Ch) {
q = ps.parseActions(q)
break
}
q = ps.parseText(q)
}
return q
}
func (ps *parserState) parseContext(q *Query, pos int) *Query {
inp := ps.inp
if q == nil || len(q.zids) == 0 {
ps.skipSpace()
if ps.mustStop() {
inp.SetPos(pos)
return q
}
zid, ok := ps.scanZid()
if !ok {
inp.SetPos(pos)
return q
}
q = createIfNeeded(q)
q.zids = append(q.zids, zid)
}
spec := &ContextSpec{}
for {
ps.skipSpace()
if ps.mustStop() {
break
}
pos = inp.Pos
if ps.acceptSingleKw(api.BackwardDirective) {
spec.Direction = ContextDirBackward
continue
}
inp.SetPos(pos)
if ps.acceptSingleKw(api.ForwardDirective) {
spec.Direction = ContextDirForward
continue
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.CostDirective) {
if ps.parseCost(spec) {
continue
}
}
inp.SetPos(pos)
if ps.acceptKwArgs(api.MaxDirective) {
if ps.parseCount(spec) {
continue
}
}
inp.SetPos(pos)
break
}
q.directives = append(q.directives, spec)
return q
}
func (ps *parserState) parseCost(spec *ContextSpec) bool {
num, ok := ps.scanPosInt()
if !ok {
return false
}
if spec.MaxCost == 0 || spec.MaxCost >= num {
spec.MaxCost = num
}
return true
}
func (ps *parserState) parseCount(spec *ContextSpec) bool {
num, ok := ps.scanPosInt()
if !ok {
return false
}
if spec.MaxCount == 0 || spec.MaxCount >= num {
spec.MaxCount = num
}
return true
}
func (ps *parserState) parseUnlinked(q *Query) *Query {
inp := ps.inp
spec := &UnlinkedSpec{}
for {
ps.skipSpace()
if ps.mustStop() {
break
}
pos := inp.Pos
if ps.acceptKwArgs(api.PhraseDirective) {
if word := ps.scanWord(); len(word) > 0 {
spec.words = append(spec.words, string(word))
continue
}
}
inp.SetPos(pos)
break
}
q.directives = append(q.directives, spec)
return q
}
func (ps *parserState) parsePick(q *Query) (*Query, bool) {
num, ok := ps.scanPosInt()
if !ok {
return q, false
}
q = createIfNeeded(q)
if q.pick == 0 || q.pick >= num {
q.pick = num
}
return q, true
}
func (ps *parserState) parseOrder(q *Query) (*Query, bool) {
reverse := false
if ps.acceptKwArgs(api.ReverseDirective) {
reverse = true
}
word := ps.scanWord()
if len(word) == 0 {
return q, false
}
if sWord := string(word); meta.KeyIsValid(sWord) {
|
| ︙ | ︙ | |||
346 347 348 349 350 351 352 |
allowKey := !hasOp
for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
if allowKey {
switch inp.Ch {
case searchOperatorNotChar, existOperatorChar,
searchOperatorEqualChar, searchOperatorHasChar,
| | > | 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 |
allowKey := !hasOp
for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
if allowKey {
switch inp.Ch {
case searchOperatorNotChar, existOperatorChar,
searchOperatorEqualChar, searchOperatorHasChar,
searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
searchOperatorLessChar, searchOperatorGreaterChar:
allowKey = false
if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
return nil, key
}
}
}
inp.Next()
|
| ︙ | ︙ | |||
420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
op = cmpSuffix
case searchOperatorPrefixChar:
inp.Next()
op = cmpPrefix
case searchOperatorMatchChar:
inp.Next()
op = cmpMatch
default:
if negate {
return cmpNoMatch, true
}
return cmpUnknown, false
}
if negate {
| > > > > > > | 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 |
op = cmpSuffix
case searchOperatorPrefixChar:
inp.Next()
op = cmpPrefix
case searchOperatorMatchChar:
inp.Next()
op = cmpMatch
case searchOperatorLessChar:
inp.Next()
op = cmpLess
case searchOperatorGreaterChar:
inp.Next()
op = cmpGreater
default:
if negate {
return cmpNoMatch, true
}
return cmpUnknown, false
}
if negate {
|
| ︙ | ︙ |
Changes to query/parser_test.go.
| ︙ | ︙ | |||
18 19 20 21 22 23 24 |
func TestParser(t *testing.T) {
t.Parallel()
testcases := []struct {
spec string
exp string
}{
| > > > > > > > > | > > > > > > > > > > | > > > > | | | | | | | > > > > > > | | | | | > > | | > > | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
func TestParser(t *testing.T) {
t.Parallel()
testcases := []struct {
spec string
exp string
}{
{"1", "1"}, // Just a number will transform to search for that numer in all zettel
{"1 IDENT", "00000000000001 IDENT"},
{"IDENT", "IDENT"},
{"1 ITEMS", "00000000000001 ITEMS"},
{"ITEMS", "ITEMS"},
{"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"},
{"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"},
{"00000000000001 CONTEXT", "00000000000001 CONTEXT"},
{"100000000000001 CONTEXT", "100000000000001 CONTEXT"},
{"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"},
{"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"},
{"1 CONTEXT COST ", "00000000000001 CONTEXT COST"},
{"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"},
{"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"},
{"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
{"1 CONTEXT | N", "00000000000001 CONTEXT | N"},
{"1 1 CONTEXT", "00000000000001 CONTEXT"},
{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},
{"CONTEXT 0", "CONTEXT 0"}, {"CONTEXT 1", "00000000000001 CONTEXT"},
{"CONTEXT 00000000000001", "00000000000001 CONTEXT"},
{"CONTEXT 100000000000001", "CONTEXT 100000000000001"},
{"CONTEXT 1 BACKWARD", "00000000000001 CONTEXT BACKWARD"},
{"CONTEXT 1 FORWARD", "00000000000001 CONTEXT FORWARD"},
{"CONTEXT 1 COST 3", "00000000000001 CONTEXT COST 3"}, {"CONTEXT 1 COST x", "00000000000001 CONTEXT COST x"},
{"CONTEXT 1 MAX 5", "00000000000001 CONTEXT MAX 5"}, {"CONTEXT 1 MAX y", "00000000000001 CONTEXT MAX y"},
{"CONTEXT 1 MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
{"CONTEXT 1 | N", "00000000000001 CONTEXT | N"},
{"1 UNLINKED", "00000000000001 UNLINKED"},
{"UNLINKED", "UNLINKED"},
{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},
{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},
{"key?", "key?"}, {"key!?", "key!?"},
{"b key?", "key? b"}, {"b key!?", "key!? b"},
{"key?a", "key?a"}, {"key!?a", "key!?a"},
{"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {"[", ""}, {"![", ""}, {"]", ""}, {"!]", ""}, {"~", ""}, {"!~", ""}, {"<", ""}, {"!<", ""}, {">", ""}, {"!>", ""},
{`a`, `a`}, {`!a`, `!a`},
{`=a`, `=a`}, {`!=a`, `!=a`},
{`:a`, `:a`}, {`!:a`, `!:a`},
{`[a`, `[a`}, {`![a`, `![a`},
{`]a`, `]a`}, {`!]a`, `!]a`},
{`~a`, `a`}, {`!~a`, `!a`},
{`key=`, `key=`}, {`key!=`, `key!=`},
{`key:`, `key:`}, {`key!:`, `key!:`},
{`key[`, `key[`}, {`key![`, `key![`},
{`key]`, `key]`}, {`key!]`, `key!]`},
{`key~`, `key~`}, {`key!~`, `key!~`},
{`key<`, `key<`}, {`key!<`, `key!<`},
{`key>`, `key>`}, {`key!>`, `key!>`},
{`key=a`, `key=a`}, {`key!=a`, `key!=a`},
{`key:a`, `key:a`}, {`key!:a`, `key!:a`},
{`key[a`, `key[a`}, {`key![a`, `key![a`},
{`key]a`, `key]a`}, {`key!]a`, `key!]a`},
{`key~a`, `key~a`}, {`key!~a`, `key!~a`},
{`key<a`, `key<a`}, {`key!<a`, `key!<a`},
{`key>a`, `key>a`}, {`key!>a`, `key!>a`},
{`key1:a key2:b`, `key1:a key2:b`},
{`key1: key2:b`, `key1: key2:b`},
{"word key:a", "key:a word"},
{`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`},
{"PICK a", "PICK a"},
{`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`},
{`RANDOM RANDOM a`, `a RANDOM`},
|
| ︙ | ︙ |
Changes to query/print.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package query import ( "io" "strconv" "strings" | | > | | | | | | | | | | | | | > > > > | | > > > < < | < | | | > | | | | > > > | > | > | < < < < < | < < < < < < | > | < | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
package query
import (
"io"
"strconv"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/zettel/id"
)
var op2string = map[compareOp]string{
cmpExist: api.ExistOperator,
cmpNotExist: api.ExistNotOperator,
cmpEqual: api.SearchOperatorEqual,
cmpNotEqual: api.SearchOperatorNotEqual,
cmpHas: api.SearchOperatorHas,
cmpHasNot: api.SearchOperatorHasNot,
cmpPrefix: api.SearchOperatorPrefix,
cmpNoPrefix: api.SearchOperatorNoPrefix,
cmpSuffix: api.SearchOperatorSuffix,
cmpNoSuffix: api.SearchOperatorNoSuffix,
cmpMatch: api.SearchOperatorMatch,
cmpNoMatch: api.SearchOperatorNoMatch,
cmpLess: api.SearchOperatorLess,
cmpNoLess: api.SearchOperatorNotLess,
cmpGreater: api.SearchOperatorGreater,
cmpNoGreater: api.SearchOperatorNotGreater,
}
func (q *Query) String() string {
var sb strings.Builder
q.Print(&sb)
return sb.String()
}
// Print the query in a parseable form.
func (q *Query) Print(w io.Writer) {
if q == nil {
return
}
env := PrintEnv{w: w}
env.printZids(q.zids)
for _, d := range q.directives {
d.Print(&env)
}
for i, term := range q.terms {
if i > 0 {
env.writeString(" OR")
}
for _, name := range maps.Keys(term.keys) {
env.printSpace()
env.writeString(name)
if op := term.keys[name]; op == cmpExist || op == cmpNotExist {
env.writeString(op2string[op])
} else {
env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator)
}
}
for _, name := range maps.Keys(term.mvals) {
env.printExprValues(name, term.mvals[name])
}
if len(term.search) > 0 {
env.printExprValues("", term.search)
}
}
env.printPosInt(api.PickDirective, q.pick)
env.printOrder(q.order)
env.printPosInt(api.OffsetDirective, q.offset)
env.printPosInt(api.LimitDirective, q.limit)
env.printActions(q.actions)
}
// PrintEnv is an environment where queries are printed.
type PrintEnv struct {
w io.Writer
space bool
}
var bsSpace = []byte{' '}
func (pe *PrintEnv) printSpace() {
if pe.space {
pe.w.Write(bsSpace)
return
}
pe.space = true
}
func (pe *PrintEnv) write(ch byte) { pe.w.Write([]byte{ch}) }
func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) }
func (pe *PrintEnv) writeStrings(sSeq ...string) {
for _, s := range sSeq {
io.WriteString(pe.w, s)
}
}
func (pe *PrintEnv) printZids(zids []id.Zid) {
for i, zid := range zids {
if i > 0 {
pe.printSpace()
}
pe.writeString(zid.String())
pe.space = true
}
}
func (pe *PrintEnv) printExprValues(key string, values []expValue) {
for _, val := range values {
pe.printSpace()
pe.writeString(key)
switch op := val.op; op {
case cmpMatch:
// An empty key signals a full-text search. Since "~" is the default op in this case,
// it can be ignored. Therefore, print only "~" if there is a key.
|
| ︙ | ︙ | |||
153 154 155 156 157 158 159 |
}
// PrintHuman the query to a writer in a human readable form.
func (q *Query) PrintHuman(w io.Writer) {
if q == nil {
return
}
| | | > > > | 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
}
// PrintHuman the query to a writer in a human readable form.
func (q *Query) PrintHuman(w io.Writer) {
if q == nil {
return
}
env := PrintEnv{w: w}
env.printZids(q.zids)
for _, d := range q.directives {
d.Print(&env)
}
for i, term := range q.terms {
if i > 0 {
env.writeString(" OR ")
env.space = false
}
for _, name := range maps.Keys(term.keys) {
if env.space {
|
| ︙ | ︙ | |||
193 194 195 196 197 198 199 |
}
env.writeString("ANY")
env.printHumanSelectExprValues(term.search)
env.space = true
}
}
| | | | | | 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
}
env.writeString("ANY")
env.printHumanSelectExprValues(term.search)
env.space = true
}
}
env.printPosInt(api.PickDirective, q.pick)
env.printOrder(q.order)
env.printPosInt(api.OffsetDirective, q.offset)
env.printPosInt(api.LimitDirective, q.limit)
env.printActions(q.actions)
}
func (pe *PrintEnv) printHumanSelectExprValues(values []expValue) {
if len(values) == 0 {
pe.writeString(" MATCH ANY")
return
}
for j, val := range values {
if j > 0 {
|
| ︙ | ︙ | |||
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
pe.writeString(" SUFFIX ")
case cmpNoSuffix:
pe.writeString(" NOT SUFFIX ")
case cmpMatch:
pe.writeString(" MATCH ")
case cmpNoMatch:
pe.writeString(" NOT MATCH ")
default:
pe.writeString(" MaTcH ")
}
if val.value == "" {
pe.writeString("NOTHING")
} else {
pe.writeString(val.value)
}
}
}
| > > > > > > > > | | | | | | < < | | 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
pe.writeString(" SUFFIX ")
case cmpNoSuffix:
pe.writeString(" NOT SUFFIX ")
case cmpMatch:
pe.writeString(" MATCH ")
case cmpNoMatch:
pe.writeString(" NOT MATCH ")
case cmpLess:
pe.writeString(" LESS ")
case cmpNoLess:
pe.writeString(" NOT LESS ")
case cmpGreater:
pe.writeString(" GREATER ")
case cmpNoGreater:
pe.writeString(" NOT GREATER ")
default:
pe.writeString(" MaTcH ")
}
if val.value == "" {
pe.writeString("NOTHING")
} else {
pe.writeString(val.value)
}
}
}
func (pe *PrintEnv) printOrder(order []sortOrder) {
for _, o := range order {
if o.isRandom() {
pe.printSpace()
pe.writeString(api.RandomDirective)
continue
}
pe.printSpace()
pe.writeString(api.OrderDirective)
if o.descending {
pe.printSpace()
pe.writeString(api.ReverseDirective)
}
pe.printSpace()
pe.writeString(o.key)
}
}
func (pe *PrintEnv) printPosInt(key string, val int) {
if val > 0 {
pe.printSpace()
pe.writeStrings(key, " ", strconv.Itoa(val))
}
}
func (pe *PrintEnv) printActions(words []string) {
if len(words) > 0 {
pe.printSpace()
pe.write(actionSeparatorChar)
for _, word := range words {
pe.printSpace()
pe.writeString(word)
}
}
}
|
Changes to query/query.go.
| ︙ | ︙ | |||
36 37 38 39 40 41 42 |
// Select all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
SearchContains(s string) id.Set
}
// Query specifies a mechanism for querying zettel.
type Query struct {
| | | | | | > > > > > > > > > > > > > > > > > > > > > > > > > | 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
// Select all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
SearchContains(s string) id.Set
}
// Query specifies a mechanism for querying zettel.
type Query struct {
// Präfixed zettel identifier.
zids []id.Zid
// Querydirectives, like CONTEXT, ...
directives []Directive
// Fields to be used for selecting
preMatch MetaMatchFunc // Match that must be true
terms []conjTerms
// Allow to create predictable randomness
seed int
pick int // Randomly pick elements, <= 0: no pick
// Fields to be used for sorting
order []sortOrder
offset int // <= 0: no offset
limit int // <= 0: no limit
// Execute specification
actions []string
}
// GetZids returns a slide of all specified zettel identifier.
func (q *Query) GetZids() []id.Zid {
if q == nil || len(q.zids) == 0 {
return nil
}
result := make([]id.Zid, len(q.zids))
copy(result, q.zids)
return result
}
// Directive are executed to process the list of metadata.
type Directive interface {
Print(*PrintEnv)
}
// GetDirectives returns the slice of query directives.
func (q *Query) GetDirectives() []Directive {
if q == nil || len(q.directives) == 0 {
return nil
}
result := make([]Directive, len(q.directives))
copy(result, q.directives)
return result
}
type keyExistMap map[string]compareOp
type expMetaValues map[string][]expValue
type conjTerms struct {
keys keyExistMap
mvals expMetaValues // Expected values for a meta datum
|
| ︙ | ︙ | |||
109 110 111 112 113 114 115 |
// Clone the query value.
func (q *Query) Clone() *Query {
if q == nil {
return nil
}
c := new(Query)
| | > | > | > | < | 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
// Clone the query value.
func (q *Query) Clone() *Query {
if q == nil {
return nil
}
c := new(Query)
if len(q.zids) > 0 {
c.zids = make([]id.Zid, len(q.zids))
copy(c.zids, q.zids)
}
if len(q.directives) > 0 {
c.directives = make([]Directive, len(q.directives))
copy(c.directives, q.directives)
}
c.preMatch = q.preMatch
c.terms = make([]conjTerms, len(q.terms))
for i, term := range q.terms {
if len(term.keys) > 0 {
c.terms[i].keys = make(keyExistMap, len(term.keys))
|
| ︙ | ︙ | |||
162 163 164 165 166 167 168 169 170 171 |
cmpHasNot
cmpPrefix
cmpNoPrefix
cmpSuffix
cmpNoSuffix
cmpMatch
cmpNoMatch
)
var negateMap = map[compareOp]compareOp{
| > > > > | | | | | | | | | | | | > > > > | | | | | | > > | 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
cmpHasNot
cmpPrefix
cmpNoPrefix
cmpSuffix
cmpNoSuffix
cmpMatch
cmpNoMatch
cmpLess
cmpNoLess
cmpGreater
cmpNoGreater
)
var negateMap = map[compareOp]compareOp{
cmpUnknown: cmpUnknown,
cmpExist: cmpNotExist,
cmpEqual: cmpNotEqual,
cmpNotEqual: cmpEqual,
cmpHas: cmpHasNot,
cmpHasNot: cmpHas,
cmpPrefix: cmpNoPrefix,
cmpNoPrefix: cmpPrefix,
cmpSuffix: cmpNoSuffix,
cmpNoSuffix: cmpSuffix,
cmpMatch: cmpNoMatch,
cmpNoMatch: cmpMatch,
cmpLess: cmpNoLess,
cmpNoLess: cmpLess,
cmpGreater: cmpNoGreater,
cmpNoGreater: cmpGreater,
}
func (op compareOp) negate() compareOp { return negateMap[op] }
var negativeMap = map[compareOp]bool{
cmpNotExist: true,
cmpNotEqual: true,
cmpHasNot: true,
cmpNoPrefix: true,
cmpNoSuffix: true,
cmpNoMatch: true,
cmpNoLess: true,
cmpNoGreater: true,
}
func (op compareOp) isNegated() bool { return negativeMap[op] }
type expValue struct {
value string
op compareOp
|
| ︙ | ︙ | |||
239 240 241 242 243 244 245 |
q = createIfNeeded(q)
if q.seed <= 0 {
q.seed = int(rand.Intn(10000) + 1)
}
return q
}
| < < < < < < < < < < < < < < < < < < | 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
q = createIfNeeded(q)
if q.seed <= 0 {
q.seed = int(rand.Intn(10000) + 1)
}
return q
}
// Actions returns the slice of action specifications
func (q *Query) Actions() []string {
if q == nil {
return nil
}
return q.actions
}
|
| ︙ | ︙ | |||
278 279 280 281 282 283 284 |
// EnrichNeeded returns true, if the query references a metadata key that
// is calculated via metadata enrichments.
func (q *Query) EnrichNeeded() bool {
if q == nil {
return false
}
| | | 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
// EnrichNeeded returns true, if the query references a metadata key that
// is calculated via metadata enrichments.
func (q *Query) EnrichNeeded() bool {
if q == nil {
return false
}
if len(q.zids) > 0 {
return true
}
if len(q.actions) > 0 {
// Unknown, what an action will use. Example: RSS needs api.KeyPublished.
return true
}
for _, term := range q.terms {
|
| ︙ | ︙ | |||
305 306 307 308 309 310 311 |
if meta.IsProperty(o.key) {
return true
}
}
return false
}
| < < < < < < | | < < < < < < < | < < < | | | | | | | | | | | | | | | | | | 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 |
if meta.IsProperty(o.key) {
return true
}
}
return false
}
// RetrieveAndCompile queries the search index and returns a predicate
// for its results and returns a matching predicate.
func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled {
if q == nil {
return Compiled{
PreMatch: matchAlways,
Terms: []CompiledTerm{{
Match: matchAlways,
Retrieve: alwaysIncluded,
}}}
}
q = q.Clone()
preMatch := q.preMatch
if preMatch == nil {
preMatch = matchAlways
}
startSet := metaList2idSet(metaSeq)
result := Compiled{
hasQuery: true,
seed: q.seed,
pick: q.pick,
order: q.order,
offset: q.offset,
limit: q.limit,
startMeta: metaSeq,
PreMatch: preMatch,
Terms: []CompiledTerm{},
}
for _, term := range q.terms {
cTerm := term.retrieveAndCompileTerm(searcher, startSet)
if cTerm.Retrieve == nil {
if cTerm.Match == nil {
// no restriction on match/retrieve -> all will match
result.Terms = []CompiledTerm{{
Match: matchAlways,
Retrieve: alwaysIncluded,
}}
break
}
cTerm.Retrieve = alwaysIncluded
}
if cTerm.Match == nil {
cTerm.Match = matchAlways
}
result.Terms = append(result.Terms, cTerm)
}
return result
}
func metaList2idSet(ml []*meta.Meta) id.Set {
if ml == nil {
return nil
}
result := id.NewSetCap(len(ml))
for _, m := range ml {
result = result.Zid(m.Zid)
}
return result
}
func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet id.Set) CompiledTerm {
match := ct.compileMeta() // Match might add some searches
var pred RetrievePredicate
if searcher != nil {
pred = ct.retrieveIndex(searcher)
if startSet != nil {
if pred == nil {
pred = startSet.Contains
} else {
predSet := id.NewSetCap(len(startSet))
for zid := range startSet {
if pred(zid) {
predSet = predSet.Zid(zid)
}
}
pred = predSet.Contains
}
}
|
| ︙ | ︙ |
Changes to query/retrieve.go.
| ︙ | ︙ | |||
24 25 26 27 28 29 30 |
s string
op compareOp
}
type searchFunc func(string) id.Set
type searchCallMap map[searchOp]searchFunc
var cmpPred = map[compareOp]func(string, string) bool{
| | | | | | > > | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
s string
op compareOp
}
type searchFunc func(string) id.Set
type searchCallMap map[searchOp]searchFunc
var cmpPred = map[compareOp]func(string, string) bool{
cmpEqual: stringEqual,
cmpPrefix: strings.HasPrefix,
cmpSuffix: strings.HasSuffix,
cmpMatch: strings.Contains,
cmpHas: strings.Contains, // the "has" operator have string semantics here in a index search
cmpLess: strings.Contains, // in index search there is no "less", only "has"
cmpGreater: strings.Contains, // in index search there is no "greater", only "has"
}
func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) {
pred := cmpPred[op]
for k := range scm {
if op == cmpMatch {
if strings.Contains(k.s, s) {
|
| ︙ | ︙ | |||
158 159 160 161 162 163 164 |
switch op {
case cmpEqual:
return searcher.SearchEqual
case cmpPrefix:
return searcher.SearchPrefix
case cmpSuffix:
return searcher.SearchSuffix
| | | 160 161 162 163 164 165 166 167 168 169 170 171 172 |
switch op {
case cmpEqual:
return searcher.SearchEqual
case cmpPrefix:
return searcher.SearchPrefix
case cmpSuffix:
return searcher.SearchSuffix
case cmpMatch, cmpHas, cmpLess, cmpGreater: // for index search we assume string semantics
return searcher.SearchContains
default:
panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op))
}
}
|
Changes to query/select.go.
| ︙ | ︙ | |||
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// under this license.
//-----------------------------------------------------------------------------
package query
import (
"fmt"
"strings"
"unicode/utf8"
"zettelstore.de/z/encoder/textenc"
"zettelstore.de/z/parser"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/meta"
)
type matchValueFunc func(value string) bool
func matchValueNever(string) bool { return false }
type matchSpec struct {
key string
match matchValueFunc
}
// compileMeta calculates a selection func based on the given select criteria.
func (ct *conjTerms) compileMeta() MetaMatchFunc {
| > | | > > > | > > > > > > > > > > > | 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
// under this license.
//-----------------------------------------------------------------------------
package query
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
"zettelstore.de/z/encoder/textenc"
"zettelstore.de/z/parser"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/meta"
)
type matchValueFunc func(value string) bool
func matchValueNever(string) bool { return false }
type matchSpec struct {
key string
match matchValueFunc
}
// compileMeta calculates a selection func based on the given select criteria.
func (ct *conjTerms) compileMeta() MetaMatchFunc {
for key, vals := range ct.mvals {
// All queried keys must exist, if there is at least one non-negated compare operation
//
// This is only an optimization to make selection of metadata faster.
if countNegatedOps(vals) < len(vals) {
ct.addKey(key, cmpExist)
}
}
for _, op := range ct.keys {
if op != cmpExist && op != cmpNotExist {
return matchNever
}
}
posSpecs, negSpecs := ct.createSelectSpecs()
if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 {
return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys)
}
return nil
}
func countNegatedOps(vals []expValue) int {
count := 0
for _, val := range vals {
if val.op.isNegated() {
count++
}
}
return count
}
func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) {
posSpecs = make([]matchSpec, 0, len(ct.mvals))
negSpecs = make([]matchSpec, 0, len(ct.mvals))
for key, values := range ct.mvals {
if !meta.KeyIsValid(key) {
continue
|
| ︙ | ︙ | |||
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
switch meta.Type(key) {
case meta.TypeCredential:
return matchValueNever
case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
return createMatchIDFunc(values, addSearch)
case meta.TypeIDSet:
return createMatchIDSetFunc(values, addSearch)
case meta.TypeTagSet:
return createMatchTagSetFunc(values, addSearch)
case meta.TypeWord:
return createMatchWordFunc(values, addSearch)
case meta.TypeWordSet:
return createMatchWordSetFunc(values, addSearch)
case meta.TypeZettelmarkup:
return createMatchZmkFunc(values, addSearch)
}
return createMatchStringFunc(values, addSearch)
}
func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
| > > | > > > > > > > > > > > > | 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
switch meta.Type(key) {
case meta.TypeCredential:
return matchValueNever
case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
return createMatchIDFunc(values, addSearch)
case meta.TypeIDSet:
return createMatchIDSetFunc(values, addSearch)
case meta.TypeNumber:
return createMatchNumberFunc(values, addSearch)
case meta.TypeTagSet:
return createMatchTagSetFunc(values, addSearch)
case meta.TypeWord:
return createMatchWordFunc(values, addSearch)
case meta.TypeWordSet:
return createMatchWordSetFunc(values, addSearch)
case meta.TypeZettelmarkup:
return createMatchZmkFunc(values, addSearch)
}
return createMatchStringFunc(values, addSearch)
}
func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
preds := valuesToIDPredicates(values, addSearch)
return func(value string) bool {
for _, pred := range preds {
if !pred(value) {
return false
}
}
return true
}
}
func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
predList := valuesToWordSetPredicates(preprocessSet(values), addSearch)
return func(value string) bool {
ids := meta.ListFromValue(value)
for _, preds := range predList {
for _, pred := range preds {
if !pred(ids) {
return false
}
}
}
return true
}
}
func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
preds := valuesToNumberPredicates(values, addSearch)
return func(value string) bool {
for _, pred := range preds {
if !pred(value) {
return false
}
}
return true
}
}
func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
predList := valuesToWordSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
|
| ︙ | ︙ | |||
312 313 314 315 316 317 318 319 320 321 322 323 324 325 |
result = append(result, valueElems)
}
}
return result
}
type stringPredicate func(string) bool
func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
result := make([]stringPredicate, len(values))
for i, v := range values {
op := disambiguatedStringOp(v.op)
if !op.isNegated() {
addSearch(v) // addSearch only for positive selections
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 |
result = append(result, valueElems)
}
}
return result
}
type stringPredicate func(string) bool
func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
result := make([]stringPredicate, len(values))
for i, v := range values {
value := v.value
if len(value) > 14 {
value = value[:14]
}
switch op := disambiguatedIDOp(v.op); op {
case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
if isDigits(value) {
// Never add the strValue to search.
// Append enough zeroes to make it comparable as string.
// (an ID and a timestamp always have 14 digits)
strValue := value + "00000000000000"[:14-len(value)]
result[i] = createIDCompareFunc(strValue, op)
continue
}
fallthrough
default:
// Otherwise compare as a word.
if !op.isNegated() {
addSearch(v) // addSearch only for positive selections
}
result[i] = createWordCompareFunc(value, op)
}
}
return result
}
func isDigits(s string) bool {
for i := 0; i < len(s); i++ {
if ch := s[i]; ch < '0' || '9' < ch {
return false
}
}
return true
}
func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }
func createIDCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
return createWordCompareFunc(cmpVal, cmpOp)
}
func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
result := make([]stringPredicate, len(values))
for i, v := range values {
switch op := disambiguatedNumberOp(v.op); op {
case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
iValue, err := strconv.ParseInt(v.value, 10, 64)
if err == nil {
// Never add the strValue to search.
result[i] = createNumberCompareFunc(iValue, op)
continue
}
fallthrough
default:
// In all other cases, a number is treated like a word.
if !op.isNegated() {
addSearch(v) // addSearch only for positive selections
}
result[i] = createWordCompareFunc(v.value, op)
}
}
return result
}
func disambiguatedNumberOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }
func createNumberCompareFunc(cmpVal int64, cmpOp compareOp) stringPredicate {
var cmpFunc func(int64) bool
switch cmpOp {
case cmpEqual:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal == cmpVal }
case cmpNotEqual:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal != cmpVal }
case cmpLess:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal < cmpVal }
case cmpNoLess:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal >= cmpVal }
case cmpGreater:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal }
case cmpNoGreater:
cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal }
default:
panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
}
return func(metaVal string) bool {
iMetaVal, err := strconv.ParseInt(metaVal, 10, 64)
if err != nil {
return false
}
return cmpFunc(iMetaVal)
}
}
func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
result := make([]stringPredicate, len(values))
for i, v := range values {
op := disambiguatedStringOp(v.op)
if !op.isNegated() {
addSearch(v) // addSearch only for positive selections
|
| ︙ | ︙ | |||
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 |
return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) }
case cmpNoSuffix:
return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) }
case cmpMatch:
return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) }
case cmpNoMatch:
return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) }
case cmpHas, cmpHasNot:
panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal))
default:
panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
}
}
type stringSetPredicate func(value []string) bool
func valuesToWordSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate {
result := make([][]stringSetPredicate, len(values))
for i, val := range values {
elemPreds := make([]stringSetPredicate, len(val))
for j, v := range val {
opVal := v.value // loop variable is used in closure --> save needed value
switch op := disambiguateWordOp(v.op); op {
case cmpEqual:
addSearch(v) // addSearch only for positive selections
| > > > > > > > > | | | | | | > > | | | > > | > > | 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 |
return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) }
case cmpNoSuffix:
return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) }
case cmpMatch:
return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) }
case cmpNoMatch:
return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) }
case cmpLess:
return func(metaVal string) bool { return metaVal < cmpVal }
case cmpNoLess:
return func(metaVal string) bool { return metaVal >= cmpVal }
case cmpGreater:
return func(metaVal string) bool { return metaVal > cmpVal }
case cmpNoGreater:
return func(metaVal string) bool { return metaVal <= cmpVal }
case cmpHas, cmpHasNot:
panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal))
default:
panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
}
}
type stringSetPredicate func(value []string) bool
func valuesToWordSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate {
result := make([][]stringSetPredicate, len(values))
for i, val := range values {
elemPreds := make([]stringSetPredicate, len(val))
for j, v := range val {
opVal := v.value // loop variable is used in closure --> save needed value
switch op := disambiguateWordOp(v.op); op {
case cmpEqual:
addSearch(v) // addSearch only for positive selections
fallthrough
case cmpNotEqual:
elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, op == cmpEqual)
case cmpPrefix:
addSearch(v)
fallthrough
case cmpNoPrefix:
elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, op == cmpPrefix)
case cmpSuffix:
addSearch(v)
fallthrough
case cmpNoSuffix:
elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, op == cmpSuffix)
case cmpMatch:
addSearch(v)
fallthrough
case cmpNoMatch:
elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, op == cmpMatch)
case cmpLess, cmpNoLess:
elemPreds[j] = makeStringSetPredicate(opVal, stringLess, op == cmpLess)
case cmpGreater, cmpNoGreater:
elemPreds[j] = makeStringSetPredicate(opVal, stringGreater, op == cmpGreater)
case cmpHas, cmpHasNot:
panic(fmt.Sprintf("operator %d not disambiguated with value %q", op, opVal))
default:
panic(fmt.Sprintf("Unknown compare operation %d with value %q", op, opVal))
}
}
result[i] = elemPreds
}
return result
}
func stringEqual(val1, val2 string) bool { return val1 == val2 }
func stringLess(val1, val2 string) bool { return val1 < val2 }
func stringGreater(val1, val2 string) bool { return val1 > val2 }
type compareStringFunc func(val1, val2 string) bool
func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate {
return func(metaVals []string) bool {
for _, metaVal := range metaVals {
if compare(metaVal, neededValue) {
|
| ︙ | ︙ |
Changes to query/select_test.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package query_test import ( "context" "testing" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package query_test
import (
"context"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/query"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func TestMatchZidNegate(t *testing.T) {
q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))
compiled := q.RetrieveAndCompile(context.Background(), nil, nil)
testCases := []struct {
zid api.ZettelID
exp bool
}{
{api.ZidVersion, false},
{api.ZidLicense, false},
|
| ︙ | ︙ |
Changes to query/sorter.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package query import ( "strconv" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package query
import (
"strconv"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/meta"
)
type sortFunc func(i, j int) bool
func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc {
hasID := false
|
| ︙ | ︙ |
Added query/specs.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package query
import "zettelstore.de/client.fossil/api"
// IdentSpec contains all specification values to calculate the ident directive.
type IdentSpec struct{}
func (spec *IdentSpec) Print(pe *PrintEnv) {
pe.printSpace()
pe.writeString(api.IdentDirective)
}
// ItemsSpec contains all specification values to calculate items.
type ItemsSpec struct{}
func (spec *ItemsSpec) Print(pe *PrintEnv) {
pe.printSpace()
pe.writeString(api.ItemsDirective)
}
|
Added query/unlinked.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package query
import (
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/meta"
)
// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
words []string
}
func (spec *UnlinkedSpec) Print(pe *PrintEnv) {
pe.printSpace()
pe.writeString(api.UnlinkedDirective)
for _, word := range spec.words {
pe.writeStrings(" ", api.PhraseDirective, " ", word)
}
}
func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string {
if words := spec.words; len(words) > 0 {
result := make([]string, len(words))
copy(result, words)
return result
}
result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title
for _, m := range metaSeq {
title, hasTitle := m.Get(api.KeyTitle)
if !hasTitle {
continue
}
result = append(result, strfun.MakeWords(title)...)
}
return result
}
|
Changes to strfun/strfun.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//-----------------------------------------------------------------------------
// Package strfun provides some string functions.
package strfun
import (
"strings"
"unicode/utf8"
)
// Length returns the number of runes in the given string.
func Length(s string) int {
return utf8.RuneCountInString(s)
}
| > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
// Package strfun provides some string functions.
package strfun
import (
"strings"
"unicode"
"unicode/utf8"
)
// Length returns the number of runes in the given string.
func Length(s string) int {
return utf8.RuneCountInString(s)
}
|
| ︙ | ︙ | |||
47 48 49 50 51 52 53 |
// SplitLines splits the given string into a list of lines.
func SplitLines(s string) []string {
return strings.FieldsFunc(s, func(r rune) bool {
return r == '\n' || r == '\r'
})
}
| > > > > > > > > | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
// SplitLines splits the given string into a list of lines.
func SplitLines(s string) []string {
return strings.FieldsFunc(s, func(r rune) bool {
return r == '\n' || r == '\r'
})
}
// MakeWords produces a list of words, i.e. string that were separated by
// control character, space characters, or separator characters.
func MakeWords(text string) []string {
return strings.FieldsFunc(text, func(r rune) bool {
return unicode.In(r, unicode.C, unicode.P, unicode.Z)
})
}
|
Changes to tests/client/client_test.go.
| ︙ | ︙ | |||
16 17 18 19 20 21 22 | "flag" "fmt" "net/http" "net/url" "strconv" "testing" | | | | 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
"flag"
"fmt"
"net/http"
"net/url"
"strconv"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/client"
"zettelstore.de/z/kernel"
)
func nextZid(zid api.ZettelID) api.ZettelID {
numVal, err := strconv.ParseUint(string(zid), 10, 64)
if err != nil {
panic(err)
|
| ︙ | ︙ | |||
45 46 47 48 49 50 51 |
}
}
}
func TestListZettel(t *testing.T) {
const (
| | | | | | | | | | | | | | > > > | | | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
}
}
}
func TestListZettel(t *testing.T) {
const (
ownerZettel = 47
configRoleZettel = 29
writerZettel = ownerZettel - 23
readerZettel = ownerZettel - 23
creatorZettel = 7
publicZettel = 4
)
testdata := []struct {
user string
exp int
}{
{"", publicZettel},
{"creator", creatorZettel},
{"reader", readerZettel},
{"writer", writerZettel},
{"owner", ownerZettel},
}
t.Parallel()
c := getClient()
for i, tc := range testdata {
t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) {
c.SetAuth(tc.user, tc.user)
q, h, l, err := c.QueryZettelData(context.Background(), "")
if err != nil {
tt.Error(err)
return
}
if q != "" {
tt.Errorf("Query should be empty, but is %q", q)
}
if h != "" {
tt.Errorf("Human should be empty, but is %q", q)
}
got := len(l)
if got != tc.exp {
tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
}
})
}
search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id"
q, h, l, err := c.QueryZettelData(context.Background(), search)
if err != nil {
t.Error(err)
return
}
expQ := "role:configuration ORDER id"
if q != expQ {
t.Errorf("Query should be %q, but is %q", expQ, q)
}
expH := "role HAS configuration ORDER id"
if h != expH {
t.Errorf("Human should be %q, but is %q", expH, h)
}
got := len(l)
if got != configRoleZettel {
t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l)
}
pl, err := c.QueryZettel(context.Background(), search)
if err != nil {
t.Error(err)
return
}
compareZettelList(t, pl, l)
}
func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) {
t.Helper()
if len(pl) != len(l) {
t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l))
} else {
for i, line := range pl {
if got := api.ZettelID(line[:14]); got != l[i].ID {
t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got)
}
}
}
}
func TestGetZettelData(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if m := z.Meta; len(m) == 0 {
t.Errorf("Exptected non-empty meta, but got %v", z.Meta)
}
if z.Content == "" || z.Encoding != "" {
t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
}
mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if mr.Rights == api.ZettelCanNone {
t.Error("rights must be greater zero")
}
if len(mr.Meta) != len(z.Meta) {
t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta)
return
}
for k, v := range z.Meta {
got, ok := mr.Meta[k]
if !ok {
t.Errorf("Pure meta has no key %q", k)
continue
}
if got != v {
t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v)
}
|
| ︙ | ︙ | |||
192 193 194 195 196 197 198 |
}
if len(content) == 0 {
t.Errorf("Empty content for evaluated encoding %v", enc)
}
}
}
| < < < < < < < < < | | < < < < | | | | 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
}
if len(content) == 0 {
t.Errorf("Empty content for evaluated encoding %v", enc)
}
}
}
func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) {
t.Helper()
if got := api.ZettelID(l[pos].ID); got != expected {
t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got)
}
}
func TestGetZettelOrder(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
if err != nil {
t.Error(err)
return
}
if got := len(metaSeq); got != 2 {
t.Errorf("Expected list of length 2, got %d", got)
return
}
checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
checkListZid(t, metaSeq, 1, api.ZidTemplateNewUser)
}
// func TestGetZettelContext(t *testing.T) {
// const (
// allUserZid = api.ZettelID("20211019200500")
// ownerZid = api.ZettelID("20210629163300")
// writerZid = api.ZettelID("20210629165000")
|
| ︙ | ︙ | |||
280 281 282 283 284 285 286 |
// checkListZid(t, l, 0, allUserZid)
// }
func TestGetUnlinkedReferences(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
| | < < < < | | 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
// checkListZid(t, l, 0, allUserZid)
// }
func TestGetUnlinkedReferences(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
if err != nil {
t.Error(err)
return
}
if got := len(metaSeq); got != 1 {
t.Errorf("Expected list of length 1, got %d", got)
return
}
}
func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool {
if err != nil {
|
| ︙ | ︙ | |||
335 336 337 338 339 340 341 |
}
}
func TestListTags(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
| | | | | | | | | | | | | | | 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 |
}
}
func TestListTags(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags)
if err != nil {
t.Error(err)
return
}
tags := []struct {
key string
size int
}{
{"#invisible", 1},
{"#user", 4},
{"#test", 4},
}
if len(agg) != len(tags) {
t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(agg), agg)
}
for _, tag := range tags {
if zl, ok := agg[tag.key]; !ok {
t.Errorf("No tag %v: %v", tag.key, agg)
} else if len(zl) != tag.size {
t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
}
}
for i, id := range agg["#user"] {
if id != agg["#test"][i] {
t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
}
}
}
func TestListRoles(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("owner", "owner")
agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole)
if err != nil {
t.Error(err)
return
}
exp := []string{"configuration", "user", "zettel"}
if len(agg) != len(exp) {
t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(agg), agg)
}
for _, id := range exp {
if _, found := agg[id]; !found {
t.Errorf("Role map expected key %q", id)
}
}
}
func TestVersion(t *testing.T) {
t.Parallel()
c := getClient()
ver, err := c.GetVersionInfo(context.Background())
if err != nil {
t.Error(err)
return
}
if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
t.Error(ver)
}
|
| ︙ | ︙ |
Changes to tests/client/crud_test.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package client_test import ( "context" "strings" "testing" | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package client_test
import (
"context"
"strings"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/client"
)
// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.
func TestCreateGetRenameDeleteZettel(t *testing.T) {
// Is not to be allowed to run in parallel with other tests.
|
| ︙ | ︙ | |||
55 56 57 58 59 60 61 |
t.Error("Cannot rename", zid, ":", err)
newZid = zid
}
doDelete(t, c, newZid)
}
| | | | 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
t.Error("Cannot rename", zid, ":", err)
newZid = zid
}
doDelete(t, c, newZid)
}
func TestCreateGetRenameDeleteZettelData(t *testing.T) {
// Is not to be allowed to run in parallel with other tests.
c := getClient()
c.SetAuth("creator", "creator")
zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
Meta: nil,
Encoding: "",
Content: "Example",
})
if err != nil {
t.Error("Cannot create zettel:", err)
return
|
| ︙ | ︙ | |||
84 85 86 87 88 89 90 |
newZid = zid
}
c.SetAuth("owner", "owner")
doDelete(t, c, newZid)
}
| | | | | 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
newZid = zid
}
c.SetAuth("owner", "owner")
doDelete(t, c, newZid)
}
func TestCreateGetDeleteZettelData(t *testing.T) {
// Is not to be allowed to run in parallel with other tests.
c := getClient()
c.SetAuth("owner", "owner")
wrongModified := "19691231115959"
zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
Meta: api.ZettelMeta{
api.KeyTitle: "A\nTitle", // \n must be converted into a space
api.KeyModified: wrongModified,
},
})
if err != nil {
t.Error("Cannot create zettel:", err)
return
}
z, err := c.GetZettelData(context.Background(), zid)
if err != nil {
t.Error("Cannot get zettel:", zid, err)
} else {
exp := "A Title"
if got := z.Meta[api.KeyTitle]; got != exp {
t.Errorf("Expected title %q, but got %q", exp, got)
}
|
| ︙ | ︙ | |||
148 149 150 151 152 153 154 |
if string(zt) != newZettel {
t.Errorf("Expected zettel %q, got %q", newZettel, zt)
}
// Must delete to clean up for next tests
doDelete(t, c, api.ZidDefaultHome)
}
| | | | | | 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
if string(zt) != newZettel {
t.Errorf("Expected zettel %q, got %q", newZettel, zt)
}
// Must delete to clean up for next tests
doDelete(t, c, api.ZidDefaultHome)
}
func TestUpdateZettelData(t *testing.T) {
c := getClient()
c.SetAuth("writer", "writer")
z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if got := z.Meta[api.KeyTitle]; got != "Home" {
t.Errorf("Title of zettel is not \"Home\", but %q", got)
return
}
newTitle := "New Home"
z.Meta[api.KeyTitle] = newTitle
wrongModified := "19691231235959"
z.Meta[api.KeyModified] = wrongModified
err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z)
if err != nil {
t.Error(err)
return
}
zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
if err != nil {
t.Error(err)
return
}
if got := zt.Meta[api.KeyTitle]; got != newTitle {
t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
}
|
| ︙ | ︙ |
Changes to tests/client/embed_test.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package client_test import ( "context" "strings" "testing" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package client_test
import (
"context"
"strings"
"testing"
"zettelstore.de/client.fossil/api"
)
const (
abcZid = api.ZettelID("20211020121000")
abc10Zid = api.ZettelID("20211020121100")
)
|
| ︙ | ︙ | |||
73 74 75 76 77 78 79 |
}
func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("reader", "reader")
| | | 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
}
func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
t.Parallel()
c := getClient()
c.SetAuth("reader", "reader")
zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji)
if err != nil {
t.Error(err)
return
}
expectedEnc := "base64"
if got := zettelData.Encoding; expectedEnc != got {
t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
|
| ︙ | ︙ |
Changes to tests/markdown_test.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "bytes" "encoding/json" "fmt" "os" "strings" "testing" | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | "bytes" "encoding/json" "fmt" "os" "strings" "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/config" "zettelstore.de/z/encoder" _ "zettelstore.de/z/encoder/htmlenc" _ "zettelstore.de/z/encoder/mdenc" _ "zettelstore.de/z/encoder/shtmlenc" _ "zettelstore.de/z/encoder/szenc" |
| ︙ | ︙ |
Changes to tests/regression_test.go.
| ︙ | ︙ | |||
17 18 19 20 21 22 23 | "io" "net/url" "os" "path/filepath" "strings" "testing" | | | 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | "io" "net/url" "os" "path/filepath" "strings" "testing" "zettelstore.de/client.fossil/api" "zettelstore.de/z/ast" "zettelstore.de/z/box" "zettelstore.de/z/box/manager" "zettelstore.de/z/config" "zettelstore.de/z/encoder" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" |
| ︙ | ︙ |
Changes to tools/build.go.
| ︙ | ︙ | |||
24 25 26 27 28 29 30 | "path/filepath" "strings" "time" "zettelstore.de/z/strfun" ) | | > | 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
"path/filepath"
"strings"
"time"
"zettelstore.de/z/strfun"
)
var envDirectProxy = []string{"GOPROXY=direct"}
var envGoVCS = []string{"GOVCS=zettelstore.de:fossil"}
func executeCommand(env []string, name string, arg ...string) (string, error) {
logCommand("EXEC", env, name, arg)
var out strings.Builder
cmd := prepareCommand(env, name, arg, &out)
err := cmd.Run()
return out.String(), err
|
| ︙ | ︙ | |||
135 136 137 138 139 140 141 142 143 |
return err
}
}
return checkFossilExtra()
}
func checkGoTest(pkg string, testParams ...string) error {
args := []string{"test", pkg}
args = append(args, testParams...)
| > > > | | | | | | | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
return err
}
}
return checkFossilExtra()
}
func checkGoTest(pkg string, testParams ...string) error {
var env []string
env = append(env, envDirectProxy...)
env = append(env, envGoVCS...)
args := []string{"test", pkg}
args = append(args, testParams...)
out, err := executeCommand(env, "go", args...)
if err != nil {
for _, line := range strfun.SplitLines(out) {
if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
continue
}
fmt.Fprintln(os.Stderr, line)
}
}
return err
}
func checkGoVet() error {
out, err := executeCommand(envGoVCS, "go", "vet", "./...")
if err != nil {
fmt.Fprintln(os.Stderr, "Some checks failed")
if len(out) > 0 {
fmt.Fprintln(os.Stderr, out)
}
}
return err
}
func checkShadow(forRelease bool) error {
path, err := findExecStrict("shadow", forRelease)
if path == "" {
return err
}
out, err := executeCommand(envGoVCS, path, "-strict", "./...")
if err != nil {
fmt.Fprintln(os.Stderr, "Some shadowed variables found")
if len(out) > 0 {
fmt.Fprintln(os.Stderr, out)
}
}
return err
}
func checkStaticcheck() error {
out, err := executeCommand(envGoVCS, "staticcheck", "./...")
if err != nil {
fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
if len(out) > 0 {
fmt.Fprintln(os.Stderr, out)
}
}
return err
}
func checkUnparam(forRelease bool) error {
path, err := findExecStrict("unparam", forRelease)
if path == "" {
return err
}
out, err := executeCommand(envGoVCS, path, "./...")
if err != nil {
fmt.Fprintln(os.Stderr, "Some unparam problems found")
if len(out) > 0 {
fmt.Fprintln(os.Stderr, out)
}
}
if forRelease {
if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
if len(out2) > 0 {
fmt.Fprintln(os.Stderr, out2)
}
}
}
return err
}
func checkGoVulncheck() error {
out, err := executeCommand(envGoVCS, "govulncheck", "./...")
if err != nil {
fmt.Fprintln(os.Stderr, "Some checks failed")
if len(out) > 0 {
fmt.Fprintln(os.Stderr, out)
}
}
return err
|
| ︙ | ︙ | |||
279 280 281 282 283 284 285 |
func startZettelstore(info *zsInfo) error {
info.adminAddress = ":2323"
name, arg := "go", []string{
"run", "cmd/zettelstore/main.go", "run",
"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
logCommand("FORK", nil, name, arg)
| | > | 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
func startZettelstore(info *zsInfo) error {
info.adminAddress = ":2323"
name, arg := "go", []string{
"run", "cmd/zettelstore/main.go", "run",
"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
logCommand("FORK", nil, name, arg)
cmd := prepareCommand(envGoVCS, name, arg, &info.out)
if !verbose {
cmd.Stderr = nil
}
err := cmd.Start()
time.Sleep(2 * time.Second)
for i := 0; i < 100; i++ {
time.Sleep(time.Millisecond * 100)
if addressInUse(info.adminAddress) {
info.cmd = cmd
return err
}
}
time.Sleep(4 * time.Second) // Wait for all zettel to be indexed.
return errors.New("zettelstore did not start")
}
func stopZettelstore(i *zsInfo) error {
conn, err := net.Dial("tcp", i.adminAddress)
if err != nil {
fmt.Println("Unable to stop Zettelstore")
|
| ︙ | ︙ | |||
317 318 319 320 321 322 323 |
return false
}
conn.Close()
return true
}
func cmdBuild() error {
| | > | 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
return false
}
conn.Close()
return true
}
func cmdBuild() error {
return doBuild(envDirectProxy, getVersion(), "bin/zettelstore")
}
func doBuild(env []string, version, target string) error {
env = append(env, "CGO_ENABLED=0")
env = append(env, envGoVCS...)
out, err := executeCommand(
env,
"go", "build",
"-tags", "osusergo,netgo",
"-trimpath",
"-ldflags", fmt.Sprintf("-X main.version=%v -w", version),
"-o", target,
|
| ︙ | ︙ | |||
423 424 425 426 427 428 429 |
{"amd64", "darwin", nil, "zettelstore"},
{"arm64", "darwin", nil, "zettelstore"},
{"amd64", "windows", nil, "zettelstore.exe"},
}
for _, rel := range releases {
env := append([]string{}, rel.env...)
env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
| > | | 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
{"amd64", "darwin", nil, "zettelstore"},
{"arm64", "darwin", nil, "zettelstore"},
{"amd64", "windows", nil, "zettelstore.exe"},
}
for _, rel := range releases {
env := append([]string{}, rel.env...)
env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
env = append(env, envDirectProxy...)
env = append(env, envGoVCS...)
zsName := filepath.Join("releases", rel.name)
if err := doBuild(env, base, zsName); err != nil {
return err
}
zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
return err
|
| ︙ | ︙ |
Changes to usecase/authenticate.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "context" "math/rand" "net/http" "time" | | < < < < < < < < | | < | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import (
"context"
"math/rand"
"net/http"
"time"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/auth/cred"
"zettelstore.de/z/logger"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Authenticate is the data for this use case.
type Authenticate struct {
log *logger.Logger
token auth.TokenManager
ucGetUser *GetUser
}
// NewAuthenticate creates a new use case.
func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate {
return Authenticate{
log: log,
token: token,
ucGetUser: ucGetUser,
}
}
// Run executes the use case.
//
// Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it
// for other purposes.
|
| ︙ | ︙ |
Changes to usecase/create_zettel.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package usecase import ( "context" "time" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package usecase import ( "context" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/config" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ | |||
70 71 72 73 74 75 76 |
// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
origMeta := origZettel.Meta
m := meta.New(id.Invalid)
if title, found := origMeta.Get(api.KeyTitle); found {
m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
}
| | < < > > > > > > > > > > > > < < | > > > > > > | 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
origMeta := origZettel.Meta
m := meta.New(id.Invalid)
if title, found := origMeta.Get(api.KeyTitle); found {
m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
}
updateMetaRoleTagsSyntax(m, origMeta)
m.Set(api.KeyPrecursor, origMeta.Zid.String())
return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}
// PrepareChild the zettel for further modification.
func (*CreateZettel) PrepareChild(origZettel zettel.Zettel) zettel.Zettel {
origMeta := origZettel.Meta
m := origMeta.Clone()
if title, found := m.Get(api.KeyTitle); found {
m.Set(api.KeyTitle, prependTitle(title, "Child", "Child of "))
}
updateMetaRoleTagsSyntax(m, origMeta)
m.Set(api.KeySuperior, origMeta.Zid.String())
return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}
// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel) zettel.Zettel {
m := meta.New(id.Invalid)
om := origZettel.Meta
m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
updateMetaRoleTagsSyntax(m, om)
const prefixLen = len(meta.NewPrefix)
for _, pair := range om.PairsRest() {
if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
m.Set(key[prefixLen:], pair.Value)
}
}
content := origZettel.Content
content.TrimSpace()
return zettel.Zettel{Meta: m, Content: content}
}
func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, ""))
m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, ""))
}
func prependTitle(title, s0, s1 string) string {
if len(title) > 0 {
return s1 + title
}
return s0
}
|
| ︙ | ︙ |
Changes to usecase/evaluate.go.
| ︙ | ︙ | |||
21 22 23 24 25 26 27 |
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Evaluate is the data for this use case.
type Evaluate struct {
| | | | < | | | | < | | < < | > > > | < < < < < | | | | | 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Evaluate is the data for this use case.
type Evaluate struct {
rtConfig config.Config
ucGetZettel *GetZettel
ucQuery *Query
}
// NewEvaluate creates a new use case.
func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate {
return Evaluate{
rtConfig: rtConfig,
ucGetZettel: ucGetZettel,
ucQuery: ucQuery,
}
}
// Run executes the use case.
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
zettel, err := uc.ucGetZettel.Run(ctx, zid)
if err != nil {
return nil, err
}
return uc.RunZettel(ctx, zettel, syntax), nil
}
// RunZettel executes the use case for a given zettel.
func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode {
zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig)
evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn)
return zn
}
// RunBlockNode executes the use case for a metadata list.
func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice {
if bn == nil {
return nil
}
bns := ast.BlockSlice{bn}
evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
return bns
}
// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
is := parser.ParseMetadata(value)
evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
return is
}
// GetZettel retrieves the full zettel of a given zettel identifier.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
return uc.ucGetZettel.Run(ctx, zid)
}
// QueryMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
return uc.ucQuery.Run(ctx, q)
}
|
Deleted usecase/get_all_meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/get_all_zettel.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package usecase
import (
"context"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
)
// GetAllZettelPort is the interface used by this use case.
type GetAllZettelPort interface {
GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
}
// GetAllZettel is the data for this use case.
type GetAllZettel struct {
port GetAllZettelPort
}
// NewGetAllZettel creates a new use case.
func NewGetAllZettel(port GetAllZettelPort) GetAllZettel {
return GetAllZettel{port: port}
}
// Run executes the use case.
func (uc GetAllZettel) Run(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
return uc.port.GetAllZettel(ctx, zid)
}
|
Deleted usecase/get_meta.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/get_user.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package usecase import ( "context" | | > | | | | | | | | > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
//-----------------------------------------------------------------------------
package usecase
import (
"context"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/box"
"zettelstore.de/z/query"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// Use case: return user identified by meta key ident.
// ---------------------------------------------------
// GetUserPort is the interface used by this use case.
type GetUserPort interface {
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}
// GetUser is the data for this use case.
type GetUser struct {
authz auth.AuthzManager
port GetUserPort
}
// NewGetUser creates a new use case.
func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser {
return GetUser{authz: authz, port: port}
}
// Run executes the use case.
func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) {
ctx = box.NoEnrichContext(ctx)
// It is important to try first with the owner. First, because another user
// could give herself the same ''ident''. Second, in most cases the owner
// will authenticate.
identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner())
if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident {
return identZettel.Meta, nil
}
// Owner was not found or has another ident. Try via list search.
q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident)
metaList, err := uc.port.SelectMeta(ctx, nil, q)
if err != nil {
return nil, err
}
if len(metaList) < 1 {
return nil, nil
}
return metaList[len(metaList)-1], nil
}
// Use case: return an user identified by zettel id and assert given ident value.
// ------------------------------------------------------------------------------
// GetUserByZidPort is the interface used by this use case.
type GetUserByZidPort interface {
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}
// GetUserByZid is the data for this use case.
type GetUserByZid struct {
port GetUserByZidPort
}
// NewGetUserByZid creates a new use case.
func NewGetUserByZid(port GetUserByZidPort) GetUserByZid {
return GetUserByZid{port: port}
}
// GetUser executes the use case.
func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) {
userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid)
if err != nil {
return nil, err
}
userMeta := userZettel.Meta
if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident {
return nil, nil
}
return userMeta, nil
}
|
Changes to usecase/lists.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package usecase import ( "context" | | < < < < < | < < < | < < < < < < < < < < < < < < | | < | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
//-----------------------------------------------------------------------------
package usecase
import (
"context"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/box"
"zettelstore.de/z/parser"
"zettelstore.de/z/query"
"zettelstore.de/z/zettel/meta"
)
// -------- List syntax ------------------------------------------------------
// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}
// ListSyntax is the data for this use case.
type ListSyntax struct {
port ListSyntaxPort
}
// NewListSyntax creates a new use case.
func NewListSyntax(port ListSyntaxPort) ListSyntax {
return ListSyntax{port: port}
}
// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key
metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
if err != nil {
return nil, err
}
result := meta.CreateArrangement(metas, api.KeySyntax)
for _, syn := range parser.GetSyntaxes() {
if _, found := result[syn]; !found {
delete(result, syn)
}
}
return result, nil
}
// -------- List roles -------------------------------------------------------
// ListRolesPort is the interface used by this use case.
type ListRolesPort interface {
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}
// ListRoles is the data for this use case.
type ListRoles struct {
port ListRolesPort
}
// NewListRoles creates a new use case.
func NewListRoles(port ListRolesPort) ListRoles {
return ListRoles{port: port}
}
// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key
metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
if err != nil {
return nil, err
}
return meta.CreateArrangement(metas, api.KeyRole), nil
}
|
Deleted usecase/order.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Added usecase/query.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package usecase
import (
"context"
"errors"
"fmt"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/collect"
"zettelstore.de/z/parser"
"zettelstore.de/z/query"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// QueryPort is the interface used by this use case.
type QueryPort interface {
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}
// Query is the data for this use case.
type Query struct {
port QueryPort
ucEvaluate Evaluate
}
// NewQuery creates a new use case.
func NewQuery(port QueryPort) Query {
return Query{port: port}
}
// SetEvaluate sets the usecase Evaluate, because of circular dependencies.
func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate }
// Run executes the use case.
func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
zids := q.GetZids()
if zids == nil {
return uc.port.SelectMeta(ctx, nil, q)
}
if len(zids) == 0 {
return nil, nil
}
metaSeq, err := uc.getMetaZid(ctx, zids)
if err != nil {
return metaSeq, err
}
if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 {
return uc.port.SelectMeta(ctx, metaSeq, q)
}
return nil, nil
}
func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) {
metaSeq := make([]*meta.Meta, 0, len(zids))
for _, zid := range zids {
m, err := uc.port.GetMeta(ctx, zid)
if err == nil {
metaSeq = append(metaSeq, m)
continue
}
if errors.Is(err, &box.ErrNotAllowed{}) {
continue
}
return metaSeq, err
}
return metaSeq, nil
}
func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta {
if len(directives) == 0 {
return metaSeq
}
for _, dir := range directives {
if len(metaSeq) == 0 {
return nil
}
switch ds := dir.(type) {
case *query.ContextSpec:
metaSeq = uc.processContextDirective(ctx, ds, metaSeq)
case *query.IdentSpec:
// Nothing to do.
case *query.ItemsSpec:
metaSeq = uc.processItemsDirective(ctx, ds, metaSeq)
case *query.UnlinkedSpec:
metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq)
default:
panic(fmt.Sprintf("Unknown directive %T", ds))
}
}
if len(metaSeq) == 0 {
return nil
}
return metaSeq
}
func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
return spec.Execute(ctx, metaSeq, uc.port)
}
func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
result := make([]*meta.Meta, 0, len(metaSeq))
for _, m := range metaSeq {
zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, ""))
if err != nil {
continue
}
for _, ref := range collect.Order(zn) {
if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
result = append(result, z.Meta)
}
}
}
}
return result
}
func (uc *Query) processUnlinkedDirective(ctx context.Context, spec *query.UnlinkedSpec, metaSeq []*meta.Meta) []*meta.Meta {
words := spec.GetWords(metaSeq)
if len(words) == 0 {
return metaSeq
}
var sb strings.Builder
for _, word := range words {
sb.WriteString(" :")
sb.WriteString(word)
}
q := (*query.Query)(nil).Parse(sb.String())
candidates, err := uc.port.SelectMeta(ctx, nil, q)
if err != nil {
return nil
}
metaZids := id.NewSetCap(len(metaSeq))
refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
for _, m := range metaSeq {
metaZids.Zid(m.Zid)
refZids.Zid(m.Zid)
for _, pair := range m.ComputedPairsRest() {
switch meta.Type(pair.Key) {
case meta.TypeID:
if zid, errParse := id.Parse(pair.Value); errParse == nil {
refZids.Zid(zid)
}
case meta.TypeIDSet:
for _, value := range meta.ListFromValue(pair.Value) {
if zid, errParse := id.Parse(value); errParse == nil {
refZids.Zid(zid)
}
}
}
}
}
candidates = filterByZid(candidates, refZids)
return uc.filterCandidates(ctx, candidates, words)
}
func filterByZid(candidates []*meta.Meta, ignoreSeq id.Set) []*meta.Meta {
result := make([]*meta.Meta, 0, len(candidates))
for _, m := range candidates {
if !ignoreSeq.Contains(m.Zid) {
result = append(result, m)
}
}
return result
}
func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
result := make([]*meta.Meta, 0, len(candidates))
candLoop:
for _, cand := range candidates {
zettel, err := uc.port.GetZettel(ctx, cand.Zid)
if err != nil {
continue
}
v := unlinkedVisitor{
words: words,
found: false,
}
v.text = v.joinWords(words)
for _, pair := range zettel.Meta.Pairs() {
if meta.Type(pair.Key) != meta.TypeZettelmarkup {
continue
}
is := uc.ucEvaluate.RunMetadata(ctx, pair.Value)
ast.Walk(&v, &is)
if v.found {
result = append(result, cand)
continue candLoop
}
}
syntax := zettel.Meta.GetDefault(api.KeySyntax, "")
if !parser.IsASTParser(syntax) {
continue
}
zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax)
ast.Walk(&v, &zn.Ast)
if v.found {
result = append(result, cand)
}
}
return result
}
func (*unlinkedVisitor) joinWords(words []string) string {
return " " + strings.ToLower(strings.Join(words, " ")) + " "
}
type unlinkedVisitor struct {
words []string
text string
found bool
}
func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
switch n := node.(type) {
case *ast.InlineSlice:
v.checkWords(n)
return nil
case *ast.HeadingNode:
return nil
case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode:
return nil
}
return v
}
func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) {
if len(*is) < 2*len(v.words)-1 {
return
}
for _, text := range v.splitInlineTextList(is) {
if strings.Contains(text, v.text) {
v.found = true
}
}
}
func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
var result []string
var curList []string
for _, in := range *is {
switch n := in.(type) {
case *ast.TextNode:
curList = append(curList, strfun.MakeWords(n.Text)...)
case *ast.SpaceNode:
default:
if curList != nil {
result = append(result, v.joinWords(curList))
curList = nil
}
}
}
if curList != nil {
result = append(result, v.joinWords(curList))
}
return result
}
|
Changes to usecase/rename_zettel.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package usecase import ( "context" "zettelstore.de/z/box" "zettelstore.de/z/logger" | | | < | < < | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
package usecase
import (
"context"
"zettelstore.de/z/box"
"zettelstore.de/z/logger"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
)
// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
RenameZettel(ctx context.Context, curZid, newZid id.Zid) error
}
// RenameZettel is the data for this use case.
type RenameZettel struct {
log *logger.Logger
port RenameZettelPort
|
| ︙ | ︙ | |||
45 46 47 48 49 50 51 |
func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel {
return RenameZettel{log: log, port: port}
}
// Run executes the use case.
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
noEnrichCtx := box.NoEnrichContext(ctx)
| | | | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel {
return RenameZettel{log: log, port: port}
}
// Run executes the use case.
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
noEnrichCtx := box.NoEnrichContext(ctx)
if _, err := uc.port.GetZettel(noEnrichCtx, curZid); err != nil {
return err
}
if newZid == curZid {
// Nothing to do
return nil
}
if _, err := uc.port.GetZettel(noEnrichCtx, newZid); err == nil {
return &ErrZidInUse{Zid: newZid}
}
err := uc.port.RenameZettel(ctx, curZid, newZid)
uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel")
return err
}
|
Deleted usecase/unlinked_refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to usecase/update_zettel.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package usecase import ( "context" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package usecase import ( "context" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/logger" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) |
| ︙ | ︙ |
Changes to web/adapter/api/api.go.
| ︙ | ︙ | |||
13 14 15 16 17 18 19 | import ( "bytes" "context" "net/http" "time" | | | 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "bytes" "context" "net/http" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ |
Changes to web/adapter/api/command.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package api import ( "context" "net/http" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package api import ( "context" "net/http" "zettelstore.de/client.fossil/api" "zettelstore.de/z/usecase" ) // MakePostCommandHandler creates a new HTTP handler to execute certain commands. func (a *API) MakePostCommandHandler( ucIsAuth *usecase.IsAuthenticated, ucRefresh *usecase.Refresh, |
| ︙ | ︙ |
Changes to web/adapter/api/create_zettel.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package api import ( "bytes" "net/http" | > | > > > > > > | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package api
import (
"bytes"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/content"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
)
type zidJSON struct {
ID api.ZettelID `json:"id"`
}
// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
enc, encStr := getEncoding(r, q)
var zettel zettel.Zettel
var err error
switch enc {
case api.EncoderPlain:
zettel, err = buildZettelFromPlainData(r, id.Invalid)
case api.EncoderData:
zettel, err = buildZettelFromData(r, id.Invalid)
case api.EncoderJson:
zettel, err = buildZettelFromJSONData(r, id.Invalid)
default:
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if err != nil {
|
| ︙ | ︙ | |||
54 55 56 57 58 59 60 61 62 |
var result []byte
var contentType string
location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String()))
switch enc {
case api.EncoderPlain:
result = newZid.Bytes()
contentType = content.PlainText
case api.EncoderJson:
var buf bytes.Buffer
| > > > | | 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
var result []byte
var contentType string
location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String()))
switch enc {
case api.EncoderPlain:
result = newZid.Bytes()
contentType = content.PlainText
case api.EncoderData:
result = []byte(sx.Int64(newZid).Repr())
contentType = content.SXPF
case api.EncoderJson:
var buf bytes.Buffer
err = encodeJSONData(&buf, zidJSON{ID: api.ZettelID(newZid.String())})
if err != nil {
a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
result = buf.Bytes()
contentType = content.JSON
|
| ︙ | ︙ |
Changes to web/adapter/api/get_data.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2022-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( | < | | | | | | | | < < < < < < < < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
//-----------------------------------------------------------------------------
// Copyright (c) 2022-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package api
import (
"net/http"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/usecase"
"zettelstore.de/z/zettel/id"
)
// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
version := ucVersion.Run()
err := a.writeObject(w, id.Invalid, sx.MakeList(
sx.Int64(version.Major),
sx.Int64(version.Minor),
sx.Int64(version.Patch),
sx.MakeString(version.Info),
sx.MakeString(version.Hash),
))
a.log.IfErr(err).Msg("Write Version Info")
}
}
|
Deleted web/adapter/api/get_order.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Deleted web/adapter/api/get_unlinked_refs.go.
|
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
Changes to web/adapter/api/get_zettel.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "bytes" "context" "fmt" "net/http" | | > | < < | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
import (
"bytes"
"context"
"fmt"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/sexp"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/encoder"
"zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/content"
"zettelstore.de/z/zettel/id"
)
// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
http.NotFound(w, r)
return
}
q := r.URL.Query()
part := getPart(q, partContent)
ctx := r.Context()
switch enc, encStr := getEncoding(r, q); enc {
case api.EncoderPlain:
a.writePlainData(w, ctx, zid, part, getZettel)
case api.EncoderData:
a.writeSzData(w, ctx, zid, part, getZettel)
case api.EncoderJson:
a.writeJSONData(w, ctx, zid, part, getZettel)
default:
var zn *ast.ZettelNode
var em func(value string) ast.InlineSlice
if q.Has(api.QueryKeyParseOnly) {
zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
em = parser.ParseMetadata
|
| ︙ | ︙ | |||
69 70 71 72 73 74 75 | return } a.writeEncodedZettelPart(w, zn, em, enc, encStr, part) } } } | | > > > > > > < < < < < | | | | < < < < < | < < < < < | > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < | < | < < < < < | | 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
return
}
a.writeEncodedZettelPart(w, zn, em, enc, encStr, part)
}
}
}
func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
var buf bytes.Buffer
var contentType string
var err error
z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
if err != nil {
a.reportUsecaseError(w, err)
return
}
switch part {
case partZettel:
_, err = z.Meta.Write(&buf)
if err == nil {
err = buf.WriteByte('\n')
}
if err == nil {
_, err = z.Content.Write(&buf)
}
case partMeta:
contentType = content.PlainText
_, err = z.Meta.Write(&buf)
case partContent:
contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, ""))
_, err = z.Content.Write(&buf)
}
if err != nil {
a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = writeBuffer(w, &buf, contentType)
a.log.IfErr(err).Zid(zid).Msg("Write Plain data")
}
func (a *API) writeSzData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
z, err := getZettel.Run(ctx, zid)
if err != nil {
a.reportUsecaseError(w, err)
return
}
var obj sx.Object
switch part {
case partZettel:
zContent, zEncoding := z.Content.Encode()
obj = sexp.EncodeZettel(api.ZettelData{
Meta: z.Meta.Map(),
Rights: a.getRights(ctx, z.Meta),
Encoding: zEncoding,
Content: zContent,
})
case partMeta:
obj = sexp.EncodeMetaRights(api.MetaRights{
Meta: z.Meta.Map(),
Rights: a.getRights(ctx, z.Meta),
})
}
err = a.writeObject(w, zid, obj)
a.log.IfErr(err).Zid(zid).Msg("write sx data")
}
type zettelJSON struct {
ID api.ZettelID `json:"id"`
Meta api.ZettelMeta `json:"meta"`
Encoding string `json:"encoding"`
Content string `json:"content"`
Rights api.ZettelRights `json:"rights"`
}
type zettelMetaJSON struct {
Meta api.ZettelMeta `json:"meta"`
Rights api.ZettelRights `json:"rights"`
}
type zettelContentJSON struct {
Encoding string `json:"encoding"`
Content string `json:"content"`
}
func (a *API) writeJSONData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getZettel usecase.GetZettel) {
z, err := getZettel.Run(ctx, zid)
if err != nil {
a.reportUsecaseError(w, err)
return
}
var buf bytes.Buffer
switch part {
case partZettel:
zContent, encoding := z.Content.Encode()
err = encodeJSONData(&buf, zettelJSON{
ID: api.ZettelID(zid.String()),
Meta: z.Meta.Map(),
Encoding: encoding,
Content: zContent,
Rights: a.getRights(ctx, z.Meta),
})
case partMeta:
m := z.Meta
err = encodeJSONData(&buf, zettelMetaJSON{
Meta: m.Map(),
Rights: a.getRights(ctx, m),
})
case partContent:
zContent, encoding := z.Content.Encode()
err = encodeJSONData(&buf, zettelContentJSON{
Encoding: encoding,
Content: zContent,
})
}
if err != nil {
a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store zettel in buffer")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
| ︙ | ︙ |
Changes to web/adapter/api/json.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( | < < | < < < < < < < < | < < | < < < < < < < < < | | > | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package api
import (
"encoding/json"
"io"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func encodeJSONData(w io.Writer, data interface{}) error {
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
return enc.Encode(data)
}
type zettelDataJSON struct {
Meta api.ZettelMeta `json:"meta"`
Encoding string `json:"encoding"`
Content string `json:"content"`
}
func buildZettelFromJSONData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
var zettel zettel.Zettel
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
var zettelData zettelDataJSON
if err := dec.Decode(&zettelData); err != nil {
return zettel, err
}
m := meta.New(zid)
for k, v := range zettelData.Meta {
m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
}
|
| ︙ | ︙ |
Changes to web/adapter/api/login.go.
1 2 3 4 5 6 7 8 9 10 11 12 13 | //----------------------------------------------------------------------------- // Copyright (c) 2020-present Detlef Stern // // This file is part of Zettelstore. // // Zettelstore is licensed under the latest version of the EUPL (European Union // Public License). Please see file LICENSE.txt for your rights and obligations // under this license. //----------------------------------------------------------------------------- package api import ( | < < | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package api
import (
"net/http"
"time"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/auth"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/zettel/id"
)
// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !a.withAuth() {
err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour)
|
| ︙ | ︙ | |||
92 93 94 95 96 97 98 |
}
err = a.writeToken(w, string(token), a.tokenLifetime)
a.log.IfErr(err).Msg("Write renewed token")
}
}
func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
| | | | | | < < < < < < | < < < | 90 91 92 93 94 95 96 97 98 99 100 101 102 |
}
err = a.writeToken(w, string(token), a.tokenLifetime)
a.log.IfErr(err).Msg("Write renewed token")
}
}
func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
return a.writeObject(w, id.Invalid, sx.MakeList(
sx.MakeString("Bearer"),
sx.MakeString(token),
sx.Int64(int64(lifetime/time.Second)),
))
}
|
Changes to web/adapter/api/query.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | "bytes" "fmt" "io" "net/http" "strconv" "strings" | > > | | | > > > > > > > > | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
"bytes"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/sexp"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/query"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/content"
"zettelstore.de/z/zettel/meta"
)
// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(queryMeta *usecase.Query) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
sq := adapter.GetQuery(q)
metaSeq, err := queryMeta.Run(ctx, sq)
if err != nil {
a.reportUsecaseError(w, err)
return
}
var encoder zettelEncoder
var contentType string
switch enc, _ := getEncoding(r, q); enc {
case api.EncoderPlain:
encoder = &plainZettelEncoder{}
contentType = content.PlainText
case api.EncoderData:
encoder = &dataZettelEncoder{
sf: sx.MakeMappedFactory(),
sq: sq,
getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
}
contentType = content.SXPF
case api.EncoderJson: // DEPRECATED
encoder = &jsonZettelEncoder{
sq: sq,
getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
}
contentType = content.JSON
default:
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
var buf bytes.Buffer
err = queryAction(&buf, encoder, metaSeq, sq)
if err != nil {
a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
err = writeBuffer(w, &buf, contentType)
a.log.IfErr(err).Msg("write result buffer")
|
| ︙ | ︙ | |||
85 86 87 88 89 90 91 |
max = num
continue
}
}
acts = append(acts, act)
}
for _, act := range acts {
| > > > > | < | > > > > > > > > > > | | | | | 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
max = num
continue
}
}
acts = append(acts, act)
}
for _, act := range acts {
switch act {
case "KEYS":
return encodeKeysArrangement(w, enc, ml, act)
}
switch key := strings.ToLower(act); meta.Type(key) {
case meta.TypeWord, meta.TypeTagSet:
return encodeMetaKeyArrangement(w, enc, ml, key, min, max)
}
}
}
return enc.writeMetaList(w, ml)
}
func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error {
arr := make(meta.Arrangement, 128)
for _, m := range ml {
for k := range m.Map() {
arr[k] = append(arr[k], m)
}
}
return enc.writeArrangement(w, act, arr)
}
func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error {
arr0 := meta.CreateArrangement(ml, key)
arr := make(meta.Arrangement, len(arr0))
for k0, ml0 := range arr0 {
if len(ml0) < min || (max > 0 && len(ml0) > max) {
continue
}
arr[k0] = ml0
}
return enc.writeArrangement(w, key, arr)
}
type zettelEncoder interface {
writeMetaList(w io.Writer, ml []*meta.Meta) error
writeArrangement(w io.Writer, act string, arr meta.Arrangement) error
}
type plainZettelEncoder struct{}
func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
for _, m := range ml {
_, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle())
if err != nil {
return err
}
}
return nil
}
func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
for key, ml := range arr {
_, err := io.WriteString(w, key)
if err != nil {
return err
}
for i, m := range ml {
if i == 0 {
|
| ︙ | ︙ | |||
151 152 153 154 155 156 157 158 159 160 161 162 163 |
if err != nil {
return err
}
}
return nil
}
type jsonZettelEncoder struct {
sq *query.Query
getRights func(*meta.Meta) api.ZettelRights
}
func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | > > > > > | | | | 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
if err != nil {
return err
}
}
return nil
}
type dataZettelEncoder struct {
sf sx.SymbolFactory
sq *query.Query
getRights func(*meta.Meta) api.ZettelRights
}
func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
sf := dze.sf
result := make([]sx.Object, len(ml)+1)
result[0] = sf.MustMake("list")
symID, symZettel := sf.MustMake("id"), sf.MustMake("zettel")
for i, m := range ml {
msz := sexp.EncodeMetaRights(api.MetaRights{
Meta: m.Map(),
Rights: dze.getRights(m),
})
msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
result[i+1] = msz
}
_, err := sx.Print(w, sx.MakeList(
sf.MustMake("meta-list"),
sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())),
sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())),
sx.MakeList(result...),
))
return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
sf := dze.sf
result := sx.Nil()
for aggKey, metaList := range arr {
sxMeta := sx.Nil()
for i := len(metaList) - 1; i >= 0; i-- {
sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
}
sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
result = result.Cons(sxMeta)
}
_, err := sx.Print(w, sx.MakeList(
sf.MustMake("aggregate"),
sx.MakeString(act),
sx.MakeList(sf.MustMake("query"), sx.MakeString(dze.sq.String())),
sx.MakeList(sf.MustMake("human"), sx.MakeString(dze.sq.Human())),
result.Cons(sf.MustMake("list")),
))
return err
}
// jsonZettelEncoder is DEPRECATED
type jsonZettelEncoder struct {
sq *query.Query
getRights func(*meta.Meta) api.ZettelRights
}
type zidMetaJSON struct {
ID api.ZettelID `json:"id"`
Meta api.ZettelMeta `json:"meta"`
Rights api.ZettelRights `json:"rights"`
}
type zettelListJSON struct {
Query string `json:"query"`
Human string `json:"human"`
List []zidMetaJSON `json:"list"`
}
func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
result := make([]zidMetaJSON, 0, len(ml))
for _, m := range ml {
result = append(result, zidMetaJSON{
ID: api.ZettelID(m.Zid.String()),
Meta: m.Map(),
Rights: jze.getRights(m),
})
}
err := encodeJSONData(w, zettelListJSON{
Query: jze.sq.String(),
Human: jze.sq.Human(),
List: result,
})
return err
}
type mapListJSON struct {
Map api.Aggregate `json:"map"`
}
func (*jsonZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
mm := make(api.Aggregate, len(arr))
for key, metaList := range arr {
zidList := make([]api.ZettelID, 0, len(metaList))
for _, m := range metaList {
zidList = append(zidList, api.ZettelID(m.Zid.String()))
}
mm[key] = zidList
}
return encodeJSONData(w, mapListJSON{Map: mm})
}
|
Changes to web/adapter/api/rename_zettel.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package api import ( "net/http" "net/url" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package api
import (
"net/http"
"net/url"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/usecase"
"zettelstore.de/z/zettel/id"
)
// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
|
| ︙ | ︙ |
Changes to web/adapter/api/request.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package api import ( "io" "net/http" "net/url" | | > > | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package api import ( "io" "net/http" "net/url" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/sexp" "zettelstore.de/sx.fossil/sxreader" "zettelstore.de/z/input" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" ) // getEncoding returns the data encoding selected by the caller. |
| ︙ | ︙ | |||
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
if p == defPart {
return ""
}
return p.String()
}
func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
b, err := io.ReadAll(r.Body)
if err != nil {
return zettel.Zettel{}, err
}
inp := input.NewInput(b)
m := meta.NewFromInput(zid, inp)
return zettel.Zettel{
Meta: m,
Content: zettel.NewContent(inp.Src[inp.Pos:]),
}, nil
| > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
if p == defPart {
return ""
}
return p.String()
}
func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
return zettel.Zettel{}, err
}
inp := input.NewInput(b)
m := meta.NewFromInput(zid, inp)
return zettel.Zettel{
Meta: m,
Content: zettel.NewContent(inp.Src[inp.Pos:]),
}, nil
}
func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
defer r.Body.Close()
rdr := sxreader.MakeReader(r.Body)
obj, err := rdr.Read()
if err != nil {
return zettel.Zettel{}, err
}
zd, err := sexp.ParseZettel(obj)
if err != nil {
return zettel.Zettel{}, err
}
m := meta.New(zid)
for k, v := range zd.Meta {
if !meta.IsComputed(k) {
m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
}
}
var content zettel.Content
if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil {
return zettel.Zettel{}, err
}
return zettel.Zettel{
Meta: m,
Content: content,
}, nil
}
|
Added web/adapter/api/response.go.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------
package api
import (
"bytes"
"net/http"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/web/content"
"zettelstore.de/z/zettel/id"
)
func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
var buf bytes.Buffer
if _, err := sx.Print(&buf, obj); err != nil {
msg := a.log.Fatal().Err(err)
if msg != nil {
if zid.IsValid() {
msg = msg.Zid(zid)
}
msg.Msg("Unable to store object in buffer")
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil
}
return writeBuffer(w, &buf, content.SXPF)
}
|
Changes to web/adapter/api/update_zettel.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package api import ( "net/http" | | > > | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
//-----------------------------------------------------------------------------
package api
import (
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
)
// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
http.NotFound(w, r)
return
}
q := r.URL.Query()
var zettel zettel.Zettel
switch enc, _ := getEncoding(r, q); enc {
case api.EncoderPlain:
zettel, err = buildZettelFromPlainData(r, zid)
case api.EncoderData:
zettel, err = buildZettelFromData(r, zid)
case api.EncoderJson:
zettel, err = buildZettelFromJSONData(r, zid)
default:
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
|
| ︙ | ︙ |
Changes to web/adapter/request.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "net/http" "net/url" "strconv" "strings" | | < | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import (
"net/http"
"net/url"
"strconv"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/kernel"
"zettelstore.de/z/query"
)
// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
err := r.ParseForm()
if err != nil {
kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form")
|
| ︙ | ︙ | |||
49 50 51 52 53 54 55 | result = result.SetSeed(int(si)) break } } } return result } | < < < < < < < < < < < < < < < < < < < < < < < < < < | 48 49 50 51 52 53 54 | result = result.SetSeed(int(si)) break } } } return result } |
Changes to web/adapter/response.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package adapter import ( "errors" "fmt" "net/http" | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package adapter
import (
"errors"
"fmt"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/box"
"zettelstore.de/z/usecase"
)
// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
if len(data) == 0 {
|
| ︙ | ︙ |
Changes to web/adapter/webui/const.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // WebUI related constants. const queryKeyAction = "action" // Values for queryKeyAction const ( valueActionCopy = "copy" valueActionFolge = "folge" valueActionNew = "new" valueActionVersion = "version" ) // Enumeration for queryKeyAction type createAction uint8 const ( | > > | | < | | | < | < | > | < > > | > | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// WebUI related constants.
const queryKeyAction = "action"
// Values for queryKeyAction
const (
valueActionChild = "child"
valueActionCopy = "copy"
valueActionFolge = "folge"
valueActionNew = "new"
valueActionVersion = "version"
)
// Enumeration for queryKeyAction
type createAction uint8
const (
actionChild createAction = iota
actionCopy
actionFolge
actionNew
actionVersion
)
var createActionMap = map[string]createAction{
valueActionChild: actionChild,
valueActionCopy: actionCopy,
valueActionFolge: actionFolge,
valueActionNew: actionNew,
valueActionVersion: actionVersion,
}
func getCreateAction(s string) createAction {
if action, found := createActionMap[s]; found {
return action
}
return actionCopy
}
|
Changes to web/adapter/webui/create_zettel.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "bytes" "context" "net/http" "strings" | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | import ( "bytes" "context" "net/http" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/sx.fossil" "zettelstore.de/z/box" "zettelstore.de/z/encoder/zmkenc" "zettelstore.de/z/evaluator" "zettelstore.de/z/parser" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/web/server" |
| ︙ | ︙ | |||
48 49 50 51 52 53 54 |
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
switch op {
| | | | | > > | 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
switch op {
case actionChild:
wui.renderZettelForm(ctx, w, createZettel.PrepareChild(origZettel), "Child Zettel", "", roleData, syntaxData)
case actionCopy:
wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
case actionFolge:
wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
case actionNew:
title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), title, "", roleData, syntaxData)
case actionVersion:
wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
}
}
}
func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
roleData := dataListFromArrangement(ucListRoles.Run(ctx))
syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
|
| ︙ | ︙ | |||
96 97 98 99 100 101 102 |
for _, p := range m.PairsRest() {
sb.WriteString(p.Key)
sb.WriteString(": ")
sb.WriteString(p.Value)
sb.WriteByte('\n')
}
env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
| | | | | | 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
for _, p := range m.PairsRest() {
sb.WriteString(p.Key)
sb.WriteString(": ")
sb.WriteString(p.Value)
sb.WriteByte('\n')
}
env, rb := wui.createRenderEnv(ctx, "form", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
rb.bindString("heading", sx.MakeString(title))
rb.bindString("form-action-url", sx.MakeString(formActionURL))
rb.bindString("role-data", makeStringList(roleData))
rb.bindString("syntax-data", makeStringList(syntaxData))
rb.bindString("meta", sx.MakeString(sb.String()))
if !ztl.Content.IsBinary() {
rb.bindString("content", sx.MakeString(ztl.Content.AsString()))
}
wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
if rb.err == nil {
rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
}
if err := rb.err; err != nil {
wui.reportError(ctx, w, err)
|
| ︙ | ︙ | |||
146 147 148 149 150 151 152 | } } } // MakeGetZettelFromListHandler creates a new HTTP handler to store content of // an existing zettel. func (wui *WebUI) MakeGetZettelFromListHandler( | | | | | 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
}
}
}
// MakeGetZettelFromListHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeGetZettelFromListHandler(
queryMeta *usecase.Query, evaluate *usecase.Evaluate,
ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := adapter.GetQuery(r.URL.Query())
ctx := r.Context()
metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
if err != nil {
wui.reportError(ctx, w, err)
return
}
bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig))
enc := zmkenc.Create()
var zmkContent bytes.Buffer
_, err = enc.WriteBlocks(&zmkContent, &bns)
if err != nil {
wui.reportError(ctx, w, err)
return
}
|
| ︙ | ︙ |
Changes to web/adapter/webui/delete_zettel.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package webui import ( "net/http" | | | | | | | | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
//-----------------------------------------------------------------------------
package webui
import (
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/box"
"zettelstore.de/z/strfun"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(getZettel usecase.GetZettel, getAllZettel usecase.GetAllZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
zs, err := getAllZettel.Run(ctx, zid)
if err != nil {
wui.reportError(ctx, w, err)
return
}
m := zs[0].Meta
user := server.GetUser(ctx)
env, rb := wui.createRenderEnv(
ctx, "delete",
wui.rtConfig.Get(ctx, nil, api.KeyLang), "Delete Zettel "+m.Zid.String(), user)
if len(zs) > 1 {
rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
rb.bindString("incoming", nil)
} else {
rb.bindString("shadowed-box", nil)
rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
}
wui.bindCommonZettelData(ctx, &rb, user, m, nil)
if rb.err == nil {
err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
}
if err != nil {
wui.reportError(ctx, w, err)
}
}
}
func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
zidMap := make(strfun.Set)
addListValues(zidMap, m, api.KeyBackward)
for _, kd := range meta.GetSortedKeyDescriptions() {
inverseKey := kd.Inverse
if inverseKey == "" {
continue
}
|
| ︙ | ︙ |
Changes to web/adapter/webui/edit_zettel.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package webui import ( "net/http" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package webui import ( "net/http" "zettelstore.de/client.fossil/api" "zettelstore.de/z/box" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) // MakeEditGetZettelHandler creates a new HTTP handler to display the |
| ︙ | ︙ |
Changes to web/adapter/webui/forms.go.
| ︙ | ︙ | |||
15 16 17 18 19 20 21 | "errors" "io" "net/http" "regexp" "strings" "unicode" | | | 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | "errors" "io" "net/http" "regexp" "strings" "unicode" "zettelstore.de/client.fossil/api" "zettelstore.de/z/input" "zettelstore.de/z/kernel" "zettelstore.de/z/parser" "zettelstore.de/z/web/content" "zettelstore.de/z/zettel" "zettelstore.de/z/zettel/id" "zettelstore.de/z/zettel/meta" |
| ︙ | ︙ | |||
51 52 53 54 55 56 57 |
} else {
m = meta.New(zid)
}
if postTitle, ok := trimmedFormValue(r, "title"); ok {
m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
}
if postTags, ok := trimmedFormValue(r, "tags"); ok {
| | > > > > > | | | 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
} else {
m = meta.New(zid)
}
if postTitle, ok := trimmedFormValue(r, "title"); ok {
m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
}
if postTags, ok := trimmedFormValue(r, "tags"); ok {
if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
for i, tag := range tags {
if tag[0] != '#' {
tags[i] = "#" + tag
}
}
m.SetList(api.KeyTags, tags)
}
}
if postRole, ok := trimmedFormValue(r, "role"); ok {
m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
}
if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
}
if data := textContent(r); data != nil {
return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil
}
if data, m2 := uploadedContent(r, m); data != nil {
return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil
|
| ︙ | ︙ |
Changes to web/adapter/webui/get_info.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "context" "net/http" "sort" "strings" | | | | | | | | | | | | | | | < | > | | | | | | | | | | > > > > > > > > > > > > > > > > > > | | | | | | < | | | | | | | | | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
import (
"context"
"net/http"
"sort"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/collect"
"zettelstore.de/z/encoder"
"zettelstore.de/z/evaluator"
"zettelstore.de/z/parser"
"zettelstore.de/z/query"
"zettelstore.de/z/strfun"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/id"
)
// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
ucParseZettel usecase.ParseZettel,
ucEvaluate *usecase.Evaluate,
ucGetZettel usecase.GetZettel,
ucGetAllMeta usecase.GetAllZettel,
ucQuery *usecase.Query,
) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
if err != nil {
wui.reportError(ctx, w, err)
return
}
enc := wui.getSimpleHTMLEncoder()
getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
evalMeta := func(val string) ast.InlineSlice {
return ucEvaluate.RunMetadata(ctx, val)
}
pairs := zn.Meta.ComputedPairs()
metadata := sx.Nil()
for i := len(pairs) - 1; i >= 0; i-- {
key := pairs[i].Key
sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval))
}
summary := collect.References(zn)
locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
phrase := q.Get(api.QueryKeyPhrase)
if phrase == "" {
phrase = title
}
unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
if err != nil {
wui.reportError(ctx, w, err)
return
}
bns := ucEvaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig))
unlinkedContent, _, err := enc.BlocksSxn(&bns)
if err != nil {
wui.reportError(ctx, w, err)
return
}
encTexts := encodingTexts()
shadowLinks := getShadowLinks(ctx, zid, ucGetAllMeta)
user := server.GetUser(ctx)
env, rb := wui.createRenderEnv(ctx, "info", wui.rtConfig.Get(ctx, nil, api.KeyLang), title, user)
rb.bindString("metadata", metadata)
rb.bindString("local-links", locLinks)
rb.bindString("query-links", queryLinks)
rb.bindString("ext-links", extLinks)
rb.bindString("unlinked-content", unlinkedContent)
rb.bindString("phrase", sx.MakeString(phrase))
rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
rb.bindString("shadow-links", shadowLinks)
wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
if rb.err == nil {
err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
}
if err != nil {
wui.reportError(ctx, w, err)
}
}
}
func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
for i := len(links) - 1; i >= 0; i-- {
ref := links[i]
if ref.State == ast.RefStateSelf || ref.IsZettel() {
continue
}
if ref.State == ast.RefStateQuery {
queries = queries.Cons(
sx.Cons(
sx.MakeString(ref.Value),
sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))
continue
}
if ref.IsExternal() {
extLinks = extLinks.Cons(sx.MakeString(ref.String()))
continue
}
locLinks = locLinks.Cons(sx.Cons(sx.MakeBoolean(ref.IsValid()), sx.MakeString(ref.String())))
}
return locLinks, queries, extLinks
}
func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
var sb strings.Builder
sb.Write(zid.Bytes())
sb.WriteByte(' ')
sb.WriteString(api.UnlinkedDirective)
for _, word := range strfun.MakeWords(phrase) {
sb.WriteByte(' ')
sb.WriteString(api.PhraseDirective)
sb.WriteByte(' ')
sb.WriteString(word)
}
sb.WriteByte(' ')
sb.WriteString(api.OrderDirective)
sb.WriteByte(' ')
sb.WriteString(api.KeyID)
return query.Parse(sb.String())
}
func encodingTexts() []string {
encodings := encoder.GetEncodings()
encTexts := make([]string, 0, len(encodings))
for _, f := range encodings {
encTexts = append(encTexts, f.String())
}
sort.Strings(encTexts)
return encTexts
}
var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}
func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
matrix := sx.Nil()
u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
for ip := len(apiParts) - 1; ip >= 0; ip-- {
part := apiParts[ip]
row := sx.Nil()
for je := len(encTexts) - 1; je >= 0; je-- {
enc := encTexts[je]
if parseOnly {
u.AppendKVQuery(api.QueryKeyParseOnly, "")
}
u.AppendKVQuery(api.QueryKeyPart, part)
u.AppendKVQuery(api.QueryKeyEncoding, enc)
row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String())))
u.ClearQuery()
}
matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row))
}
return matrix
}
func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
matrix := wui.infoAPIMatrix(zid, true, encTexts)
u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
line, isLine := sx.GetPair(row.Car())
if !isLine || line == nil {
continue
}
last := line.LastPair()
part := apiParts[i]
u.AppendKVQuery(api.QueryKeyPart, part)
last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String())))
u.ClearQuery()
if i < 2 {
u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
u.AppendKVQuery(api.QueryKeyPart, part)
last = last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String())))
u.ClearQuery()
u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
u.AppendKVQuery(api.QueryKeyPart, part)
last.AppendBang(sx.Cons(sx.MakeString("json"), sx.MakeString(u.String())))
u.ClearQuery()
}
i++
}
return matrix
}
func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
result := sx.Nil()
if zl, err := getAllZettel.Run(ctx, zid); err == nil {
for i := len(zl) - 1; i >= 1; i-- {
if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
result = result.Cons(sx.MakeString(boxNo))
}
}
}
return result
}
|
Changes to web/adapter/webui/get_zettel.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package webui import ( "context" "net/http" | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package webui
import (
"context"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/box"
"zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getZettel usecase.GetZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
|
| ︙ | ︙ | |||
52 53 54 55 56 57 58 |
cssRoleURL, err := wui.getCSSRoleURL(ctx, zn.InhMeta)
if err != nil {
wui.reportError(ctx, w, err)
return
}
user := server.GetUser(ctx)
| | | | > > > > > > < | 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
cssRoleURL, err := wui.getCSSRoleURL(ctx, zn.InhMeta)
if err != nil {
wui.reportError(ctx, w, err)
return
}
user := server.GetUser(ctx)
getTextTitle := wui.makeGetTextTitle(ctx, getZettel)
title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
env, rb := wui.createRenderEnv(ctx, "zettel", wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), title, user)
rb.bindSymbol(wui.symMetaHeader, metaObj)
rb.bindString("css-role-url", sx.MakeString(cssRoleURL))
rb.bindString("heading", sx.MakeString(title))
if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
}
if role, found := zn.InhMeta.Get(api.KeyFolgeRole); found && role != "" {
rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
}
rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
rb.bindString("superior-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeySuperior, getTextTitle))
rb.bindString("content", content)
rb.bindString("endnotes", endnotes)
rb.bindString("folge-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyFolge, getTextTitle))
rb.bindString("subordinate-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySubordinates, getTextTitle))
rb.bindString("back-links", wui.zettelLinksSxn(zn.InhMeta, api.KeyBack, getTextTitle))
rb.bindString("successor-links", wui.zettelLinksSxn(zn.InhMeta, api.KeySuccessors, getTextTitle))
wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
|
| ︙ | ︙ | |||
91 92 93 94 95 96 97 |
}
if cssZid == id.Invalid {
return "", nil
}
return wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String(), nil
}
| | | < < < < < < < < | | | | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
}
if cssZid == id.Invalid {
return "", nil
}
return wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String(), nil
}
func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
if values, ok := m.GetList(key); ok {
return wui.transformIdentifierSet(values, getTextTitle)
}
return nil
}
func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
values, ok := m.GetList(key)
if !ok || len(values) == 0 {
return nil
}
return wui.zidLinksSxn(values, getTextTitle)
}
func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
for i := len(values) - 1; i >= 0; i-- {
val := values[i]
zid, err := id.Parse(val)
if err != nil {
continue
}
if title, found := getTextTitle(zid); found > 0 {
url := sx.MakeString(wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String())
if title == "" {
lst = lst.Cons(sx.Cons(sx.MakeString(val), url))
} else {
lst = lst.Cons(sx.Cons(sx.MakeString(title), url))
}
}
}
return lst
}
|
Changes to web/adapter/webui/home.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package webui import ( "context" "errors" "net/http" | | | | < | | | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package webui
import (
"context"
"errors"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/box"
"zettelstore.de/z/config"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
)
type getRootStore interface {
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}
// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.URL.Path != "/" {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
apiHomeZid := api.ZettelID(homeZid.String())
if homeZid != id.DefaultHomeZid {
if _, err := s.GetZettel(ctx, homeZid); err == nil {
wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
return
}
homeZid = id.DefaultHomeZid
}
_, err := s.GetZettel(ctx, homeZid)
if err == nil {
wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
return
}
if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
wui.redirectFound(w, r, wui.NewURLBuilder('i'))
return
|
| ︙ | ︙ |
Changes to web/adapter/webui/htmlgen.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package webui import ( "net/url" "strings" | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
package webui
import (
"net/url"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/attrs"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/client.fossil/shtml"
"zettelstore.de/client.fossil/sz"
"zettelstore.de/sx.fossil"
"zettelstore.de/sx.fossil/sxeval"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoder"
"zettelstore.de/z/encoder/szenc"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/meta"
)
// Builder allows to build new URLs for the web service.
type urlBuilder interface {
GetURLPrefix() string
NewURLBuilder(key byte) *api.URLBuilder
}
type htmlGenerator struct {
tx *szenc.Transformer
th *shtml.Transformer
symAt *sx.Symbol
}
func (wui *WebUI) createGenerator(builder urlBuilder) *htmlGenerator {
th := shtml.NewTransformer(1, wui.sf)
symA := wui.symA
symImg := th.Make("img")
symAttr := wui.symAttr
symHref := wui.symHref
symClass := th.Make("class")
symTarget := th.Make("target")
symRel := th.Make("rel")
findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) {
pair, isPair := sx.GetPair(obj)
if !isPair || !symA.IsEqual(pair.Car()) {
return nil, nil, nil
}
rest = pair.Tail()
if rest == nil {
return nil, nil, nil
}
objA := rest.Car()
attr, isPair = sx.GetPair(objA)
if !isPair || !symAttr.IsEqual(attr.Car()) {
return nil, nil, nil
}
return attr, attr.Tail(), rest.Tail()
}
linkZettel := func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
obj, err := prevFn.Call(nil, nil, args)
if err != nil {
return sx.Nil()
}
attr, assoc, rest := findA(obj)
if attr == nil {
return obj
}
hrefP := assoc.Assoc(symHref)
if hrefP == nil {
return obj
}
href, ok := sx.GetString(hrefP.Cdr())
if !ok {
return obj
}
zid, fragment, hasFragment := strings.Cut(href.String(), "#")
u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
if hasFragment {
u = u.SetFragment(fragment)
}
assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
}
th.SetRebinder(func(te *shtml.TransformEnv) {
te.Rebind(sz.NameSymLinkZettel, linkZettel)
te.Rebind(sz.NameSymLinkFound, linkZettel)
te.Rebind(sz.NameSymLinkBased, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
obj, err := prevFn.Call(nil, nil, args)
if err != nil {
return sx.Nil()
}
attr, assoc, rest := findA(obj)
if attr == nil {
return obj
}
hrefP := assoc.Assoc(symHref)
if hrefP == nil {
return obj
}
href, ok := sx.GetString(hrefP.Cdr())
if !ok {
return obj
}
u := builder.NewURLBuilder('/').SetRawLocal(href.String())
assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
})
te.Rebind(sz.NameSymLinkQuery, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
obj, err := prevFn.Call(nil, nil, args)
if err != nil {
return sx.Nil()
}
attr, assoc, rest := findA(obj)
if attr == nil {
return obj
}
hrefP := assoc.Assoc(symHref)
if hrefP == nil {
return obj
}
href, ok := sx.GetString(hrefP.Cdr())
if !ok {
return obj
}
ur, err := url.Parse(href.String())
if err != nil {
return obj
}
q := ur.Query().Get(api.QueryKeyQuery)
if q == "" {
return obj
}
u := builder.NewURLBuilder('h').AppendQuery(q)
assoc = assoc.Cons(sx.Cons(symHref, sx.MakeString(u.String())))
return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
})
te.Rebind(sz.NameSymLinkExternal, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
obj, err := prevFn.Call(nil, nil, args)
if err != nil {
return sx.Nil()
}
attr, assoc, rest := findA(obj)
if attr == nil {
return obj
}
assoc = assoc.Cons(sx.Cons(symClass, sx.MakeString("external"))).
Cons(sx.Cons(symTarget, sx.MakeString("_blank"))).
Cons(sx.Cons(symRel, sx.MakeString("noopener noreferrer")))
return rest.Cons(assoc.Cons(symAttr)).Cons(symA)
})
te.Rebind(sz.NameSymEmbed, func(args []sx.Object, prevFn sxeval.Callable) sx.Object {
obj, err := prevFn.Call(nil, nil, args)
if err != nil {
return sx.Nil()
}
pair, isPair := sx.GetPair(obj)
if !isPair || !symImg.IsEqual(pair.Car()) {
return obj
}
attr, isPair := sx.GetPair(pair.Tail().Car())
if !isPair || !symAttr.IsEqual(attr.Car()) {
return obj
}
symSrc := th.Make("src")
srcP := attr.Tail().Assoc(symSrc)
if srcP == nil {
return obj
}
src, isString := sx.GetString(srcP.Cdr())
if !isString {
return obj
}
zid := api.ZettelID(src)
if !zid.IsValid() {
return obj
}
u := builder.NewURLBuilder('z').SetZid(zid)
imgAttr := attr.Tail().Cons(sx.Cons(symSrc, sx.MakeString(u.String()))).Cons(symAttr)
return pair.Tail().Tail().Cons(imgAttr).Cons(symImg)
})
})
return &htmlGenerator{
tx: szenc.NewTransformer(),
th: th,
symAt: symAttr,
}
}
// SetUnique sets a prefix to make several HTML ids unique.
func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g }
var mapMetaKey = map[string]string{
api.KeyCopyright: "copyright",
api.KeyLicense: "license",
}
func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
tm := g.tx.GetMeta(m, evalMeta)
hm, err := g.th.Transform(tm)
if err != nil {
return nil
}
ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
metaMap := make(map[string]*sx.Pair, m.Length())
if tags, ok := m.Get(api.KeyTags); ok {
metaMap[api.KeyTags] = g.transformMetaTags(tags)
ignore.Set(api.KeyTags)
}
for elem := hm; elem != nil; elem = elem.Tail() {
mlst, isPair := sx.GetPair(elem.Car())
if !isPair {
continue
}
att, isPair := sx.GetPair(mlst.Tail().Car())
if !isPair {
continue
}
if !att.Car().IsEqual(g.symAt) {
continue
}
a := make(attrs.Attributes, 32)
for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
if p, ok := sx.GetPair(aelem.Car()); ok {
key := p.Car()
val := p.Cdr()
if tail, isTail := sx.GetPair(val); isTail {
val = tail.Car()
}
a = a.Set(key.String(), val.String())
}
}
name, found := a.Get("name")
if !found || ignore.Has(name) {
continue
}
newName, found := mapMetaKey[name]
if !found {
continue
}
a = a.Set("name", newName)
metaMap[newName] = g.th.TransformMeta(a)
}
result := sx.Nil()
keys := maps.Keys(metaMap)
for i := len(keys) - 1; i >= 0; i-- {
result = result.Cons(metaMap[keys[i]])
}
return result
}
func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair {
var sb strings.Builder
for i, val := range meta.ListFromValue(tags) {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(strings.TrimPrefix(val, "#"))
}
metaTags := sb.String()
if len(metaTags) == 0 {
return nil
}
return g.th.TransformMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}
func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
if bs == nil || len(*bs) == 0 {
return nil, nil, nil
}
sx := g.tx.GetSz(bs)
sh, err := g.th.Transform(sx)
if err != nil {
return nil, nil, err
}
return sh, g.th.Endnotes(), nil
}
// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
if is == nil || len(*is) == 0 {
return nil
}
sx := g.tx.GetSz(is)
sh, err := g.th.Transform(sx)
if err != nil {
return nil
}
return sh
}
|
Changes to web/adapter/webui/htmlmeta.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package webui import ( "context" "errors" | < < < | | | | < | | | | | | | > > > | > > > > > | > > > > | | | | < | | | | | | | | | | | | | | | | | | > | < < < < < < < | | < | | < < < < < < < < < < | < | < < < < < < < < < < < < < < < < < | | | | | | | | | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
//-----------------------------------------------------------------------------
package webui
import (
"context"
"errors"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/sx.fossil/sxhtml"
"zettelstore.de/z/ast"
"zettelstore.de/z/box"
"zettelstore.de/z/parser"
"zettelstore.de/z/usecase"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func (wui *WebUI) writeHTMLMetaValue(
key, value string,
getTextTitle getTextTitleFunc,
evalMetadata evalMetadataFunc,
gen *htmlGenerator,
) sx.Object {
switch kt := meta.Type(key); kt {
case meta.TypeCredential:
return sx.MakeString(value)
case meta.TypeEmpty:
return sx.MakeString(value)
case meta.TypeID:
return wui.transformIdentifier(value, getTextTitle)
case meta.TypeIDSet:
return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
case meta.TypeNumber:
return wui.transformLink(key, value, value)
case meta.TypeString:
return sx.MakeString(value)
case meta.TypeTagSet:
return wui.transformTagSet(key, meta.ListFromValue(value))
case meta.TypeTimestamp:
if ts, ok := meta.TimeValue(value); ok {
return sx.MakeList(
wui.sf.MustMake("time"),
sx.MakeList(
wui.symAttr,
sx.Cons(wui.sf.MustMake("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
),
sx.MakeList(wui.sf.MustMake(sxhtml.NameSymNoEscape), sx.MakeString(ts.Format("2006-01-02 15:04:05"))),
)
}
return sx.Nil()
case meta.TypeURL:
text := sx.MakeString(value)
if res, err := wui.url2html([]sx.Object{text}); err == nil {
return res
}
return text
case meta.TypeWord:
return wui.transformLink(key, value, value)
case meta.TypeWordSet:
return wui.transformWordSet(key, meta.ListFromValue(value))
case meta.TypeZettelmarkup:
return wui.transformZmkMetadata(value, evalMetadata, gen)
default:
return sx.MakeList(wui.sf.MustMake("b"), sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name))
}
}
func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
text := sx.MakeString(val)
zid, err := id.Parse(val)
if err != nil {
return text
}
title, found := getTextTitle(zid)
switch {
case found > 0:
ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
attrs := sx.Nil()
if title != "" {
attrs = attrs.Cons(sx.Cons(wui.sf.MustMake("title"), sx.MakeString(title)))
}
attrs = attrs.Cons(sx.Cons(wui.symHref, sx.MakeString(ub.String()))).Cons(wui.symAttr)
return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(wui.symA)
case found == 0:
return sx.MakeList(wui.sf.MustMake("s"), text)
default: // case found < 0:
return text
}
}
func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
if len(vals) == 0 {
return nil
}
space := sx.MakeString(" ")
text := make([]sx.Object, 0, 2*len(vals))
for _, val := range vals {
text = append(text, space, wui.transformIdentifier(val, getTextTitle))
}
return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}
func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
if len(tags) == 0 {
return nil
}
space := sx.MakeString(" ")
text := make([]sx.Object, 0, 2*len(tags))
for _, tag := range tags {
text = append(text, space, wui.transformLink(key, tag, tag))
}
return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}
func (wui *WebUI) transformWordSet(key string, words []string) sx.Object {
if len(words) == 0 {
return sx.Nil()
}
space := sx.MakeString(" ")
text := make([]sx.Object, 0, 2*len(words))
for _, word := range words {
text = append(text, space, wui.transformLink(key, word, word))
}
return sx.MakeList(text[1:]...).Cons(wui.symSpan)
}
func (wui *WebUI) transformLink(key, value, text string) *sx.Pair {
return sx.MakeList(
wui.symA,
sx.MakeList(
wui.symAttr,
sx.Cons(wui.symHref, sx.MakeString(wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value).String())),
),
sx.MakeString(text),
)
}
type evalMetadataFunc = func(string) ast.InlineSlice
func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }
}
type getTextTitleFunc func(id.Zid) (string, int)
func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc {
return func(zid id.Zid) (string, int) {
z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
if err != nil {
if errors.Is(err, &box.ErrNotAllowed{}) {
return "", -1
}
return "", 0
}
return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1
}
}
func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object {
is := evalMetadata(value)
return gen.InlinesSxHTML(&is).Cons(wui.symSpan)
}
|
Changes to web/adapter/webui/lists.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 | package webui import ( "context" "io" "net/http" "strings" | > | | | | | | | | | | | | | | > > | | > > > | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
package webui
import (
"context"
"io"
"net/http"
"strconv"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/z/ast"
"zettelstore.de/z/encoding/atom"
"zettelstore.de/z/encoding/rss"
"zettelstore.de/z/encoding/xml"
"zettelstore.de/z/evaluator"
"zettelstore.de/z/query"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(queryMeta *usecase.Query) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
q := adapter.GetQuery(r.URL.Query())
q = q.SetDeterministic()
ctx := r.Context()
metaSeq, err := queryMeta.Run(ctx, q)
if err != nil {
wui.reportError(ctx, w, err)
return
}
if actions := q.Actions(); len(actions) > 0 {
switch actions[0] {
case "ATOM":
wui.renderAtom(w, q, metaSeq)
return
case "RSS":
wui.renderRSS(ctx, w, q, metaSeq)
return
}
}
var content, endnotes *sx.Pair
if bn := evaluator.QueryAction(ctx, q, metaSeq, wui.rtConfig); bn != nil {
enc := wui.getSimpleHTMLEncoder()
content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
if err != nil {
wui.reportError(ctx, w, err)
return
}
}
user := server.GetUser(ctx)
env, rb := wui.createRenderEnv(
ctx, "list",
wui.rtConfig.Get(ctx, nil, api.KeyLang),
wui.rtConfig.GetSiteName(), user)
if q == nil {
rb.bindString("heading", sx.MakeString(wui.rtConfig.GetSiteName()))
} else {
var sb strings.Builder
q.PrintHuman(&sb)
rb.bindString("heading", sx.MakeString(sb.String()))
}
rb.bindString("query-value", sx.MakeString(q.String()))
rb.bindString("content", content)
rb.bindString("endnotes", endnotes)
apiURL := wui.NewURLBuilder('z').AppendQuery(q.String())
seed, found := q.GetSeed()
if found {
apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
} else {
seed = 0
}
rb.bindString("plain-url", sx.MakeString(apiURL.String()))
rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
if wui.canCreate(ctx, user) {
rb.bindString("create-url", sx.MakeString(wui.createNewURL))
rb.bindString("seed", sx.Int64(seed))
}
if rb.err == nil {
err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
}
if err != nil {
wui.reportError(ctx, w, err)
}
|
| ︙ | ︙ |
Changes to web/adapter/webui/login.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package webui import ( "context" "net/http" | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package webui import ( "context" "net/http" "zettelstore.de/client.fossil/api" "zettelstore.de/sx.fossil" "zettelstore.de/z/auth" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" "zettelstore.de/z/zettel/id" ) // MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view, |
| ︙ | ︙ | |||
34 35 36 37 38 39 40 |
}
wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
}
}
func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil)
| < | | 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
}
wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
}
}
func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
env, rb := wui.createRenderEnv(ctx, "login", wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", nil)
rb.bindString("retry", sx.MakeBoolean(retry))
if rb.err == nil {
rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env)
}
if err := rb.err; err != nil {
wui.reportError(ctx, w, err)
}
}
|
| ︙ | ︙ |
Changes to web/adapter/webui/rename_zettel.go.
| ︙ | ︙ | |||
11 12 13 14 15 16 17 | package webui import ( "fmt" "net/http" "strings" | | | | > | | 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
package webui
import (
"fmt"
"net/http"
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/box"
"zettelstore.de/z/usecase"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/id"
)
// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getZettel usecase.GetZettel) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
zid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
z, err := getZettel.Run(ctx, zid)
if err != nil {
wui.reportError(ctx, w, err)
return
}
m := z.Meta
user := server.GetUser(ctx)
env, rb := wui.createRenderEnv(
ctx, "rename",
wui.rtConfig.Get(ctx, nil, api.KeyLang), "Rename Zettel "+m.Zid.String(), user)
rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
wui.bindCommonZettelData(ctx, &rb, user, m, nil)
if rb.err == nil {
err = wui.renderSxnTemplate(ctx, w, id.RenameTemplateZid, env)
}
if err != nil {
wui.reportError(ctx, w, err)
}
|
| ︙ | ︙ | |||
62 63 64 65 66 67 68 69 70 71 |
curZid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
if err = r.ParseForm(); err != nil {
wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
return
}
| > > | > > | > > | 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
curZid, err := id.Parse(r.URL.Path[1:])
if err != nil {
wui.reportError(ctx, w, box.ErrNotFound)
return
}
if err = r.ParseForm(); err != nil {
wui.log.Trace().Err(err).Msg("unable to read rename zettel form")
wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
return
}
formCurZidStr := r.PostFormValue("curzid")
if formCurZid, err1 := id.Parse(formCurZidStr); err1 != nil || formCurZid != curZid {
if err1 != nil {
wui.log.Trace().Str("formCurzid", formCurZidStr).Err(err1).Msg("unable to parse as zid")
} else if formCurZid != curZid {
wui.log.Trace().Zid(formCurZid).Zid(curZid).Msg("zid differ (form/url)")
}
wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
return
}
formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
newZid, err := id.Parse(formNewZid)
if err != nil {
wui.reportError(
|
| ︙ | ︙ |
Changes to web/adapter/webui/response.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package webui import ( "net/http" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//-----------------------------------------------------------------------------
package webui
import (
"net/http"
"zettelstore.de/client.fossil/api"
)
func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
us := ub.String()
wui.log.Debug().Str("uri", us).Msg("redirect")
http.Redirect(w, r, us, http.StatusFound)
}
|
Changes to web/adapter/webui/template.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 17 18 | package webui import ( "bytes" "context" "fmt" "net/http" | > > | | | | | | | > | | | | | | | | | > > | | | | | < | | | < < < < < < < < < < < < < < < < | | | < | < < | | | < < > > > | | | < < < < | | < < < < < < | < < < < < < < < < < < < < < < < < < < < < < | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | | | | | < < | | | | | | | | | | | | > > | > > > > > > > > > > > > > | > > > > > > | > > | < < < < | > > > > > > > > > | | | | | | | > > > > > > > | | | | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
package webui
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"zettelstore.de/client.fossil/api"
"zettelstore.de/sx.fossil"
"zettelstore.de/sx.fossil/sxbuiltins"
"zettelstore.de/sx.fossil/sxbuiltins/binding"
"zettelstore.de/sx.fossil/sxbuiltins/boolean"
"zettelstore.de/sx.fossil/sxbuiltins/callable"
"zettelstore.de/sx.fossil/sxbuiltins/cond"
"zettelstore.de/sx.fossil/sxbuiltins/define"
"zettelstore.de/sx.fossil/sxbuiltins/env"
"zettelstore.de/sx.fossil/sxbuiltins/list"
"zettelstore.de/sx.fossil/sxbuiltins/quote"
"zettelstore.de/sx.fossil/sxeval"
"zettelstore.de/sx.fossil/sxhtml"
"zettelstore.de/sx.fossil/sxreader"
"zettelstore.de/z/box"
"zettelstore.de/z/collect"
"zettelstore.de/z/config"
"zettelstore.de/z/parser"
"zettelstore.de/z/web/adapter"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
func (wui *WebUI) createRenderEngine() *sxeval.Engine {
root := sx.MakeRootEnvironment()
engine := sxeval.MakeEngine(wui.sf, root)
quote.InstallQuoteSyntax(root, wui.symQuote)
quote.InstallQuasiQuoteSyntax(root, wui.symQQ, wui.symUQ, wui.symUQS)
engine.BindSyntax("if", cond.IfS)
engine.BindSyntax("and", boolean.AndS)
engine.BindSyntax("or", boolean.OrS)
engine.BindSyntax("lambda", callable.LambdaS)
engine.BindSyntax("define", define.DefineS)
engine.BindSyntax("let", binding.LetS)
engine.BindBuiltinEEA("bound?", env.BoundP)
engine.BindBuiltinEEA("map", callable.Map)
engine.BindBuiltinEEA("apply", callable.Apply)
engine.BindBuiltinA("list", list.List)
engine.BindBuiltinA("append", list.Append)
engine.BindBuiltinA("car", list.Car)
engine.BindBuiltinA("cdr", list.Cdr)
engine.BindBuiltinA("url-to-html", wui.url2html)
return engine
}
func (wui *WebUI) url2html(args []sx.Object) (sx.Object, error) {
err := sxbuiltins.CheckArgs(args, 1, 1)
text, err := sxbuiltins.GetString(err, args, 0)
if err != nil {
return nil, err
}
if u, errURL := url.Parse(text.String()); errURL == nil {
if us := u.String(); us != "" {
return sx.MakeList(
wui.symA,
sx.MakeList(
wui.symAttr,
sx.Cons(wui.symHref, sx.MakeString(us)),
sx.Cons(wui.sf.MustMake("target"), sx.MakeString("_blank")),
sx.Cons(wui.sf.MustMake("rel"), sx.MakeString("noopener noreferrer")),
),
text), nil
}
}
return text, nil
}
// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (sx.Environment, renderBinder) {
userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
env := sx.MakeChildEnvironment(wui.engine.RootEnvironment(), name, 128)
rb := makeRenderBinder(wui.sf, env, nil)
rb.bindString("lang", sx.MakeString(lang))
rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
rb.bindString("css-role-url", sx.MakeString(""))
rb.bindString("title", sx.MakeString(title))
rb.bindString("home-url", sx.MakeString(wui.homeURL))
rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
rb.bindString("user-zettel-url", sx.MakeString(userZettelURL))
rb.bindString("user-ident", sx.MakeString(userIdent))
rb.bindString("login-url", sx.MakeString(wui.loginURL))
rb.bindString("logout-url", sx.MakeString(wui.logoutURL))
rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL))
rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL))
rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL))
if wui.canRefresh(user) {
rb.bindString("refresh-url", sx.MakeString(wui.refreshURL))
}
rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
rb.bindString("search-url", sx.MakeString(wui.searchURL))
rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
rb.bindSymbol(wui.symMetaHeader, sx.Nil())
rb.bindSymbol(wui.symDetail, sx.Nil())
return env, rb
}
func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
if user == nil {
return false, "", ""
}
return true, wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String(), user.GetDefault(api.KeyUserID, "")
}
type renderBinder struct {
err error
make func(string) (*sx.Symbol, error)
bind func(*sx.Symbol, sx.Object) error
}
func makeRenderBinder(sf sx.SymbolFactory, env sx.Environment, err error) renderBinder {
return renderBinder{make: sf.Make, bind: env.Bind, err: err}
}
func (rb *renderBinder) bindString(key string, obj sx.Object) {
if rb.err == nil {
sym, err := rb.make(key)
if err == nil {
rb.err = rb.bind(sym, obj)
return
}
rb.err = err
}
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
if rb.err == nil {
rb.err = rb.bind(sym, obj)
}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
rb.bindString("meta-"+key, sx.MakeString(value))
if kt := meta.Type(key); kt.IsSet {
rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
}
}
func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
strZid := m.Zid.String()
apiZid := api.ZettelID(strZid)
newURLBuilder := wui.NewURLBuilder
rb.bindString("zid", sx.MakeString(strZid))
rb.bindString("web-url", sx.MakeString(wui.NewURLBuilder('h').SetZid(apiZid).String()))
if content != nil && wui.canWrite(ctx, user, m, *content) {
rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String()))
}
rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String()))
if wui.canCreate(ctx, user) {
if content != nil && !content.IsBinary() {
rb.bindString("copy-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
}
rb.bindString("version-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
rb.bindString("child-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionChild).String()))
rb.bindString("folge-url", sx.MakeString(wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
}
if wui.canRename(ctx, user, m) {
rb.bindString("rename-url", sx.MakeString(wui.NewURLBuilder('b').SetZid(apiZid).String()))
}
if wui.canDelete(ctx, user, m) {
rb.bindString("delete-url", sx.MakeString(wui.NewURLBuilder('d').SetZid(apiZid).String()))
}
if val, found := m.Get(api.KeyUselessFiles); found {
rb.bindString("useless", sx.Cons(sx.MakeString(val), nil))
}
rb.bindString("context-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(strZid+" "+api.ContextDirective).String()))
// Ensure to have title, role, tags, and syntax included as "meta-*"
rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, ""))
sentinel := sx.Cons(nil, nil)
curr := sentinel
for _, p := range m.ComputedPairs() {
key, value := p.Key, p.Value
curr = curr.AppendBang(sx.Cons(sx.MakeString(key), sx.MakeString(value)))
rb.bindKeyValue(key, value)
}
rb.bindString("metapairs", sentinel.Tail())
}
func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
if !wui.canCreate(ctx, user) {
return nil
}
ctx = box.NoEnrichContext(ctx)
menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
if err != nil {
return nil
}
refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))
for i := len(refs) - 1; i >= 0; i-- {
zid, err2 := id.Parse(refs[i].URL.Path)
if err2 != nil {
continue
}
z, err2 := wui.box.GetZettel(ctx, zid)
if err2 != nil {
continue
}
if !wui.policy.CanRead(user, z.Meta) {
continue
}
text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle()))
link := sx.MakeString(wui.NewURLBuilder('c').SetZid(api.ZettelID(zid.String())).
AppendKVQuery(queryKeyAction, valueActionNew).String())
lst = lst.Cons(sx.Cons(text, link))
}
return lst
}
func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
htmlEnc := wui.getSimpleHTMLEncoder().SetUnique("footer-")
if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
if content != nil && endnotes != nil {
content.LastPair().SetCdr(sx.Cons(endnotes, nil))
}
return content
}
}
}
return nil
}
func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid) error {
if expr := wui.getSxnCache(zid); expr != nil {
return nil
}
rdr, err := wui.makeZettelReader(ctx, zid)
if err != nil {
return err
}
for {
form, err2 := rdr.Read()
if err2 != nil {
if err2 == io.EOF {
wui.setSxnCache(zid, sxeval.TrueExpr) // Hack to load only once
return nil
}
return err2
}
wui.log.Trace().Str("form", form.Repr()).Msg("Load sxn code")
_, err2 = wui.engine.Eval(wui.engine.GetToplevelEnv(), form)
if err2 != nil {
return err2
}
}
}
func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sxeval.Expr, error) {
if t := wui.getSxnCache(zid); t != nil {
return t, nil
}
reader, err := wui.makeZettelReader(ctx, zid)
if err != nil {
return nil, err
}
objs, err := reader.ReadAll()
if err != nil {
wui.log.IfErr(err).Zid(zid).Msg("reading sxn template")
return nil, err
}
if len(objs) != 1 {
return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
}
t, err := wui.engine.Parse(env, objs[0])
if err != nil {
return nil, err
}
wui.setSxnCache(zid, wui.engine.Rework(env, t))
return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
ztl, err := wui.box.GetZettel(ctx, zid)
if err != nil {
return nil, err
}
reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()), sxreader.WithSymbolFactory(wui.sf))
quote.InstallQuoteReader(reader, wui.symQuote, '\'')
quote.InstallQuasiQuoteReader(reader, wui.symQQ, '`', wui.symUQ, ',', wui.symUQS, '@')
return reader, nil
}
func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, env sx.Environment) (sx.Object, error) {
templateExpr, err := wui.getSxnTemplate(ctx, zid, env)
if err != nil {
return nil, err
}
return wui.engine.Execute(env, templateExpr)
}
func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, env sx.Environment) error {
return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, env)
}
func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, env sx.Environment) error {
err := wui.loadSxnCodeZettel(ctx, id.TemplateSxnZid)
if err != nil {
return err
}
detailObj, err := wui.evalSxnTemplate(ctx, templateID, env)
if err != nil {
return err
}
env.Bind(wui.symDetail, detailObj)
pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, env)
if err != nil {
return err
}
wui.log.Debug().Str("page", pageObj.Repr()).Msg("render")
gen := sxhtml.NewGenerator(wui.sf, sxhtml.WithNewline)
var sb bytes.Buffer
_, err = gen.WriteHTML(&sb, pageObj)
if err != nil {
return err
}
wui.prepareAndWriteHeader(w, code)
_, err = w.Write(sb.Bytes())
wui.log.IfErr(err).Msg("Unable to write HTML via template")
return nil // No error reporting, since we do not know what happended during write to client.
}
func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
code, text := adapter.CodeMessageFromError(err)
if code == http.StatusInternalServerError {
wui.log.Error().Msg(err.Error())
} else {
wui.log.Trace().Err(err).Msg("reportError")
}
user := server.GetUser(ctx)
env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
rb.bindString("heading", sx.MakeString(http.StatusText(code)))
rb.bindString("message", sx.MakeString(text))
if rb.err == nil {
rb.err = wui.renderSxnTemplate(ctx, w, id.ErrorTemplateZid, env)
}
if errBind := rb.err; errBind != nil {
wui.log.Error().Err(errBind).Msg("while rendering error message")
fmt.Fprintf(w, "Error while rendering error message: %v", errBind)
}
}
func makeStringList(sl []string) *sx.Pair {
if len(sl) == 0 {
return nil
}
result := sx.Nil()
for i := len(sl) - 1; i >= 0; i-- {
result = result.Cons(sx.MakeString(sl[i]))
}
return result
}
|
Changes to web/adapter/webui/webui.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "context" "net/http" "strings" "sync" "time" | | | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | import ( "context" "net/http" "strings" "sync" "time" "zettelstore.de/client.fossil/api" "zettelstore.de/sx.fossil" "zettelstore.de/sx.fossil/sxeval" "zettelstore.de/sx.fossil/sxhtml" "zettelstore.de/z/auth" "zettelstore.de/z/box" "zettelstore.de/z/config" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/usecase" "zettelstore.de/z/web/adapter" |
| ︙ | ︙ | |||
45 46 47 48 49 50 51 | token auth.TokenManager box webuiBox policy auth.Policy evalZettel *usecase.Evaluate mxCache sync.RWMutex | | | | | | | | | | | < < < < | | 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
token auth.TokenManager
box webuiBox
policy auth.Policy
evalZettel *usecase.Evaluate
mxCache sync.RWMutex
templateCache map[id.Zid]sxeval.Expr
mxRoleCSSMap sync.RWMutex
roleCSSMap map[string]id.Zid
tokenLifetime time.Duration
cssBaseURL string
cssUserURL string
homeURL string
listZettelURL string
listRolesURL string
listTagsURL string
refreshURL string
withAuth bool
loginURL string
logoutURL string
searchURL string
createNewURL string
sf sx.SymbolFactory
engine *sxeval.Engine
genHTML *sxhtml.Generator
symQuote, symQQ *sx.Symbol
symUQ, symUQS *sx.Symbol
symMetaHeader *sx.Symbol
symDetail *sx.Symbol
symA, symHref *sx.Symbol
symSpan *sx.Symbol
symAttr *sx.Symbol
}
type webuiBox interface {
CanCreateZettel(ctx context.Context) bool
GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool
AllowRenameZettel(ctx context.Context, zid id.Zid) bool
CanDeleteZettel(ctx context.Context, zid id.Zid) bool
}
// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
loginoutBase := ab.NewURLBuilder('i')
sf := sx.MakeMappedFactory()
wui := &WebUI{
log: log,
debug: kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
ab: ab,
rtConfig: rtConfig,
authz: authz,
|
| ︙ | ︙ | |||
130 131 132 133 134 135 136 |
symQuote: sf.MustMake("quote"),
symQQ: sf.MustMake("quasiquote"),
symUQ: sf.MustMake("unquote"),
symUQS: sf.MustMake("unquote-splicing"),
symDetail: sf.MustMake("DETAIL"),
symMetaHeader: sf.MustMake("META-HEADER"),
symA: sf.MustMake("a"),
| < | | < < < < < < | | > > > > > > > > > > > > > > > > | | | 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
symQuote: sf.MustMake("quote"),
symQQ: sf.MustMake("quasiquote"),
symUQ: sf.MustMake("unquote"),
symUQS: sf.MustMake("unquote-splicing"),
symDetail: sf.MustMake("DETAIL"),
symMetaHeader: sf.MustMake("META-HEADER"),
symA: sf.MustMake("a"),
symHref: sf.MustMake("href"),
symSpan: sf.MustMake("span"),
symAttr: sf.MustMake(sxhtml.NameSymAttr),
}
wui.engine = wui.createRenderEngine()
wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
mgr.RegisterObserver(wui.observe)
return wui
}
func (wui *WebUI) observe(ci box.UpdateInfo) {
wui.mxCache.Lock()
if ci.Reason == box.OnReload {
wui.templateCache = make(map[id.Zid]sxeval.Expr, len(wui.templateCache))
} else {
delete(wui.templateCache, ci.Zid)
}
wui.mxCache.Unlock()
wui.mxRoleCSSMap.Lock()
if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid {
wui.roleCSSMap = nil
}
wui.mxRoleCSSMap.Unlock()
}
func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) {
wui.mxCache.Lock()
wui.templateCache[zid] = expr
wui.mxCache.Unlock()
}
func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr {
wui.mxCache.RLock()
expr, found := wui.templateCache[zid]
wui.mxCache.RUnlock()
if found {
return expr
}
return nil
}
func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m *meta.Meta) (id.Zid, error) {
wui.mxRoleCSSMap.RLock()
if wui.roleCSSMap == nil {
wui.mxRoleCSSMap.RUnlock()
wui.mxRoleCSSMap.Lock()
zMap, err := wui.box.GetZettel(ctx, id.RoleCSSMapZid)
if err == nil {
wui.roleCSSMap = createRoleCSSMap(zMap.Meta)
}
wui.mxRoleCSSMap.Unlock()
if err != nil {
return id.Invalid, err
}
wui.mxRoleCSSMap.RLock()
}
|
| ︙ | ︙ |
Changes to web/content/content.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | // It translates syntax values into content types, and vice versa. package content import ( "mime" "net/http" | | > | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// It translates syntax values into content types, and vice versa.
package content
import (
"mime"
"net/http"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel"
"zettelstore.de/z/zettel/meta"
)
const (
UnknownMIME = "application/octet-stream"
mimeGIF = "image/gif"
mimeHTML = "text/html; charset=utf-8"
mimeJPEG = "image/jpeg"
mimeMarkdown = "text/markdown; charset=utf-8"
JSON = "application/json"
PlainText = "text/plain; charset=utf-8"
mimePNG = "image/png"
SXPF = PlainText
mimeWEBP = "image/webp"
)
var encoding2mime = map[api.EncodingEnum]string{
api.EncoderHTML: mimeHTML,
api.EncoderMD: mimeMarkdown,
api.EncoderSz: SXPF,
api.EncoderSHTML: SXPF,
api.EncoderText: PlainText,
api.EncoderZmk: PlainText,
}
// MIMEFromEncoding returns the MIME encoding for a given zettel encoding
func MIMEFromEncoding(enc api.EncodingEnum) string {
if m, found := encoding2mime[enc]; found {
|
| ︙ | ︙ | |||
59 60 61 62 63 64 65 | meta.SyntaxJPG: mimeJPEG, meta.SyntaxMarkdown: mimeMarkdown, meta.SyntaxMD: mimeMarkdown, meta.SyntaxNone: "", meta.SyntaxPlain: PlainText, meta.SyntaxPNG: mimePNG, meta.SyntaxSVG: "image/svg+xml", | | | 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | meta.SyntaxJPG: mimeJPEG, meta.SyntaxMarkdown: mimeMarkdown, meta.SyntaxMD: mimeMarkdown, meta.SyntaxNone: "", meta.SyntaxPlain: PlainText, meta.SyntaxPNG: mimePNG, meta.SyntaxSVG: "image/svg+xml", meta.SyntaxSxn: SXPF, meta.SyntaxText: PlainText, meta.SyntaxTxt: PlainText, meta.SyntaxWebp: mimeWEBP, meta.SyntaxZmk: "text/x-zmk; charset=utf-8", // Additional syntaxes that are parsed as plain text. "js": "text/javascript; charset=utf-8", |
| ︙ | ︙ |
Changes to web/server/impl/impl.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package impl import ( "context" "net/http" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package impl
import (
"context"
"net/http"
"time"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/auth"
"zettelstore.de/z/logger"
"zettelstore.de/z/web/server"
"zettelstore.de/z/zettel/meta"
)
type myServer struct {
|
| ︙ | ︙ |
Changes to web/server/impl/router.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "io" "net/http" "regexp" "strings" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import ( "io" "net/http" "regexp" "strings" "zettelstore.de/client.fossil/api" "zettelstore.de/z/auth" "zettelstore.de/z/kernel" "zettelstore.de/z/logger" "zettelstore.de/z/web/server" ) type ( |
| ︙ | ︙ |
Changes to web/server/server.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | package server import ( "context" "net/http" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package server
import (
"context"
"net/http"
"time"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)
|
| ︙ | ︙ |
Changes to www/build.md.
1 2 3 4 5 6 7 8 9 10 11 12 | # How to build Zettelstore ## Prerequisites You must install the following software: * A current, supported [release of Go](https://go.dev/doc/devel/release), * [staticcheck](https://staticcheck.io/), * [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow), * [unparam](https://mvdan.cc/unparam), * [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). | | | | < < < | | > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | # How to build Zettelstore ## Prerequisites You must install the following software: * A current, supported [release of Go](https://go.dev/doc/devel/release), * [staticcheck](https://staticcheck.io/), * [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow), * [unparam](https://mvdan.cc/unparam), * [govulncheck](https://golang.org/x/vuln/cmd/govulncheck), * [Fossil](https://fossil-scm.org/), * [Git](https://git-scm.org) (so that Go can download some dependencies). See folder `docs/development` (a zettel box) for details. ## Clone the repository Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki). 1. Create a directory to store your Fossil repositories. Let's assume, you have created `$HOME/fossils`. 1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`. 1. Create a working directory. Let's assume, you have created `$HOME/zettelstore`. 1. Change into this directory: `cd $HOME/zettelstore`. 1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`. ## The build tool In the directory `tools` there is a Go file called `build.go`. It automates most aspects, (hopefully) platform-independent. The script is called as: ``` go run tools/build.go [-v] COMMAND ``` The flag `-v` enables the verbose mode. It outputs all commands called by the tool. Some important `COMMAND`s are: * `build`: builds the software with correct version information and puts it into a freshly created directory `bin`. * `check`: checks the current state of the working directory to be ready for release (or commit). * `clean`: removes the build directories and cleans the Go cache. * `version`: prints the current version information. * `tools`: installs / updates the tools described above: staticcheck, shadow, unparam, govulncheck. Therefore, the easiest way to build your own version of the Zettelstore software is to execute the command ``` go run tools/build.go build ``` In case of errors, please send the output of the verbose execution: ``` go run tools/build.go -v build ``` ## A note on the use of Fossil Zettelstore is managed by the Fossil version control system. Fossil is an alternative to the ubiquitous Git version control system. However, Go seems to prefer Git and popular platforms that just support Git. Some dependencies of Zettelstore, namely [Zettelstore client](https://zettelstore.de/client) and [sx](https://zettelstore.de/sx), are also managed by Fossil. Depending on your development setup, some error messages might occur. If the error message mentions an environment variable called `GOVCS` you should set it to the value `GOVCS=zezzelstore.de:fossil` (alternatively more generous to `GOVCS=*:all`). Since the Go build system is coupled with Git and some special platforms, you allow ot to download a Fossil repository from the host `zettelstore.de`. The build tool set `GOVCS` to the right value, but you may use other `go` commands that try to download a Fossil repository. On some operating systems, namely Termux on Android, an error message might state that an user cannot be determined (`cannot determine user`). In this case, Fossil is allowed to download the repository, but cannot associate it with an user name. Set the environment variable `USER` to any user name, like: `USER=nobody go run tools/build.go build`. |
Changes to www/changes.wiki.
1 2 3 | <title>Change Log</title> <a id="0_13"></a> | > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<title>Change Log</title>
<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (pending)</h2>
<a id="0_13"></a>
<h2>Changes for Version 0.13.0 (2023-08-07)</h2>
* There are for new search operators: less, not less, greater, not greater.
These use the same syntax as the operators prefix, not prefix, suffix, not
suffix. The latter are no denoted as <tt>[</tt>, <tt>![</tt>, <tt>]</tt>,
and <tt>!]</tt>. The first may operate numerically for metadata like
numbers, timestamps, and zettel identifier. They are not supported for
full-test search.
(breaking: api, webui)
* The API endpoint <tt>/o/{ID}</tt> (order of zettel ID) is no longer
available. Please use the query expression <tt>{ID} ITEMS</tt> instead.
(breaking: api)
* The API endpoint <tt>/u/{ID}</tt> (unlinked references of zettel ID) is no
longer available. Please use the query expression <tt>{ID} UNLINKED</tt>
instead.
(breaking: api)
* All API endpoints allow to encode zettel data with the <tt>data</tt>
encodings, incl. creating, updating, retrieving, and querying zettel.
(major: api)
* Change syntax for context query to <tt>zid ... CONTEXT</tt>. This will
allow to add more directives that operate on zettel identifier. Old syntax
<tt>CONTEXT zid</tt> will be removed in 0.14.
(major, deprecated)
* Add query directive <tt>ITEMS</tt> that will produce a list of metadata
of all zettel that are referenced by the originating zettel in a top-level
list. It replaces the API endpoint <tt>/o/{ID}</tt> (and makes it more
useful).
(major: api, webui)
* Add query directive <tt>UNLINKED</tt> that will produce a list of metadata
of all zettel that are mentioning the originating zettel in a top-level,
but do not mention them. It replaces the API endpoint <tt>/u/{ID}</tt>
(and makes it more useful).
(major: api, webui)
* Add query directive <tt>IDENT</tt> to distinguish a search for a zettel
identifier (“{ID}”), that will list all metadata of zettel
containing that zettel identifier, and a request to just list the metadata
of given zettel (“{ID} IDENT”). The latter could be filtered
further.
(minor: api, webui)
* Add support for metadata key <tt>folge-role</tt>.
(minor)
* Allow to create a child from a given zettel.
(minor: webui)
* Make zettel entry/edit form a little friendlier: auto-prepend missing '#'
to tags; ensure that role and syntax receive just a word.
(minor: webui)
* Use a zettel that defines builtins for evaluating WebUI templates.
(minor: webui)
* Add links to retrieve result of a query in other formats.
(minor: webui)
* Always log the found configuration file.
(minor: server)
* The use of the <tt>json</tt> zettel encoding is deprecated (since version
0.12.0). Support for this encoding will be removed in version 0.14.0.
Please use the new <tt>data</tt> encoding instead.
(deprecated: api)
* Some smaller bug fixes and improvements, to the software and to the
documentation.
<a id="0_12"></a>
<h2>Changes for Version 0.12.0 (2023-06-05)</h2>
* Syntax of templates for the web user interface are changed from Mustache
to Sxn (S-Expressions). Mustache is no longer supported, nowhere in the
software. Mustache was marked deprecated in version 0.11.0. If you
modified the template zettel, you must adapt to the new syntax.
|
| ︙ | ︙ |
Changes to www/download.wiki.
1 2 3 4 5 6 7 8 9 10 11 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> | | | | | | | | | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <title>Download</title> <h1>Download of Zettelstore Software</h1> <h2>Foreword</h2> * Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later. * The software is provided as-is. * There is no guarantee that it will not damage your system. * However, it is in use by the main developer since March 2020 without any damage. * It may be useful for you. It is useful for me. * Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it. <h2>ZIP-ped Executables</h2> Build: <code>v0.13.0</code> (2023-08-07). * [/uv/zettelstore-0.13.0-linux-amd64.zip|Linux] (amd64) * [/uv/zettelstore-0.13.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi) * [/uv/zettelstore-0.13.0-darwin-arm64.zip|macOS] (arm64) * [/uv/zettelstore-0.13.0-darwin-amd64.zip|macOS] (amd64) * [/uv/zettelstore-0.13.0-windows-amd64.zip|Windows] (amd64) Unzip the appropriate file, install and execute Zettelstore according to the manual. <h2>Zettel for the manual</h2> As a starter, you can download the zettel for the manual [/uv/manual-0.13.0.zip|here]. Just unzip the contained files and put them into your zettel folder or configure a file box to read the zettel directly from the ZIP file. |
Changes to www/index.wiki.
| ︙ | ︙ | |||
18 19 20 21 22 23 24 | [https://zettelstore.de/client|Zettelstore Client] provides client software to access Zettelstore via its API more easily, [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed software, which often connects to Zettelstore via its API. Some of the software packages may be experimental. | | | | | | | | | 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
[https://zettelstore.de/client|Zettelstore Client] provides client software to
access Zettelstore via its API more easily,
[https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
software, which often connects to Zettelstore via its API. Some of the software
packages may be experimental.
[https://mastodon.social/tags/Zettelstore|Stay tuned] …
<hr>
<h3>Latest Release: 0.13.0 (2023-06-05)</h3>
* [./download.wiki|Download]
* [./changes.wiki#0_13|Change summary]
* [/timeline?p=v0.13.0&bt=v0.12.0&y=ci|Check-ins for version 0.13.0],
[/vdiff?to=v0.13.0&from=v0.12.0|content diff]
* [/timeline?df=v0.13.0&y=ci|Check-ins derived from the 0.13.0 release],
[/vdiff?from=v0.13.0&to=trunk|content diff]
* [./plan.wiki|Limitations and planned improvements]
* [/timeline?t=release|Timeline of all past releases]
<hr>
<h2>Build instructions</h2>
Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.
|
| ︙ | ︙ |
Changes to www/plan.wiki.
1 2 3 4 5 | <title>Limitations and planned improvements</title> Here is a list of some shortcomings of Zettelstore. They are planned to be solved. | | < | < | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<title>Limitations and planned improvements</title>
Here is a list of some shortcomings of Zettelstore.
They are planned to be solved.
* Zettelstore must have indexed all zettel to make use of queries.
Otherwise not all zettel may be returned.
* Quoted attribute values are not yet supported in Zettelmarkup:
<code>{key="value with space"}</code>.
* The horizontal tab character (<tt>U+0009</tt>) is not supported.
* Missing support for citation keys.
* Changing the content syntax is not reflected in file extension.
* File names with additional text besides the zettel identifier are not
always preserved.
|
| ︙ | ︙ |
Changes to zettel/id/id.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | // zettel identifier. package id import ( "strconv" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // zettel identifier. package id import ( "strconv" "time" "zettelstore.de/client.fossil/api" ) // Zid is the internal identifier of a zettel. Typically, it is a // time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer. // A zettelstore implementation should try to set the last two digits to zero, // e.g. the seconds should be zero, type Zid uint64 |
| ︙ | ︙ | |||
42 43 44 45 46 47 48 49 50 51 52 53 54 55 | ListTemplateZid = MustParse(api.ZidListTemplate) ZettelTemplateZid = MustParse(api.ZidZettelTemplate) InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 | > | 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | ListTemplateZid = MustParse(api.ZidListTemplate) ZettelTemplateZid = MustParse(api.ZidZettelTemplate) InfoTemplateZid = MustParse(api.ZidInfoTemplate) FormTemplateZid = MustParse(api.ZidFormTemplate) RenameTemplateZid = MustParse(api.ZidRenameTemplate) DeleteTemplateZid = MustParse(api.ZidDeleteTemplate) ErrorTemplateZid = MustParse(api.ZidErrorTemplate) TemplateSxnZid = MustParse(api.ZidSxnTemplate) RoleCSSMapZid = MustParse(api.ZidRoleCSSMap) EmojiZid = MustParse(api.ZidEmoji) TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate) DefaultHomeZid = MustParse(api.ZidDefaultHome) ) const maxZid = 99999999999999 |
| ︙ | ︙ |
Changes to zettel/id/id_test.go.
| ︙ | ︙ | |||
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
"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)
}
}
| > > | 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
"00020000000000",
"00300000000000",
"04000000000000",
"50000000000000",
"99999999999999",
"00001007030200",
"20200310195100",
"12345678901234",
}
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",
"+1234567890123",
}
for i, sid := range invalidIDs {
if zid, err := id.Parse(sid); err == nil {
t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
}
}
|
| ︙ | ︙ |
Changes to zettel/meta/meta.go.
| ︙ | ︙ | |||
14 15 16 17 18 19 20 | import ( "regexp" "sort" "strings" "unicode" "unicode/utf8" | | | | 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import ( "regexp" "sort" "strings" "unicode" "unicode/utf8" "zettelstore.de/client.fossil/api" "zettelstore.de/client.fossil/maps" "zettelstore.de/z/input" "zettelstore.de/z/strfun" "zettelstore.de/z/zettel/id" ) type keyUsage int |
| ︙ | ︙ | |||
136 137 138 139 140 141 142 143 144 145 146 147 148 149 | 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.KeyExpire, TypeTimestamp, usageUser, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") registerKey(api.KeyLicense, TypeEmpty, usageUser, "") registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") | > | 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 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.KeyExpire, TypeTimestamp, usageUser, "") registerKey(api.KeyFolgeRole, TypeWord, usageUser, "") registerKey(api.KeyForward, TypeIDSet, usageProperty, "") registerKey(api.KeyLang, TypeWord, usageUser, "") registerKey(api.KeyLicense, TypeEmpty, usageUser, "") registerKey(api.KeyModified, TypeTimestamp, usageComputed, "") registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge) registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors) registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "") |
| ︙ | ︙ | |||
238 239 240 241 242 243 244 |
}
// 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)
| | | | 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
}
// 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 {
m.Set(key, trimValue(value))
}
}
func trimValue(value string) string {
return strings.TrimFunc(value, input.IsSpace)
}
|
| ︙ | ︙ |
Changes to zettel/meta/meta_test.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package meta import ( "strings" "testing" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package meta
import (
"strings"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
)
const testID = id.Zid(98765432101234)
func TestKeyIsValid(t *testing.T) {
t.Parallel()
|
| ︙ | ︙ |
Changes to zettel/meta/parse.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package meta import ( "strings" | | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//-----------------------------------------------------------------------------
package meta
import (
"strings"
"zettelstore.de/client.fossil/api"
"zettelstore.de/client.fossil/maps"
"zettelstore.de/z/input"
"zettelstore.de/z/strfun"
"zettelstore.de/z/zettel/id"
)
// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
|
| ︙ | ︙ |
Changes to zettel/meta/parse_test.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package meta_test import ( "strings" "testing" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package meta_test
import (
"strings"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/input"
"zettelstore.de/z/zettel/meta"
)
func parseMetaStr(src string) *meta.Meta {
return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}
|
| ︙ | ︙ |
Changes to zettel/meta/type.go.
| ︙ | ︙ | |||
12 13 14 15 16 17 18 | import ( "strconv" "strings" "sync" "time" | | | 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import (
"strconv"
"strings"
"sync"
"time"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
)
// DescriptionType is a description of a specific key type.
type DescriptionType struct {
Name string
IsSet bool
|
| ︙ | ︙ | |||
107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
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.
| > > > > > > > | 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
if key != api.KeyID {
for i, val := range values {
values[i] = trimValue(val)
}
m.pairs[key] = strings.Join(values, " ")
}
}
// SetWord stores the given word under the given key.
func (m *Meta) SetWord(key, word string) {
if slist := ListFromValue(word); len(slist) > 0 {
m.Set(key, slist[0])
}
}
// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
m.Set(key, time.Now().Local().Format(id.ZidLayout))
}
// BoolValue returns the value interpreted as a bool.
|
| ︙ | ︙ |
Changes to zettel/meta/values.go.
| ︙ | ︙ | |||
9 10 11 12 13 14 15 | //----------------------------------------------------------------------------- package meta import ( "fmt" | | | 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | //----------------------------------------------------------------------------- package meta import ( "fmt" "zettelstore.de/client.fossil/api" ) // Supported syntax values. const ( SyntaxCSS = api.ValueSyntaxCSS SyntaxDraw = api.ValueSyntaxDraw SyntaxGif = api.ValueSyntaxGif |
| ︙ | ︙ |
Changes to zettel/meta/write_test.go.
| ︙ | ︙ | |||
10 11 12 13 14 15 16 | package meta_test import ( "strings" "testing" | | | 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package meta_test
import (
"strings"
"testing"
"zettelstore.de/client.fossil/api"
"zettelstore.de/z/zettel/id"
"zettelstore.de/z/zettel/meta"
)
const testID = id.Zid(98765432101234)
func newMeta(title string, tags []string, syntax string) *meta.Meta {
|
| ︙ | ︙ |