Zettelstore

Check-in Differences
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Difference From v0.7.0 To trunk

2023-09-22
16:49
Version 0.14.0 ... (Leaf check-in: 0812552da8 user: stern tags: trunk, release, v0.14.0)
16:41
webUI: remove support for <search> since it is not well supported by HTML validators ... (check-in: 743e66e08b user: stern tags: trunk)
2022-09-18
12:52
Increase version to 0.7.1-dev to begin release update development cycle ... (check-in: f005505248 user: stern tags: release-0.7)
2022-09-17
15:10
Increase version to 0.8.0-dev to begin next development cycle ... (check-in: 97bb0f0d93 user: t73fde tags: trunk)
12:28
Version 0.7.0 ... (check-in: f2b8d470c7 user: stern tags: trunk, release, v0.7.0)
11:54
Update to newest go/x/crypto, go/x/sys ... (check-in: 61528d5462 user: stern tags: trunk)

Deleted .deepsource.toml.

1
2
3
4
5
6
7
8
version = 1

[[analyzers]]
name = "go"
enabled = true

  [analyzers.meta]
import_paths = ["github.com/zettelstore/zettelstore"]
<
<
<
<
<
<
<
<
















Changes to LICENSE.txt.

1
2
3
4
5
6
7
8
Copyright (c) 2020-2022 Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official
|







1
2
3
4
5
6
7
8
Copyright (c) 2020-present Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official

Changes to Makefile.

1
2
3
4
5
6
7
8
9
10
11

## Copyright (c) 2020-2021 Detlef Stern
##
## This file is part of zettelstore.
##
## Zettelstore is licensed under the latest version of the EUPL (European Union
## Public License). Please see file LICENSE.txt for your rights and obligations
## under this license.

.PHONY:  check relcheck api build release clean


|

|







1
2
3
4
5
6
7
8
9
10
11

## 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.

.PHONY:  check relcheck api build release clean

Changes to README.md.

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://twitter.com/zettelstore)&nbsp;&hellip;







|
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)&nbsp;&hellip;

Changes to VERSION.

1
0.7.0
|
1
0.14.0

Changes to ast/ast.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package ast provides the abstract syntax tree for parsed zettel content.
package ast

import (
	"net/url"

	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// ZettelNode is the root node of the abstract syntax tree.
// It is *not* part of the visitor pattern.
type ZettelNode struct {
	Meta    *meta.Meta     // Original metadata
	Content domain.Content // Original content
	Zid     id.Zid         // Zettel identification.
	InhMeta *meta.Meta     // Metadata of the zettel, with inherited values.
	Ast     BlockSlice     // Zettel abstract syntax tree is a sequence of block nodes.
	Syntax  string         // Syntax / parser that produced the Ast
}

// Node is the interface, all nodes must implement.

|














|
|
|






|







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
//-----------------------------------------------------------------------------
// 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 provides the abstract syntax tree for parsed zettel content.
package ast

import (
	"net/url"

	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ZettelNode is the root node of the abstract syntax tree.
// It is *not* part of the visitor pattern.
type ZettelNode struct {
	Meta    *meta.Meta     // Original metadata
	Content zettel.Content // Original content
	Zid     id.Zid         // Zettel identification.
	InhMeta *meta.Meta     // Metadata of the zettel, with inherited values.
	Ast     BlockSlice     // Zettel abstract syntax tree is a sequence of block nodes.
	Syntax  string         // Syntax / parser that produced the Ast
}

// Node is the interface, all nodes must implement.

Changes to ast/block.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package ast

import "zettelstore.de/c/attrs"

// Definition of Block nodes.

// BlockSlice is a slice of BlockNodes.
type BlockSlice []BlockNode

func (*BlockSlice) blockNode() { /* Just a marker */ }

|










|







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 */ }
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

func (*VerbatimNode) blockNode() { /* Just a marker */ }
func (*VerbatimNode) itemNode()  { /* Just a marker */ }

// WalkChildren does nothing.
func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ }

// Supported syntax values for VerbatimEval.
const (
	VerbatimEvalSyntaxDraw = "draw"
)

//--------------------------------------------------------------------------

// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
	Kind    RegionKind
	Attrs   attrs.Attributes
	Blocks  BlockSlice







<
<
<
<
<







86
87
88
89
90
91
92





93
94
95
96
97
98
99

func (*VerbatimNode) blockNode() { /* Just a marker */ }
func (*VerbatimNode) itemNode()  { /* Just a marker */ }

// WalkChildren does nothing.
func (*VerbatimNode) WalkChildren(Visitor) { /* No children*/ }






//--------------------------------------------------------------------------

// RegionNode encapsulates a region of block nodes.
type RegionNode struct {
	Kind    RegionKind
	Attrs   attrs.Attributes
	Blocks  BlockSlice
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ }

//--------------------------------------------------------------------------

// BLOBNode contains just binary data that must be interpreted according to
// a syntax.
type BLOBNode struct {
	Title  string
	Syntax string
	Blob   []byte
}

func (*BLOBNode) blockNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }







|
|
|






277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
func (*TranscludeNode) WalkChildren(Visitor) { /* No children*/ }

//--------------------------------------------------------------------------

// BLOBNode contains just binary data that must be interpreted according to
// a syntax.
type BLOBNode struct {
	Description InlineSlice
	Syntax      string
	Blob        []byte
}

func (*BLOBNode) blockNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*BLOBNode) WalkChildren(Visitor) { /* No children*/ }

Changes to ast/inline.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package ast

import (
	"unicode/utf8"

	"zettelstore.de/c/attrs"
)

// Definitions of inline nodes.

// InlineSlice is a list of BlockNodes.
type InlineSlice []InlineNode


|













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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 (
	"unicode/utf8"

	"zettelstore.de/client.fossil/attrs"
)

// Definitions of inline nodes.

// InlineSlice is a list of BlockNodes.
type InlineSlice []InlineNode

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
}

func (*TextNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*TextNode) WalkChildren(Visitor) { /* No children*/ }

// --------------------------------------------------------------------------

// TagNode contains a tag.
type TagNode struct {
	Tag string // The text itself.
}

func (*TagNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*TagNode) WalkChildren(Visitor) { /* No children*/ }

// --------------------------------------------------------------------------

// SpaceNode tracks inter-word space characters.
type SpaceNode struct {
	Lexeme string
}








<
<
<
<
<
<
<
<
<
<
<
<







51
52
53
54
55
56
57












58
59
60
61
62
63
64
}

func (*TextNode) inlineNode() { /* Just a marker */ }

// WalkChildren does nothing.
func (*TextNode) WalkChildren(Visitor) { /* No children*/ }













// --------------------------------------------------------------------------

// SpaceNode tracks inter-word space characters.
type SpaceNode struct {
	Lexeme string
}

Changes to ast/ref.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package ast

import (
	"net/url"
	"strings"

	"zettelstore.de/z/domain/id"
)

// QueryPrefix is the prefix that denotes a query expression.
const QueryPrefix = "query:"

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {
	if s == "" || s == "00000000000000" {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if strings.HasPrefix(s, QueryPrefix) {
		return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery}
	}
	if state, ok := localState(s); ok {
		if state == RefStateBased {
			s = s[1:]
		}
		u, err := url.Parse(s)
		if err == nil {
			return &Reference{URL: u, Value: s, State: state}
		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if len(u.Scheme)+len(u.Opaque)+len(u.Host) == 0 && u.User == nil {
		if _, err = id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateSelf}
		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}
}






func localState(path string) (RefState, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return RefStateBased, true
		}
		return RefStateHosted, true

|














|







|


















|









>
>
>
>
>







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
//-----------------------------------------------------------------------------
// 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 (
	"net/url"
	"strings"

	"zettelstore.de/z/zettel/id"
)

// QueryPrefix is the prefix that denotes a query expression.
const QueryPrefix = "query:"

// ParseReference parses a string and returns a reference.
func ParseReference(s string) *Reference {
	if invalidReference(s) {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if strings.HasPrefix(s, QueryPrefix) {
		return &Reference{URL: nil, Value: s[len(QueryPrefix):], State: RefStateQuery}
	}
	if state, ok := localState(s); ok {
		if state == RefStateBased {
			s = s[1:]
		}
		u, err := url.Parse(s)
		if err == nil {
			return &Reference{URL: u, Value: s, State: state}
		}
	}
	u, err := url.Parse(s)
	if err != nil {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if !externalURL(u) {
		if _, err = id.Parse(u.Path); err == nil {
			return &Reference{URL: u, Value: s, State: RefStateZettel}
		}
		if u.Path == "" && u.Fragment != "" {
			return &Reference{URL: u, Value: s, State: RefStateSelf}
		}
	}
	return &Reference{URL: u, Value: s, State: RefStateExternal}
}

func invalidReference(s string) bool { return s == "" || s == "00000000000000" }
func externalURL(u *url.URL) bool {
	return u.Scheme != "" || u.Opaque != "" || u.Host != "" || u.User != nil
}

func localState(path string) (RefState, bool) {
	if len(path) > 0 && path[0] == '/' {
		if len(path) > 1 && path[1] == '/' {
			return RefStateBased, true
		}
		return RefStateHosted, true

Changes to ast/ref_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to ast/walk.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to ast/walk_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package ast_test

import (
	"testing"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
)

func BenchmarkWalk(b *testing.B) {
	root := ast.BlockSlice{
		&ast.HeadingNode{
			Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"),

|













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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 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"),

Changes to auth/auth.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package auth provides services for authentification / authorization.
package auth

import (
	"time"

	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// BaseManager allows to check some base auth modes.
type BaseManager interface {
	// IsReadonly returns true, if the systems is configured to run in read-only-mode.
	IsReadonly() bool
}

|
















|
|







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
//-----------------------------------------------------------------------------
// 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 auth provides services for authentification / authorization.
package auth

import (
	"time"

	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// BaseManager allows to check some base auth modes.
type BaseManager interface {
	// IsReadonly returns true, if the systems is configured to run in read-only-mode.
	IsReadonly() bool
}
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// TokenKind specifies for which application / usage a token is/was requested.
type TokenKind int

// Allowed values of token kind
const (
	_ TokenKind = iota
	KindJSON
	KindHTML
)

// TokenData contains some important elements from a token.
type TokenData struct {
	Token   []byte
	Now     time.Time
	Issued  time.Time







|
|







38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// TokenKind specifies for which application / usage a token is/was requested.
type TokenKind int

// Allowed values of token kind
const (
	_ TokenKind = iota
	KindAPI
	KindwebUI
)

// TokenData contains some important elements from a token.
type TokenData struct {
	Token   []byte
	Now     time.Time
	Issued  time.Time

Changes to auth/cred/cred.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package cred provides some function for handling credentials.
package cred

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"
	"zettelstore.de/z/domain/id"
)

// HashCredential returns a hashed vesion of the given credential
func HashCredential(zid id.Zid, ident, credential string) (string, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost)
	if err != nil {

|

|













|







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
//-----------------------------------------------------------------------------
// 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 cred provides some function for handling credentials.
package cred

import (
	"bytes"

	"golang.org/x/crypto/bcrypt"
	"zettelstore.de/z/zettel/id"
)

// HashCredential returns a hashed vesion of the given credential
func HashCredential(zid id.Zid, ident, credential string) (string, error) {
	fullCredential := createFullCredential(zid, ident, credential)
	res, err := bcrypt.GenerateFromPassword(fullCredential, bcrypt.DefaultCost)
	if err != nil {
40
41
42
43
44
45
46
47
48
49
50
51
52
53
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident, credential string) []byte {
	var buf bytes.Buffer
	buf.WriteString(zid.String())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}







|






40
41
42
43
44
45
46
47
48
49
50
51
52
53
		return false, nil
	}
	return false, err
}

func createFullCredential(zid id.Zid, ident, credential string) []byte {
	var buf bytes.Buffer
	buf.Write(zid.Bytes())
	buf.WriteByte(' ')
	buf.WriteString(ident)
	buf.WriteByte(' ')
	buf.WriteString(credential)
	return buf.Bytes()
}

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.

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) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package impl provides services for authentification / authorization.
package impl

import (
	"errors"
	"hash/fnv"
	"io"
	"time"

	"github.com/pascaldekloe/jwt"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/policy"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

type myAuth struct {
	readonly bool
	owner    id.Zid
	secret   []byte
}

|

















|
|
|




|
|
|







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) 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 impl provides services for authentification / authorization.
package impl

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"
)

type myAuth struct {
	readonly bool
	owner    id.Zid
	secret   []byte
}
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
	}
	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 }

const reqHash = jwt.HS512


// ErrNoIdent signals that the 'ident' key is missing.
var ErrNoIdent = errors.New("auth: missing ident")

// ErrOtherKind signals that the token was defined for another token kind.
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)
	claims := jwt.Claims{
		Registered: jwt.Registered{
			Subject: subject,

			Expires: jwt.NewNumericTime(now.Add(d)),
			Issued:  jwt.NewNumericTime(now),
		},
		Set: map[string]interface{}{
			"zid": ident.Zid.String(),
			"_tk": int(kind),
		},
	}
	token, err := claims.HMACSign(reqHash, a.secret)
	if err != nil {

		return nil, err
	}
	return token, nil
}

// ErrTokenExpired signals an exired token
var ErrTokenExpired = errors.New("auth: token expired")

// CheckToken checks the validity of the token and returns relevant data.
func (a *myAuth) CheckToken(token []byte, k auth.TokenKind) (auth.TokenData, error) {

	h, err := jwt.NewHMAC(reqHash, a.secret)

	if err != nil {
		return auth.TokenData{}, err
	}
	claims, err := h.Check(token)







	if err != nil {
		return auth.TokenData{}, err
	}
	now := time.Now().Round(time.Second)
	expires := claims.Expires.Time()
	if expires.Before(now) {
		return auth.TokenData{}, ErrTokenExpired

	}
	ident := claims.Subject
	if ident == "" {
		return auth.TokenData{}, ErrNoIdent
	}
	if zidS, ok := claims.Set["zid"].(string); ok {
		if zid, err2 := id.Parse(zidS); err2 == nil {
			if kind, ok2 := claims.Set["_tk"].(float64); ok2 {
				if auth.TokenKind(kind) == k {
					return auth.TokenData{
						Token:   token,
						Now:     now,
						Issued:  claims.Issued.Time(),
						Expires: expires,
						Ident:   ident,
						Zid:     zid,
					}, nil
				}
			}


			return auth.TokenData{}, ErrOtherKind
		}
	}





	return auth.TokenData{}, ErrNoZid
}

func (a *myAuth) Owner() id.Zid { return a.owner }

func (a *myAuth) IsOwner(zid id.Zid) bool {
	return zid.IsValid() && zid == a.owner
}







|
>


















|
|
|
>
|
<
<
<
|
<
<
<
<
<
>
|
<
<






|
>
|
>

|

|
>
>
>
>
>
>
>

|

<
<
<
|
>

|

|

|
|
<
<
<
<
|
<
|
|
<
<
|
<
>
>
|
|
|
>
>
>
>
>
|







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.String(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/anon.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import (
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
)

type anonPolicy struct {
	authConfig config.AuthConfig
	pre        auth.Policy
}


|

|











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/zettel/meta"
)

type anonPolicy struct {
	authConfig config.AuthConfig
	pre        auth.Policy
}

Changes to auth/policy/box.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) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import (
	"context"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"zettelstore.de/z/web/server"
)

// BoxWithPolicy wraps the given box inside a policy box.
func BoxWithPolicy(
	manager auth.AuthzManager,
	box box.Box,
	authConfig config.AuthConfig,
) (box.Box, auth.Policy) {
	pol := newPolicy(manager, authConfig)
	return newBox(box, pol), pol
}

// polBox implements a policy box.
type polBox struct {
	box    box.Box

|
















|
|
|
|
|



|
<
<
<
<







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) 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 (
	"context"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/query"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// BoxWithPolicy wraps the given box inside a policy box.
func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) {




	pol := newPolicy(manager, authConfig)
	return newBox(box, pol), pol
}

// polBox implements a policy box.
type polBox struct {
	box    box.Box
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
	return pp.box.Location()
}

func (pp *polBox) CanCreateZettel(ctx context.Context) bool {
	return pp.box.CanCreateZettel(ctx)
}

func (pp *polBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	user := server.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.box.CreateZettel(ctx, zettel)
	}
	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
}

func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	zettel, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return domain.Zettel{}, err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRead(user, zettel.Meta) {
		return zettel, nil
	}
	return domain.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
	return pp.box.GetAllZettel(ctx, zid)
}





func (pp *polBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		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) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	return pp.box.GetAllMeta(ctx, zid)
}

func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) {
	return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid)
}

func (pp *polBox) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	user := server.GetUser(ctx)
	canRead := pp.policy.CanRead
	q = q.SetPreMatch(func(m *meta.Meta) bool { return canRead(user, m) })
	return pp.box.SelectMeta(ctx, q)
}

func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	return pp.box.CanUpdateZettel(ctx, zettel)
}

func (pp *polBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	zid := zettel.Meta.Zid
	user := server.GetUser(ctx)
	if !zid.IsValid() {
		return &box.ErrInvalidID{Zid: zid}
	}
	// Write existing zettel
	oldMeta, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		return err
	}
	if pp.policy.CanWrite(user, oldMeta, 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 {
	meta, err := pp.box.GetMeta(ctx, curZid)
	if err != nil {
		return err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRename(user, 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 {
	meta, err := pp.box.GetMeta(ctx, zid)
	if err != nil {
		return err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanDelete(user, 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)
	if pp.policy.CanRefresh(user) {
		return pp.box.Refresh(ctx)
	}
	return box.NewErrNotAllowed("Refresh", user, id.Invalid)
}







|







|
|

|


|
|

|


|


>
>
>
>













<
<
<
<
<
<
<
<
|



|


|



|



|


|



|










|




|










|




|












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
	return pp.box.Location()
}

func (pp *polBox) CanCreateZettel(ctx context.Context) bool {
	return pp.box.CanCreateZettel(ctx)
}

func (pp *polBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	user := server.GetUser(ctx)
	if pp.policy.CanCreate(user, zettel.Meta) {
		return pp.box.CreateZettel(ctx, zettel)
	}
	return id.Invalid, box.NewErrNotAllowed("Create", user, id.Invalid)
}

func (pp *polBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return zettel.Zettel{}, err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanRead(user, z.Meta) {
		return z, nil
	}
	return zettel.Zettel{}, box.NewErrNotAllowed("GetZettel", user, zid)
}

func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
	return pp.box.GetAllZettel(ctx, zid)
}

func (pp *polBox) FetchZids(ctx context.Context) (id.Set, error) {
	return nil, box.NewErrNotAllowed("fetch-zids", server.GetUser(ctx), id.Invalid)
}

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.ErrInvalidZid{Zid: zid.String()}
	}
	// 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)
	if pp.policy.CanRefresh(user) {
		return pp.box.Refresh(ctx)
	}
	return box.NewErrNotAllowed("Refresh", user, id.Invalid)
}

Changes to auth/policy/default.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
)

type defaultPolicy struct {
	manager auth.AuthzManager
}

func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true }

|

|









|

|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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
}

func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true }

Changes to auth/policy/owner.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
)

type ownerPolicy struct {
	manager    auth.AuthzManager
	authConfig config.AuthConfig
	pre        auth.Policy
}

|

|









|


|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// 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
	authConfig config.AuthConfig
	pre        auth.Policy
}

Changes to auth/policy/policy.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package policy provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
)

// newPolicy creates a policy based on given constraints.
func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy {
	var pol auth.Policy
	if manager.IsReadonly() {
		pol = &roPolicy{}

|

|












|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// 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 provides some interfaces and implementation for authorizsation policies.
package policy

import (
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/zettel/meta"
)

// newPolicy creates a policy based on given constraints.
func newPolicy(manager auth.AuthzManager, authConfig config.AuthConfig) auth.Policy {
	var pol auth.Policy
	if manager.IsReadonly() {
		pol = &roPolicy{}

Changes to auth/policy/policy_test.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import (
	"fmt"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

func TestPolicies(t *testing.T) {
	t.Parallel()
	testScene := []struct {
		readonly bool
		withAuth bool

|

|












|

|
|







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
//-----------------------------------------------------------------------------
// 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 (
	"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()
	testScene := []struct {
		readonly bool
		withAuth bool

Changes to auth/policy/readonly.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package policy

import "zettelstore.de/z/domain/meta"

type roPolicy struct{}

func (*roPolicy) CanCreate(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanRead(_, _ *meta.Meta) bool     { return true }
func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false }
func (*roPolicy) CanRename(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanDelete(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanRefresh(user *meta.Meta) bool  { return user != nil }

|

|








|









1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//-----------------------------------------------------------------------------
// 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/z/zettel/meta"

type roPolicy struct{}

func (*roPolicy) CanCreate(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanRead(_, _ *meta.Meta) bool     { return true }
func (*roPolicy) CanWrite(_, _, _ *meta.Meta) bool { return false }
func (*roPolicy) CanRename(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanDelete(_, _ *meta.Meta) bool   { return false }
func (*roPolicy) CanRefresh(user *meta.Meta) bool  { return user != nil }

Changes to box/box.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package box provides a generic interface to zettel boxes.
package box

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/url"
	"strconv"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
)

// BaseBox is implemented by all Zettel boxes.
type BaseBox interface {
	// Location returns some information where the box is located.
	// Format is dependent of the box.
	Location() string

	// CanCreateZettel returns true, if box could possibly create a new zettel.
	CanCreateZettel(ctx context.Context) bool

	// CreateZettel creates a new zettel.
	// Returns the new zettel id (and an error indication).
	CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)

	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)

	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

	// CanUpdateZettel returns true, if box could possibly update the given zettel.
	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool

	// UpdateZettel updates an existing zettel.
	UpdateZettel(ctx context.Context, zettel domain.Zettel) error

	// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
	AllowRenameZettel(ctx context.Context, zid id.Zid) bool

	// RenameZettel changes the current Zid to a new Zid.
	RenameZettel(ctx context.Context, curZid, newZid id.Zid) error


|
















<
<


|
|
|
|
|













|


|

<
<
<

|


|







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
//-----------------------------------------------------------------------------
// 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 box provides a generic interface to zettel boxes.
package box

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.
type BaseBox interface {
	// Location returns some information where the box is located.
	// Format is dependent of the box.
	Location() string

	// CanCreateZettel returns true, if box could possibly create a new zettel.
	CanCreateZettel(ctx context.Context) bool

	// CreateZettel creates a new zettel.
	// Returns the new zettel id (and an error indication).
	CreateZettel(ctx context.Context, zettel 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.
	AllowRenameZettel(ctx context.Context, zid id.Zid) bool

	// RenameZettel changes the current Zid to a new Zid.
	RenameZettel(ctx context.Context, curZid, newZid id.Zid) error

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

// 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

	// ReadStats populates st with box statistics
	ReadStats(st *ManagedBoxStats)
}

// ManagedBoxStats records statistics about the box.
type ManagedBoxStats struct {
	// ReadOnly indicates that the content of a box cannot change.
	ReadOnly bool

	// Zettel is the number of zettel managed by the box.
	Zettel int
}
















// StartStopper performs simple lifecycle management.
type StartStopper interface {



	// Start the box. Now all other functions of the box are allowed.
	// Starting an already started box is not allowed.
	Start(ctx context.Context) error

	// Stop the started box. Now only the Start() function is allowed.
	Stop(ctx context.Context)
}

// Refresher allow to refresh their internal data.
type Refresher interface {
	// Refresh the box data.
	Refresh(context.Context)
}

// 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.

	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)

	// GetAllZettel retrieves a specific zettel from all managed boxes.
	GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error)

	// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes.
	GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error)

	// Refresh the data from the box and from its managed sub-boxes.
	Refresh(context.Context) error
}

// Stats record stattistics about a box.
type Stats struct {







>
>
>



















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



>
>
>

|



















>
>
>

>
|


|
<
<
<







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

// 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

	// ReadStats populates st with box statistics
	ReadStats(st *ManagedBoxStats)
}

// ManagedBoxStats records statistics about the box.
type ManagedBoxStats struct {
	// ReadOnly indicates that the content of a box cannot change.
	ReadOnly bool

	// Zettel is the number of zettel managed by the box.
	Zettel int
}

// StartState enumerates the possible states of starting and stopping a box.
//
// StartStateStopped -> StartStateStarting -> StartStateStarted -> StateStateStopping -> StartStateStopped.
//
// Other transitions are also possible.
type StartState uint8

// Constant values of StartState
const (
	StartStateStopped StartState = iota
	StartStateStarting
	StartStateStarted
	StartStateStopping
)

// StartStopper performs simple lifecycle management.
type StartStopper interface {
	// State the current status of the box.
	State() StartState

	// Start the box. Now all other functions of the box are allowed.
	// Starting a box, which is not in state StartStateStopped is not allowed.
	Start(ctx context.Context) error

	// Stop the started box. Now only the Start() function is allowed.
	Stop(ctx context.Context)
}

// Refresher allow to refresh their internal data.
type Refresher interface {
	// Refresh the box data.
	Refresh(context.Context)
}

// 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 {
179
180
181
182
183
184
185

186
187
188
189
190
191
192
193
194
195
196
197
198
199

// UpdateReason gives an indication, why the ObserverFunc was called.
type UpdateReason uint8

// Values for Reason
const (
	_        UpdateReason = iota

	OnReload              // Box was reloaded
	OnZettel              // Something with a zettel happened
)

// UpdateInfo contains all the data about a changed zettel.
type UpdateInfo struct {
	Box    Box
	Reason UpdateReason
	Zid    id.Zid
}

// UpdateFunc is a function to be called when a change is detected.
type UpdateFunc func(UpdateInfo)








>






|







196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217

// UpdateReason gives an indication, why the ObserverFunc was called.
type UpdateReason uint8

// Values for Reason
const (
	_        UpdateReason = iota
	OnReady               // Box is started and fully operational
	OnReload              // Box was reloaded
	OnZettel              // Something with a zettel happened
)

// UpdateInfo contains all the data about a changed zettel.
type UpdateInfo struct {
	Box    BaseBox
	Reason UpdateReason
	Zid    id.Zid
}

// UpdateFunc is a function to be called when a change is detected.
type UpdateFunc func(UpdateInfo)

223
224
225
226
227
228
229








230
231
232
233
234
235
236
var ctxNoEnrichKey ctxNoEnrichType

// DoNotEnrich determines if the context is marked to not enrich metadata.
func DoNotEnrich(ctx context.Context) bool {
	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
	return ok
}









// ErrNotAllowed is returned if the caller is not allowed to perform the operation.
type ErrNotAllowed struct {
	Op   string
	User *meta.Meta
	Zid  id.Zid
}







>
>
>
>
>
>
>
>







241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
var ctxNoEnrichKey ctxNoEnrichType

// DoNotEnrich determines if the context is marked to not enrich metadata.
func DoNotEnrich(ctx context.Context) bool {
	_, ok := ctx.Value(ctxNoEnrichKey).(*ctxNoEnrichType)
	return ok
}

// NoEnrichQuery provides a context that signals not to enrich, if the query does not need this.
func NoEnrichQuery(ctx context.Context, q *query.Query) context.Context {
	if q.EnrichNeeded() {
		return ctx
	}
	return NoEnrichContext(ctx)
}

// ErrNotAllowed is returned if the caller is not allowed to perform the operation.
type ErrNotAllowed struct {
	Op   string
	User *meta.Meta
	Zid  id.Zid
}
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

// ErrStopped is returned if calling methods on a box that was not started.
var ErrStopped = errors.New("box is stopped")

// ErrReadOnly is returned if there is an attepmt to write to a read-only box.
var ErrReadOnly = errors.New("read-only box")

// ErrNotFound is returned if a zettel was not found in the box.




var ErrNotFound = errors.New("zettel not found")

// ErrConflict is returned if a box operation detected a conflict..
// One example: if calculating a new zettel identifier takes too long.
var ErrConflict = errors.New("conflict")

// ErrCapacity is returned if a box has reached its capacity.
var ErrCapacity = errors.New("capacity exceeded")

// ErrInvalidID is returned if the zettel id is not appropriate for the box operation.
type ErrInvalidID struct{ Zid id.Zid }

func (err *ErrInvalidID) Error() string { return "invalid Zettel id: " + err.Zid.String() }

// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

// GetQueryInt is a helper function to extract int values of a specified range from a box URI.
func GetQueryInt(u *url.URL, key string, min, def, max int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return def
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return def
	}
	if iVal < min {
		return min
	}
	if iVal > max {
		return max
	}
	return iVal
}







|
>
>
>
>
|








|
|

|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
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


























// ErrStopped is returned if calling methods on a box that was not started.
var ErrStopped = errors.New("box is stopped")

// ErrReadOnly is returned if there is an attepmt to write to a read-only box.
var ErrReadOnly = errors.New("read-only box")

// ErrZettelNotFound is returned if a zettel was not found in the box.
type ErrZettelNotFound struct{ Zid id.Zid }

func (eznf ErrZettelNotFound) Error() string { return "zettel not found: " + eznf.Zid.String() }

//var ErrZettelNotFound = errors.New("zettel not found")

// ErrConflict is returned if a box operation detected a conflict..
// One example: if calculating a new zettel identifier takes too long.
var ErrConflict = errors.New("conflict")

// ErrCapacity is returned if a box has reached its capacity.
var ErrCapacity = errors.New("capacity exceeded")

// ErrInvalidZid is returned if the zettel id is not appropriate for the box operation.
type ErrInvalidZid struct{ Zid string }

func (err ErrInvalidZid) Error() string { return "invalid Zettel id: " + err.Zid }

























Changes to box/compbox/compbox.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package compbox provides zettel that have computed content.
package compbox

import (
	"context"
	"net/url"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"



)

func init() {
	manager.Register(
		" comp",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return getCompBox(cdata.Number, cdata.Enricher), nil

|















|


<
<
<



>
>
>







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
//-----------------------------------------------------------------------------
// 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 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"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register(
		" comp",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return getCompBox(cdata.Number, cdata.Enricher), nil
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
// Setup remembers important values.
func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }

func (*compBox) Location() string { return "" }

func (*compBox) CanCreateZettel(context.Context) bool { return false }

func (cb *compBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) {
	if gen, ok := myZettel[zid]; ok && gen.meta != nil {
		if m := gen.meta(zid); m != nil {
			updateMeta(m)
			if genContent := gen.content; genContent != nil {
				cb.log.Trace().Msg("GetMeta/Content")
				return domain.Zettel{
					Meta:    m,
					Content: domain.NewContent(genContent(m)),
				}, nil
			}
			cb.log.Trace().Msg("GetMeta/NoContent")
			return domain.Zettel{Meta: m}, nil
		}
	}

	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err")
	return domain.Zettel{}, box.ErrNotFound
}

func (cb *compBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	if gen, ok := myZettel[zid]; ok {
		if genMeta := gen.meta; genMeta != nil {
			if m := genMeta(zid); m != nil {
				updateMeta(m)
				cb.log.Trace().Msg("GetMeta")
				return m, nil
			}
		}
	}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err")
	return nil, box.ErrNotFound
}

func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta")
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			if genMeta(zid) != nil {
				handle(zid)







|




|




|
|

|


|
|


>
|
|


|
|
<
<
<
<
|
|
|
<
<
<
<
<

|







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
// Setup remembers important values.
func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }

func (*compBox) Location() string { return "" }

func (*compBox) CanCreateZettel(context.Context) bool { return false }

func (cb *compBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

func (cb *compBox) GetZettel(_ context.Context, zid id.Zid) (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
		}
	}
	err := box.ErrZettelNotFound{Zid: zid}
	cb.log.Trace().Err(err).Msg("GetZettel/Err")
	return zettel.Zettel{}, err
}

func (*compBox) HasZettel(_ context.Context, zid id.Zid) bool {
	_, found := myZettel[zid]




	return found
}






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)
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
				handle(m)
			}
		}
	}
	return nil
}

func (*compBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false }

func (cb *compBox) UpdateZettel(context.Context, domain.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	_, ok := myZettel[zid]
	return !ok
}

func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error {
	err := box.ErrNotFound
	if _, ok := myZettel[curZid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	err := box.ErrNotFound
	if _, ok := myZettel[zid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *compBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(myZettel)
	cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

func updateMeta(m *meta.Meta) {
	if _, ok := m.Get(api.KeySyntax); !ok {
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
	}
	m.Set(api.KeyRole, api.ValueRoleConfiguration)
	m.Set(api.KeyLang, api.ValueLangEN)
	m.Set(api.KeyReadOnly, api.ValueTrue)
	if _, ok := m.Get(api.KeyVisibility); !ok {
		m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	}
}







|

|









|
<


>
>







|
<


>
>













|








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
				handle(m)
			}
		}
	}
	return nil
}

func (*compBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false }

func (cb *compBox) UpdateZettel(context.Context, zettel.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

func (*compBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	_, ok := myZettel[zid]
	return !ok
}

func (cb *compBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) {

	if _, ok := myZettel[curZid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: curZid}
	}
	cb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*compBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {

	if _, ok := myZettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *compBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(myZettel)
	cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

func updateMeta(m *meta.Meta) {
	if _, ok := m.Get(api.KeySyntax); !ok {
		m.Set(api.KeySyntax, meta.SyntaxZmk)
	}
	m.Set(api.KeyRole, api.ValueRoleConfiguration)
	m.Set(api.KeyLang, api.ValueLangEN)
	m.Set(api.KeyReadOnly, api.ValueTrue)
	if _, ok := m.Get(api.KeyVisibility); !ok {
		m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	}
}

Changes to box/compbox/config.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func genConfigZettelM(zid id.Zid) *meta.Meta {
	if myConfig == nil {
		return nil
	}
	m := meta.New(zid)

|

|











|
|
|
|







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
//-----------------------------------------------------------------------------
// 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 (
	"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 {
		return nil
	}
	m := meta.New(zid)

Changes to box/compbox/keys.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"fmt"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

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|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
	}
	return buf.Bytes()
}

|

|












|
|
|
|
















|



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
//-----------------------------------------------------------------------------
// 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 (
	"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.

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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func genLogM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Log")
	m.Set(api.KeySyntax, api.ValueSyntaxText)
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout))
	return m
}

func genLogC(*meta.Meta) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"

|













|
|
|
|





|







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
//-----------------------------------------------------------------------------
// 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 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)
	m.Set(api.KeyTitle, "Zettelstore Log")
	m.Set(api.KeySyntax, meta.SyntaxText)
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.ZidLayout))
	return m
}

func genLogC(*meta.Meta) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"

Changes to box/compbox/manager.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"fmt"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func genManagerM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Box Manager")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	return m

|

|












|
|
|
|







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
//-----------------------------------------------------------------------------
// 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 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)
	m.Set(api.KeyTitle, "Zettelstore Box Manager")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	return m

Changes to box/compbox/parser.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"fmt"
	"sort"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
)

func genParserM(zid id.Zid) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Supported Parser")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}

func genParserC(*meta.Meta) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Image Format?:\n")
	syntaxes := parser.GetSyntaxes()
	sort.Strings(syntaxes)
	for _, syntax := range syntaxes {
		info := parser.Get(syntax)
		if info.Name != syntax {
			continue
		}
		altNames := info.AltNames
		sort.Strings(altNames)
		fmt.Fprintf(
			&buf, "|%v|%v|%v|%v\n",
			syntax, strings.Join(altNames, ", "), info.IsTextParser, info.IsImageFormat)
	}
	return buf.Bytes()
}

|

|














|
|
|
|
|












|










|
|



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
//-----------------------------------------------------------------------------
// 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 compbox

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 {
	m := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Supported Parser")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}

func genParserC(*meta.Meta) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n")
	syntaxes := parser.GetSyntaxes()
	sort.Strings(syntaxes)
	for _, syntax := range syntaxes {
		info := parser.Get(syntax)
		if info.Name != syntax {
			continue
		}
		altNames := info.AltNames
		sort.Strings(altNames)
		fmt.Fprintf(
			&buf, "|%v|%v|%v|%v|%v\n",
			syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat)
	}
	return buf.Bytes()
}

Changes to box/compbox/version.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package compbox

import (
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func getVersionMeta(zid id.Zid, title string) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeyTitle, title)
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m

|

|









|
|
|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// 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)
	m.Set(api.KeyTitle, title)
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m

Changes to box/constbox/base.css.

79
80
81
82
83
84
85

86
87
88
89
90
91
92
  h1 { font-size:1.5rem;  margin:.65rem 0 }
  h2 { font-size:1.25rem; margin:.70rem 0 }
  h3 { font-size:1.15rem; margin:.75rem 0 }
  h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold }
  h5 { font-size:1.05rem; margin:.8rem 0 }
  h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter }
  p { margin: .5rem 0 0 0 }

  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;







>







79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
  h1 { font-size:1.5rem;  margin:.65rem 0 }
  h2 { font-size:1.25rem; margin:.70rem 0 }
  h3 { font-size:1.15rem; margin:.75rem 0 }
  h4 { font-size:1.05rem; margin:.8rem 0; font-weight: bold }
  h5 { font-size:1.05rem; margin:.8rem 0 }
  h6 { font-size:1.05rem; margin:.8rem 0; font-weight: lighter }
  p { margin: .5rem 0 0 0 }
  p.zs-tag-zettel { margin-top: .5rem; margin-left: 0.5rem }
  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5rem 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: 0.5rem solid lightgray;
134
135
136
137
138
139
140




141
142

143
144
145
146
147
148
149
    display:block;
    border:none;
    border-bottom:1px solid #ccc;
    width:100%;
  }
  input.zs-primary { float:right }
  input.zs-secondary { float:left }




  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }

  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }







>
>
>
>


>







135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    display:block;
    border:none;
    border-bottom:1px solid #ccc;
    width:100%;
  }
  input.zs-primary { float:right }
  input.zs-secondary { float:left }
  input.zs-upload {
    padding-left: 1em;
    padding-right: 1em;
  }
  a:not([class]) { text-decoration-skip-ink: auto }
  a.broken { text-decoration: line-through }
  a.external::after { content: "âžš"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }
  .zs-example { border-style: dotted !important }
  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;







<







186
187
188
189
190
191
192

193
194
195
196
197
198
199
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25rem;
    padding: .1rem .2rem;
    font-size: 95%;
  }

  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;

Deleted box/constbox/base.mustache.

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
<!DOCTYPE html>
<html{{#Lang}} lang="{{Lang}}"{{/Lang}}>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="generator" content="Zettelstore">
<meta name="format-detection" content="telephone=no">
{{{MetaHeader}}}
<link rel="stylesheet" href="{{{CSSBaseURL}}}">
<link rel="stylesheet" href="{{{CSSUserURL}}}">
{{#CSSRoleURL}}<link rel="stylesheet" href="{{{CSSRoleURL}}}">{{/CSSRoleURL}}
<title>{{Title}}</title>
</head>
<body>
<nav class="zs-menu">
<a href="{{{HomeURL}}}">Home</a>
{{#WithUser}}
<div class="zs-dropdown">
<button>User</button>
<nav class="zs-dropdown-content">
{{#WithAuth}}
{{#UserIsValid}}
<a href="{{{UserZettelURL}}}">{{UserIdent}}</a>
{{/UserIsValid}}
{{^UserIsValid}}
<a href="{{{LoginURL}}}">Login</a>
{{/UserIsValid}}
{{#UserIsValid}}
<a href="{{{LogoutURL}}}">Logout</a>
{{/UserIsValid}}
{{/WithAuth}}
</nav>
</div>
{{/WithUser}}
<div class="zs-dropdown">
<button>Lists</button>
<nav class="zs-dropdown-content">
<a href="{{{ListZettelURL}}}">List Zettel</a>
<a href="{{{ListRolesURL}}}">List Roles</a>
<a href="{{{ListTagsURL}}}">List Tags</a>
{{#CanRefresh}}
<a href="{{{RefreshURL}}}">Refresh</a>
{{/CanRefresh}}
</nav>
</div>
{{#HasNewZettelLinks}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks}}
</nav>
</div>
{{/HasNewZettelLinks}}
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="{{QueryKeyQuery}}">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}<footer>{{{FooterHTML}}}</footer>{{/FooterHTML}}
{{#DebugMode}}<div><b>WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!</b></div>{{/DebugMode}}
</body>
</html>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































































































Added box/constbox/base.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
`(@@@@
(html ,@(if lang `((@ (lang ,lang))))
(head
  (meta (@ (charset "utf-8")))
  (meta (@ (name "viewport") (content "width=device-width, initial-scale=1.0")))
  (meta (@ (name "generator") (content "Zettelstore")))
  (meta (@ (name "format-detection") (content "telephone=no")))
  ,@META-HEADER
  (link (@ (rel "stylesheet") (href ,css-base-url)))
  (link (@ (rel "stylesheet") (href ,css-user-url)))
  ,@(ROLE-DEFAULT-meta (current-environment))
  (title ,title))
(body
  (nav (@ (class "zs-menu"))
    (a (@ (href ,home-url)) "Home")
    ,@(if with-auth
      `((div (@ (class "zs-dropdown"))
        (button "User")
        (nav (@ (class "zs-dropdown-content"))
          ,@(if user-is-valid
            `((a (@ (href ,user-zettel-url)) ,user-ident)
              (a (@ (href ,logout-url)) "Logout"))
            `((a (@ (href ,login-url)) "Login"))
          )
      )))
    )
    (div (@ (class "zs-dropdown"))
      (button "Lists")
      (nav (@ (class "zs-dropdown-content"))
        (a (@ (href ,list-zettel-url)) "List Zettel")
        (a (@ (href ,list-roles-url)) "List Roles")
        (a (@ (href ,list-tags-url)) "List Tags")
        ,@(if (bound? 'refresh-url) `((a (@ (href ,refresh-url)) "Refresh")))
    ))
    ,@(if new-zettel-links
      `((div (@ (class "zs-dropdown"))
        (button "New")
        (nav (@ (class "zs-dropdown-content"))
          ,@(map wui-link new-zettel-links)
       )))
    )
    (form (@ (action ,search-url))
      (input (@ (type "text") (placeholder "Search..") (name ,query-key-query) (dir "auto"))))
  )
  (main (@ (class "content")) ,DETAIL)
  ,@(if FOOTER `((footer (hr) ,@FOOTER)))
  ,@(if debug-mode '((div (b "WARNING: Debug mode is enabled. DO NOT USE IN PRODUCTION!"))))
)))

Changes to box/constbox/constbox.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package constbox puts zettel inside the executable.
package constbox

import (
	"context"
	_ "embed" // Allow to embed file content
	"net/url"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"



)

func init() {
	manager.Register(
		" const",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
				log: kernel.Main.GetLogger(kernel.BoxService).Clone().
					Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(),
				number:   cdata.Number,
				zettel:   constZettelMap,
				enricher: cdata.Enricher,
			}, nil
		})
}

type constHeader map[string]string

type constZettel struct {
	header  constHeader
	content domain.Content
}

type constBox struct {
	log      *logger.Logger
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

func (*constBox) Location() string { return "const:" }

func (*constBox) CanCreateZettel(context.Context) bool { return false }

func (cb *constBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) {
	if z, ok := cb.zettel[zid]; ok {
		cb.log.Trace().Msg("GetZettel")
		return domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
	}

	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel")
	return domain.Zettel{}, box.ErrNotFound
}

func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	if z, ok := cb.zettel[zid]; ok {
		cb.log.Trace().Msg("GetMeta")
		return meta.NewWithData(zid, z.header), nil
	}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta")
	return nil, box.ErrNotFound
}

func (cb *constBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(cb.zettel))).Msg("ApplyZid")
	for zid := range cb.zettel {
		if constraint(zid) {
			handle(zid)

|
















|


<
<
<



>
>
>




















|













|




|


|

>
|
|


|
|
<
<
<
<
|







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
//-----------------------------------------------------------------------------
// 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 constbox puts zettel inside the executable.
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"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register(
		" const",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &constBox{
				log: kernel.Main.GetLogger(kernel.BoxService).Clone().
					Str("box", "const").Int("boxnum", int64(cdata.Number)).Child(),
				number:   cdata.Number,
				zettel:   constZettelMap,
				enricher: cdata.Enricher,
			}, nil
		})
}

type constHeader map[string]string

type constZettel struct {
	header  constHeader
	content zettel.Content
}

type constBox struct {
	log      *logger.Logger
	number   int
	zettel   map[id.Zid]constZettel
	enricher box.Enricher
}

func (*constBox) Location() string { return "const:" }

func (*constBox) CanCreateZettel(context.Context) bool { return false }

func (cb *constBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("CreateZettel")
	return id.Invalid, box.ErrReadOnly
}

func (cb *constBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	if z, ok := cb.zettel[zid]; ok {
		cb.log.Trace().Msg("GetZettel")
		return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
	}
	err := box.ErrZettelNotFound{Zid: zid}
	cb.log.Trace().Err(err).Msg("GetZettel/Err")
	return zettel.Zettel{}, err
}

func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool {
	_, found := cb.zettel[zid]




	return found
}

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)
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
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

func (*constBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false }

func (cb *constBox) UpdateZettel(context.Context, domain.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	_, ok := cb.zettel[zid]
	return !ok
}

func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) error {
	err := box.ErrNotFound
	if _, ok := cb.zettel[curZid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	err := box.ErrNotFound
	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly


	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *constBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(cb.zettel)
	cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

const syntaxTemplate = "mustache"

var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Runtime Configuration",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxNone,
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityOwner,
		},
		domain.NewContent(nil)},
	id.MustParse(api.ZidLicense): {
		constHeader{
			api.KeyTitle:      "Zettelstore License",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxText,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyModified:   "20220131153422",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentLicense)},
	id.MustParse(api.ZidAuthors): {
		constHeader{
			api.KeyTitle:      "Zettelstore Contributors",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		domain.NewContent(contentContributors)},
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20220824161200",
		},
		domain.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20210504135842",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentBaseMustache)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentLoginMustache)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentZettelMustache)},
	id.InfoTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentInfoMustache)},
	id.ContextTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Context HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20210218181140",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentContextMustache)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentFormMustache)},
	id.RenameTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Rename Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentRenameMustache)},
	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentDeleteMustache)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20200804111624",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentListZettelMustache)},
	id.ErrorTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     syntaxTemplate,
			api.KeyCreated:    "20210305133215",

			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentErrorMustache)},





















	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     "css",
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     "css",
			api.KeyCreated:    "20210622110143",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent([]byte("/* User-defined CSS */"))},
	id.RoleCSSMapZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Role to CSS Map",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxNone,
			api.KeyCreated:    "20220321183214",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(nil)},
	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		domain.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		domain.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:      "New Zettel",
			api.KeyRole:       api.ValueRoleZettel,
			api.KeySyntax:     api.ValueSyntaxZmk,
			api.KeyCreated:    "20201028185209",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		domain.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      api.ValueSyntaxNone,
			api.KeyCreated:                     "20201028185209",
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		domain.NewContent(nil)},
	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  api.ValueSyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",
		},
		domain.NewContent(contentHomeZettel)},
}

//go:embed license.txt
var contentLicense []byte

//go:embed contributors.zettel
var contentContributors []byte

//go:embed dependencies.zettel
var contentDependencies []byte

//go:embed base.mustache
var contentBaseMustache []byte

//go:embed login.mustache
var contentLoginMustache []byte

//go:embed zettel.mustache
var contentZettelMustache []byte

//go:embed info.mustache
var contentInfoMustache []byte

//go:embed context.mustache
var contentContextMustache []byte

//go:embed form.mustache
var contentFormMustache []byte

//go:embed rename.mustache
var contentRenameMustache []byte

//go:embed delete.mustache
var contentDeleteMustache []byte

//go:embed listzettel.mustache
var contentListZettelMustache []byte

//go:embed error.mustache
var contentErrorMustache []byte







//go:embed base.css
var contentBaseCSS []byte

//go:embed emoji_spin.gif
var contentEmoji []byte

//go:embed newtoc.zettel
var contentNewTOCZettel []byte

//go:embed home.zettel
var contentHomeZettel []byte







|

|









|
<


>
>







|
<


>
>











<
<





|



|




|






|




|





|




|




|

|




|
|
>


|




|

>


|




|
|
>


|




|

<
<
<
<
<
<
<
|
<


|




|

>


|




|

>


|




|

>


|




|
|
>


|




|

>


|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>




|



|




|



|
<
<
<
<
<
<
<
<
<




|




|




|




|




|



|




|






|




|



|











|
|

|
|

|
|

|
|

<
<
<
|
|

|
|

|
|

|
|

|
|
>
>
>
>
>
>












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
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}

func (*constBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false }

func (cb *constBox) UpdateZettel(context.Context, zettel.Zettel) error {
	cb.log.Trace().Err(box.ErrReadOnly).Msg("UpdateZettel")
	return box.ErrReadOnly
}

func (cb *constBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	_, ok := cb.zettel[zid]
	return !ok
}

func (cb *constBox) RenameZettel(_ context.Context, curZid, _ id.Zid) (err error) {

	if _, ok := cb.zettel[curZid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: curZid}
	}
	cb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*constBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {

	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (cb *constBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true
	st.Zettel = len(cb.zettel)
	cb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}



var constZettelMap = map[id.Zid]constZettel{
	id.ConfigurationZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Runtime Configuration",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyCreated:    "20200804111624",
			api.KeyVisibility: api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidLicense): {
		constHeader{
			api.KeyTitle:      "Zettelstore License",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxText,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyModified:   "20220131153422",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentLicense)},
	id.MustParse(api.ZidAuthors): {
		constHeader{
			api.KeyTitle:      "Zettelstore Contributors",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20210504135842",
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentContributors)},
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20230601163100",
		},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20230827212200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseSxn)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20230527144100",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20230907203300",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		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:   "20230907203300",

			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.KeyModified:   "20230829223600",
			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.StartSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Start Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230824160700",
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  id.BaseSxnZid.String(),
		},
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20230907203100",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentBaseCodeSxn)},
	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,
		},
		zettel.NewContent(contentBaseCSS)},
	id.MustParse(api.ZidUserCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore User CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20210622110143",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent([]byte("/* User-defined CSS */"))},









	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyCreated:    "20210504175807",
			api.KeyVisibility: api.ValueVisibilityPublic,
		},
		zettel.NewContent(contentEmoji)},
	id.TOCNewTemplateZid: {
		constHeader{
			api.KeyTitle:      "New Menu",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20210217161829",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:      "New Zettel",
			api.KeyRole:       api.ValueRoleZettel,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20201028185209",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      meta.SyntaxNone,
			api.KeyCreated:                     "20201028185209",
			meta.NewPrefix + api.KeyCredential: "",
			meta.NewPrefix + api.KeyUserID:     "",
			meta.NewPrefix + api.KeyUserRole:   api.ValueUserRoleReader,
			api.KeyVisibility:                  api.ValueVisibilityOwner,
		},
		zettel.NewContent(nil)},
	id.DefaultHomeZid: {
		constHeader{
			api.KeyTitle:   "Home",
			api.KeyRole:    api.ValueRoleZettel,
			api.KeySyntax:  meta.SyntaxZmk,
			api.KeyLang:    api.ValueLangEN,
			api.KeyCreated: "20210210190757",
		},
		zettel.NewContent(contentHomeZettel)},
}

//go:embed license.txt
var contentLicense []byte

//go:embed contributors.zettel
var contentContributors []byte

//go:embed dependencies.zettel
var contentDependencies []byte

//go:embed base.sxn
var contentBaseSxn []byte

//go:embed login.sxn
var contentLoginSxn []byte

//go:embed zettel.sxn
var contentZettelSxn []byte

//go:embed info.sxn
var contentInfoSxn []byte




//go:embed form.sxn
var contentFormSxn []byte

//go:embed rename.sxn
var contentRenameSxn []byte

//go:embed delete.sxn
var contentDeleteSxn []byte

//go:embed listzettel.sxn
var contentListZettelSxn []byte

//go:embed error.sxn
var contentErrorSxn []byte

//go:embed start.sxn
var contentStartCodeSxn []byte

//go:embed wuicode.sxn
var contentBaseCodeSxn []byte

//go:embed 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

Deleted box/constbox/context.mustache.

1
2
3
4
5
6
7
8
9
10
11
<header>
<h1>{{Title}}</h1>
<div class="zs-meta">
<a href="{{{InfoURL}}}">Info</a>
&#183; <a href="?dir=backward">Backward</a>
&#183; <a href="?dir=both">Both</a>
&#183; <a href="?dir=forward">Forward</a>
&#183; Depth:{{#Depths}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Depths}}
</div>
</header>
{{{Content}}}
<
<
<
<
<
<
<
<
<
<
<






















Deleted box/constbox/delete.mustache.

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
<article>
<header>
<h1>Delete Zettel {{Zid}}</h1>
</header>
<p>Do you really want to delete this zettel?</p>
{{#HasShadows}}
<div class="zs-info">
<h2>Infomation</h2>
<p>If you delete this zettel, the previoulsy shadowed zettel from overlayed box {{ShadowedBox}} becomes available.</p>
</div>
{{/HasShadows}}
{{#HasIncoming}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>If you delete this zettel, incoming references from the following zettel will become invalid.</p>
<ul>
{{#Incoming}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming}}
</ul>
</div>
{{/HasIncoming}}
{{#HasUselessFiles}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.</p>
<ul>
{{#UselessFiles}}
<li>{{{.}}}</li>
{{/UselessFiles}}
</ul>
</div>
{{/HasUselessFiles}}
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
<form method="POST">
<input class="zs-primary" type="submit" value="Delete">
</form>
</article>
{{end}}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































Added box/constbox/delete.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
`(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.

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
; URL
: [[https://fsnotify.org/]]
; License
: BSD 3-Clause "New" or "Revised" License
; Source
: [[https://github.com/fsnotify/fsnotify]]
```
Copyright (c) 2012 The Go Authors. All rights reserved.
Copyright (c) 2012-2019 fsnotify Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (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.
```

=== gopikchr
; URL & Source
: [[https://github.com/gopikchr/gopikchr]]
; License
: MIT License
; Remarks
: Author is [[Zellyn Hunter|https://github.com/zellyn]], he wrote a blog post [[gopikchr: a yakshave|https://zellyn.com/2022/01/gopikchr-a-yakshave/]] about his work.
: Gopikchr was incorporated into the source code of Zettelstore, moving it into package ''zettelstore.de/z/parser/pikchr''.
  Later, the source code was changed to adapt it to the needs of Zettelstore.
  For details, read README.txt in the appropriate source code folder.
```
MIT License

Copyright (c) 2022 gopikchr

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

=== hoisie/mustache / cbroglie/mustache
; URL & Source
: [[https://github.com/hoisie/mustache]] / [[https://github.com/cbroglie/mustache]]
; License
: MIT License
; Remarks
: cbroglie/mustache is a fork from hoisie/mustache (starting with commit [f9b4cbf]).
  cbroglie/mustache does not claim a copyright and includes just the license file from hoisie/mustache.
  cbroglie/mustache obviously continues with the original license.

```
Copyright (c) 2009 Michael Hoisie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```

===  pascaldekloe/jwt
; URL & Source
: [[https://github.com/pascaldekloe/jwt]]
; License
: [[CC0 1.0 Universal|https://creativecommons.org/publicdomain/zero/1.0/legalcode]]
```
To the extent possible under law, Pascal S. de Kloe has waived all
copyright and related or neighboring rights to JWT. This work is
published from The Netherlands.

https://creativecommons.org/publicdomain/zero/1.0/legalcode
```

=== yuin/goldmark
; URL & Source
: [[https://github.com/yuin/goldmark]]
; License
: MIT License







|
|

|
|
<

|
|
|
|
|
<
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

|
|
|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
|
<
<
|
<
<
<
<
<
<
|
<
<
|
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
|
<







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
; URL
: [[https://fsnotify.org/]]
; License
: BSD 3-Clause "New" or "Revised" License
; Source
: [[https://github.com/fsnotify/fsnotify]]
```
Copyright © 2012 The Go Authors. All rights reserved.
Copyright © fsnotify Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:


* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
  list of conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution.

* Neither the name of Google Inc. nor the names of its contributors may be used
  to endorse or promote products derived from this software without specific
  prior written permission.







































THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE





DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR









ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES


(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
207
208
209
210
211
212
213











IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```


















>
>
>
>
>
>
>
>
>
>
>
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

=== 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.

Deleted box/constbox/error.mustache.

1
2
3
4
5
6
<article>
<header>
<h1>{{ErrorTitle}}</h1>
</header>
{{ErrorText}}
</article>
<
<
<
<
<
<












Added box/constbox/error.sxn.









>
>
>
>
1
2
3
4
`(article
  (header (h1 ,heading))
  ,message
)

Deleted box/constbox/form.mustache.

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
<article>
<header>
<h1>{{Heading}}</h1>
</header>
<form method="POST">
<div>
<label for="zs-title">Title <a title="Main heading of this zettel. You can use inline zettelmarkup.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-title" name="title" placeholder="Title.." value="{{MetaTitle}}" autofocus>
</div>
<div>
<div>
<label for="zs-role">Role <a title="One word, without spaces, to set the main role of this zettel.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-role" {{#HasRoleData}}list="zs-role-data"{{/HasRoleData}} name="role" placeholder="role.." value="{{MetaRole}}">
{{#HasRoleData}}
<datalist id="zs-role-data">
{{#RoleData}}
<option value="{{.}}">
{{/RoleData}}
</datalist>
{{/HasRoleData}}
</div>
<label for="zs-tags">Tags <a title="Tags must begin with an '#' sign. They are separated by spaces.">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-tags" name="tags" placeholder="#tag" value="{{MetaTags}}">
</div>
<div>
<label for="zs-meta">Metadata <a title="Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.">&#9432;</a></label>
<textarea class="zs-input" id="zs-meta" name="meta" rows="4" placeholder="metakey: metavalue">
{{#MetaPairsRest}}
{{Key}}: {{Value}}
{{/MetaPairsRest}}
</textarea>
</div>
<div>
<label for="zs-syntax">Syntax <a title="Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).">&#9432;</a></label>
<input class="zs-input" type="text" id="zs-syntax" {{#HasSyntaxData}}list="zs-syntax-data"{{/HasSyntaxData}} name="syntax" placeholder="syntax.." value="{{MetaSyntax}}">
{{#HasSyntaxData}}
<datalist id="zs-syntax-data">
{{#SyntaxData}}
<option value="{{.}}">
{{/SyntaxData}}
</datalist>
{{/HasSyntaxData}}</div>
<div>
{{#IsTextContent}}
<label for="zs-content">Content <a title="Content for this zettel, according to above syntax.">&#9432;</a></label>
<textarea class="zs-input zs-content" id="zs-content" name="content" rows="20" placeholder="Your content..">{{Content}}</textarea>
{{/IsTextContent}}
</div>
<div>
<input class="zs-primary" type="submit" value="Submit">
<input class="zs-secondary" type="submit" value="Save" formaction="?save">
</div>
</form>
</article>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<












































































































Added box/constbox/form.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
`(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 "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title") (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus))))
  (div
    (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-role") (name "role") (placeholder "role..") (value ,meta-role) (dir "auto")
      ,@(if role-data '((list "zs-role-data")))
    ))
    ,@(wui-datalist "zs-role-data" role-data)
  )
  (div
    (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags") (placeholder "#tag") (value ,meta-tags) (dir "auto"))))
  (div
    (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4") (placeholder "metakey: metavalue") (dir "auto")) ,meta))
  (div
    (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-syntax") (name "syntax") (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20") (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )
  (div
    (input (@ (class "zs-primary") (type "submit") (value "Submit")))
    (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file")))
  ))
)

Deleted box/constbox/info.mustache.

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
<article>
<header>
<h1>Information for Zettel {{Zid}}</h1>
<a href="{{{WebURL}}}">Web</a>
&#183; <a href="{{{ContextURL}}}">Context</a>
{{#CanWrite}} &#183; <a href="{{{EditURL}}}">Edit</a>{{/CanWrite}}
{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanRename}}&#183; <a href="{{{RenameURL}}}">Rename</a>{{/CanRename}}
{{#CanDelete}}&#183; <a href="{{{DeleteURL}}}">Delete</a>{{/CanDelete}}
</header>
<h2>Interpreted Metadata</h2>
<table>{{#MetaData}}<tr><td>{{Key}}</td><td>{{{Value}}}</td></tr>{{/MetaData}}</table>
<h2>References</h2>
{{#HasLocLinks}}
<h3>Local</h3>
<ul>
{{#LocLinks}}
{{#Valid}}<li><a href="{{{Zid}}}">{{Zid}}</a></li>{{/Valid}}
{{^Valid}}<li>{{Zid}}</li>{{/Valid}}
{{/LocLinks}}
</ul>
{{/HasLocLinks}}
{{#HasQueryLinks}}
<h3>Queries</h3>
<ul>
{{#QueryLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/QueryLinks}}
</ul>
{{/HasQueryLinks}}
{{#HasExtLinks}}
<h3>External</h3>
<ul>
{{#ExtLinks}}
<li><a href="{{{.}}}"{{{ExtNewWindow}}}>{{.}}</a></li>
{{/ExtLinks}}
</ul>
{{/HasExtLinks}}
<h3>Unlinked</h3>
{{{UnLinksContent}}}
<form>
<label for="phrase">Search Phrase</label>
<input class="zs-input" type="text" id="phrase" name="{{QueryKeyPhrase}}" placeholder="Phrase.." value="{{UnLinksPhrase}}">
</form>
<h2>Parts and encodings</h2>
<table>
{{#EvalMatrix}}
<tr>
<th>{{Header}}</th>
{{#Elements}}<td><a href="{{{URL}}}">{{Text}}</td>
{{/Elements}}
</tr>
{{/EvalMatrix}}
</table>
<h3>Parsed (not evaluated)</h3>
<table>
{{#ParseMatrix}}
<tr>
<th>{{Header}}</th>
{{#Elements}}<td><a href="{{{URL}}}">{{Text}}</td>
{{/Elements}}
</tr>
{{/ParseMatrix}}
</table>
{{#HasShadowLinks}}
<h2>Shadowed Boxes</h2>
<ul>{{#ShadowLinks}}<li>{{.}}</li>{{/ShadowLinks}}</ul>
{{/HasShadowLinks}}
{{#Endnotes}}{{{Endnotes}}}{{/Endnotes}}
</article>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














































































































































Added box/constbox/info.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
`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-environment))
      ,@(if (bound? 'rename-url) `((@H " &#183; ") (a (@ (href ,rename-url)) "Rename")))
      ,@(if (bound? 'delete-url) `((@H " &#183; ") (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/license.txt.

1
2
3
4
5
6
7
8
Copyright (c) 2020-2022 Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official
|







1
2
3
4
5
6
7
8
Copyright (c) 2020-present Detlef Stern

                          Licensed under the EUPL

Zettelstore is licensed under the European Union Public License, version 1.2 or
later (EUPL v. 1.2). The license is available in the official languages of the
EU. The English version is included here. Please see
https://joinup.ec.europa.eu/community/eupl/og_page/eupl for official

Deleted box/constbox/listzettel.mustache.

1
2
3
4
5
6
7
<header>
<h1>{{Title}}</h1>
</header>
<form action="{{{SearchURL}}}">
<input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}">
</form>
{{{Content}}}
<
<
<
<
<
<
<














Added box/constbox/listzettel.sxn.













































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
`(article
  (header (h1 ,heading))
  (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "text") (placeholder "Search..") (name ,query-key-query) (value ,query-value) (dir "auto"))))
  ,@(if (bound? 'tag-zettel)
     `((p (@ (class "zs-tag-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@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")))
        )
      )
  )
)

Deleted box/constbox/login.mustache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<article>
<header>
<h1>{{Title}}</h1>
</header>
{{#Retry}}
<div class="zs-indication zs-error">Wrong user name / password. Try again.</div>
{{/Retry}}
<form method="POST" action="">
<div>
<label for="username">User name:</label>
<input class="zs-input" type="text" id="username" name="username" placeholder="Your user name.." autofocus>
</div>
<div>
<label for="password">Password:</label>
<input class="zs-input" type="password" id="password" name="password" placeholder="Your password..">
</div>
<div><input class="zs-primary" type="submit" value="Login"></div>
</form>
</article>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






































Added box/constbox/login.sxn.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
`(article
  (header (h1 "Login"))
  ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again.")))
  (form (@ (method "POST") (action ""))
    (div
      (label (@ (for "username")) "User name:")
      (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus))))
    (div
      (label (@ (for "password")) "Password:")
      (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password.."))))
    (div
      (input (@ (class "zs-primary") (type "submit") (value "Login"))))
  )
)

Deleted box/constbox/rename.mustache.

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
<article>
<header>
<h1>Rename Zettel {{Zid}}</h1>
</header>
<p>Do you really want to rename this zettel?</p>
{{#HasIncoming}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>If you rename this zettel, incoming references from the following zettel will become invalid.</p>
<ul>
{{#Incoming}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming}}
</ul>
</div>
{{/HasIncoming}}
{{#HasUselessFiles}}
<div class="zs-warning">
<h2>Warning!</h2>
<p>Renaming this zettel will also delete the following files, so that they will not be interpreted as content for a zettel with identifier {{Zid}}.</p>
<ul>
{{#UselessFiles}}
<li>{{{.}}}</li>
{{/UselessFiles}}
</ul>
</div>
{{/HasUselessFiles}}
<form method="POST">
<div>
<label for="newid">New zettel id</label>
<input class="zs-input" type="text" id="newzid" name="newzid" placeholder="ZID.." value="{{Zid}}" autofocus>
</div>
<input type="hidden" id="curzid" name="curzid" value="{{Zid}}">
<div><input class="zs-primary" type="submit" value="Rename"></div>
</form>
<dl>
{{#MetaPairs}}
<dt>{{Key}}:</dt><dd>{{Value}}</dd>
{{/MetaPairs}}
</dl>
</article>
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































Added box/constbox/rename.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
`(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/start.sxn.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;;;----------------------------------------------------------------------------
;;; 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.
;;;----------------------------------------------------------------------------

;;; This zettel is the start of the loading sequence for Sx code used in the
;;; Zettelstore. Via the precursor metadata, dependend zettel are evaluated
;;; before this zettel. You must always depend, directly or indirectly on the
;;; "Zettelstore Sxn Base Code" zettel. It provides the base definitions.

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
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
;;;----------------------------------------------------------------------------
;;; 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 takes a link (title . url) and returns a HTML reference.
(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)))

;; CSS-ROLE-map is a mapping (pair list, assoc list) of role names to zettel
;; identifier. It is used in the base template to update the metadata of the
;; HTML page to include some role specific CSS code.
;; Referenced in function "ROLE-DEFAULT-meta".
(define CSS-ROLE-map '())

;; ROLE-DEFAULT-meta returns some metadata for the base template. Any role
;; specific code should include the returned list of this function.
(define (ROLE-DEFAULT-meta env)
    `(,@(let (meta-role (environment-lookup 'meta-role env))
             (let (entry (assoc CSS-ROLE-map meta-role))
                  (if (pair? entry)
                      `((link (@ (rel "stylesheet") (href ,(zid-content-path (cdr entry))))))
                  )
             )
        )
    )
)

;;; ACTION-SEPARATOR defines a HTML value that separates actions links.
(define ACTION-SEPARATOR '(@H " &#183; "))

;;; ROLE-DEFAULT-actions returns the default text for actions.
(define (ROLE-DEFAULT-actions env)
    `(,@(let (copy-url (environment-lookup 'copy-url env))
             (if (defined? copy-url) `((@H " &#183; ") (a (@ (href ,copy-url)) "Copy"))))
      ,@(let (version-url (environment-lookup 'version-url env))
             (if (defined? version-url) `((@H " &#183; ") (a (@ (href ,version-url)) "Version"))))
      ,@(let (child-url (environment-lookup 'child-url env))
             (if (defined? child-url) `((@H " &#183; ") (a (@ (href ,child-url)) "Child"))))
      ,@(let (folge-url (environment-lookup 'folge-url env))
             (if (defined? folge-url) `((@H " &#183; ") (a (@ (href ,folge-url)) "Folge"))))
    )
)

;;; ROLE-tag-actions returns an additional action "Zettel" for zettel with role "tag".
(define (ROLE-tag-actions env)
    `(,@(ROLE-DEFAULT-actions env)
      ,@(let (title (environment-lookup 'title env))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (string-append "tags:" title)))) "Zettel"))
             )
        )
    )
)

;;; ROLE-DEFAULT-heading returns the default text for headings, below the
;;; references of a zettel. In most cases it should be called from an
;;; overwriting function.
(define (ROLE-DEFAULT-heading env)
    `(,@(let (meta-url (environment-lookup 'meta-url env))
           (if (defined? meta-url) `((br) "URL: " ,(url-to-html meta-url))))
      ,@(let (meta-author (environment-lookup 'meta-author env))
           (if (and (defined? meta-author) meta-author) `((br) "By " ,meta-author)))
    )
)

Deleted box/constbox/zettel.mustache.

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
<article>
<header>
<h1>{{{HTMLTitle}}}</h1>
<div class="zs-meta">
{{#CanWrite}}<a href="{{{EditURL}}}">Edit</a> &#183;{{/CanWrite}}
{{Zid}} &#183;
<a href="{{{InfoURL}}}">Info</a> &#183;
(<a href="{{{RoleURL}}}">{{RoleText}}</a>)
{{#HasTags}}&#183; {{#Tags}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags}}{{/HasTags}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
{{#Author}}<br>By {{Author}}{{/Author}}
</div>
</header>
{{{Content}}}
</article>
{{#HasFolgeLinks}}
<nav>
<details open>
<summary>Folgezettel</summary>
<ul>
{{#FolgeLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/FolgeLinks}}
</ul>
</details>
</nav>
{{/HasFolgeLinks}}
{{#HasBackLinks}}
<nav>
<details open>
<summary>Incoming</summary>
<ul>
{{#BackLinks}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/BackLinks}}
</ul>
</details>
</nav>
{{/HasBackLinks}}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




















































































Added box/constbox/zettel.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
`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#183; ")))
      ,zid (@H " &#183; ")
      (a (@ (href ,info-url)) "Info") (@H " &#183; ")
      "(" ,@(if (bound? 'role-url) `((a (@ (href ,role-url)) ,meta-role)))
          ,@(if (and (bound? 'folge-role-url) (bound? 'meta-folge-role))
                `((@H " &rarr; ") (a (@ (href ,folge-role-url)) ,meta-folge-role)))
      ")"
      ,@(if tag-refs `((@H " &#183; ") ,@tag-refs))
      ,@(ROLE-DEFAULT-actions (current-environment))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if superior-refs `((br) "Superior: " ,superior-refs))
      ,@(ROLE-DEFAULT-heading (current-environment))
    )
  )
  ,@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.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
18
19
20
21
22
23
24
25
26
27
28
29
30



31
32
33
34
35
36
37
	"os"
	"path/filepath"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"



)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		if krnl := kernel.Main; krnl != nil {
			log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child()







<
<
<



>
>
>







18
19
20
21
22
23
24



25
26
27
28
29
30
31
32
33
34
35
36
37
	"os"
	"path/filepath"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/box/notify"



	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		if krnl := kernel.Main; krnl != nil {
			log = krnl.GetLogger(kernel.BoxService).Clone().Str("box", "dir").Int("boxnum", int64(cdata.Number)).Child()
124
125
126
127
128
129
130


















131
132
133
134
135
136
137
	fCmds      []chan fileCmd
	mxCmds     sync.RWMutex
}

func (dp *dirBox) Location() string {
	return dp.location
}



















func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := uint32(0); i < dp.fSrvs; i++ {
		cc := make(chan fileCmd)







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







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
	fCmds      []chan fileCmd
	mxCmds     sync.RWMutex
}

func (dp *dirBox) Location() string {
	return dp.location
}

func (dp *dirBox) State() box.StartState {
	if ds := dp.dirSrv; ds != nil {
		switch ds.State() {
		case notify.DsCreated:
			return box.StartStateStopped
		case notify.DsStarting:
			return box.StartStateStarting
		case notify.DsWorking:
			return box.StartStateStarted
		case notify.DsMissing:
			return box.StartStateStarted
		case notify.DsStopping:
			return box.StartStateStopping
		}
	}
	return box.StartStateStopped
}

func (dp *dirBox) Start(context.Context) error {
	dp.mxCmds.Lock()
	defer dp.mxCmds.Unlock()
	dp.fCmds = make([]chan fileCmd, 0, dp.fSrvs)
	for i := uint32(0); i < dp.fSrvs; i++ {
		cc := make(chan fileCmd)
149
150
151
152
153
154
155

156
157
158
159
160
161
162
	}
	if err != nil {
		dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor")
		dp.stopFileServices()
		return err
	}
	dp.dirSrv = notify.NewDirService(

		dp.log.Clone().Str("sub", "dirsrv").Child(),
		notifier,
		dp.cdata.Notify,
	)
	dp.dirSrv.Start()
	return nil
}







>







167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
	}
	if err != nil {
		dp.log.Fatal().Err(err).Msg("Unable to create directory supervisor")
		dp.stopFileServices()
		return err
	}
	dp.dirSrv = notify.NewDirService(
		dp,
		dp.log.Clone().Str("sub", "dirsrv").Child(),
		notifier,
		dp.cdata.Notify,
	)
	dp.dirSrv.Start()
	return nil
}
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

func (dp *dirBox) stopFileServices() {
	for _, c := range dp.fCmds {
		close(c)
	}
}

func (dp *dirBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
	if chci := dp.cdata.Notify; chci != nil {
		dp.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChanged")
		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
	}
}

func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd {
	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
	sum := 2166136261 ^ uint32(zid)
	sum *= 16777619
	sum ^= uint32(zid >> 32)
	sum *= 16777619

	dp.mxCmds.RLock()
	defer dp.mxCmds.RUnlock()
	return dp.fCmds[sum%dp.fSrvs]
}

func (dp *dirBox) CanCreateZettel(_ context.Context) bool {
	return !dp.readonly
}

func (dp *dirBox) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	if dp.readonly {
		return id.Invalid, box.ErrReadOnly
	}

	newZid, err := dp.dirSrv.SetNewDirEntry()
	if err != nil {
		return id.Invalid, err
	}
	meta := zettel.Meta
	meta.Zid = newZid
	entry := notify.DirEntry{Zid: newZid}
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

	err = dp.srvSetZettel(ctx, &entry, zettel)
	if err == nil {
		err = dp.dirSrv.UpdateDirEntry(&entry)
	}
	dp.notifyChanged(box.OnZettel, meta.Zid)
	dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel")
	return meta.Zid, err
}

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return domain.Zettel{}, box.ErrNotFound
	}
	m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
	if err != nil {
		return domain.Zettel{}, err
	}
	zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)}
	dp.log.Trace().Zid(zid).Msg("GetZettel")
	return zettel, nil
}

func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	m, err := dp.doGetMeta(ctx, zid)
	dp.log.Trace().Zid(zid).Err(err).Msg("GetMeta")
	return m, err
}
func (dp *dirBox) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return nil, box.ErrNotFound
	}
	m, err := dp.srvGetMeta(ctx, entry, zid)
	if err != nil {
		return nil, err
	}
	return m, nil
}

func (dp *dirBox) 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)







|

|
|



















|

















|




|


|



|

|




|
<
<
<
<
<
|
<
<
<
<
<
<
<
<







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

func (dp *dirBox) stopFileServices() {
	for _, c := range dp.fCmds {
		close(c)
	}
}

func (dp *dirBox) notifyChanged(zid id.Zid) {
	if chci := dp.cdata.Notify; chci != nil {
		dp.log.Trace().Zid(zid).Msg("notifyChanged")
		chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid}
	}
}

func (dp *dirBox) getFileChan(zid id.Zid) chan fileCmd {
	// Based on https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
	sum := 2166136261 ^ uint32(zid)
	sum *= 16777619
	sum ^= uint32(zid >> 32)
	sum *= 16777619

	dp.mxCmds.RLock()
	defer dp.mxCmds.RUnlock()
	return dp.fCmds[sum%dp.fSrvs]
}

func (dp *dirBox) CanCreateZettel(_ context.Context) bool {
	return !dp.readonly
}

func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	if dp.readonly {
		return id.Invalid, box.ErrReadOnly
	}

	newZid, err := dp.dirSrv.SetNewDirEntry()
	if err != nil {
		return id.Invalid, err
	}
	meta := zettel.Meta
	meta.Zid = newZid
	entry := notify.DirEntry{Zid: newZid}
	dp.updateEntryFromMetaContent(&entry, meta, zettel.Content)

	err = dp.srvSetZettel(ctx, &entry, zettel)
	if err == nil {
		err = dp.dirSrv.UpdateDirEntry(&entry)
	}
	dp.notifyChanged(meta.Zid)
	dp.log.Trace().Err(err).Zid(meta.Zid).Msg("CreateZettel")
	return meta.Zid, err
}

func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
	if err != nil {
		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)
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
		}
		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
		handle(m)
	}
	return nil
}

func (dp *dirBox) CanUpdateZettel(context.Context, domain.Zettel) bool {
	return !dp.readonly
}

func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	meta := zettel.Meta
	zid := meta.Zid
	if !zid.IsValid() {
		return &box.ErrInvalidID{Zid: zid}
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
	dp.dirSrv.UpdateDirEntry(entry)
	err := dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(box.OnZettel, zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel")
	return err
}

func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content domain.Content) {
	entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax)
}

func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool {
	return !dp.readonly
}

func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if curZid == newZid {
		return nil
	}
	curEntry := dp.dirSrv.GetDirEntry(curZid)
	if !curEntry.IsValid() {
		return box.ErrNotFound
	}
	if dp.readonly {
		return box.ErrReadOnly
	}

	// Check whether zettel with new ID already exists in this box.
	if _, err := dp.doGetMeta(ctx, newZid); err == nil {
		return &box.ErrInvalidID{Zid: newZid}
	}

	oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid)
	if err != nil {
		return err
	}

	newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid)
	if err != nil {
		return err
	}
	oldMeta.Zid = newZid
	newZettel := domain.Zettel{Meta: oldMeta, Content: domain.NewContent(oldContent)}
	if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil {
		// "Rollback" rename. No error checking...
		dp.dirSrv.RenameDirEntry(&newEntry, curZid)
		return err
	}
	err = dp.srvDeleteZettel(ctx, curEntry, curZid)
	if err == nil {
		dp.notifyChanged(box.OnZettel, curZid)
		dp.notifyChanged(box.OnZettel, newZid)
	}
	dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel")
	return err
}

func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	return entry.IsValid()
}

func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return box.ErrNotFound
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(box.OnZettel, zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel")
	return err
}

func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = dp.readonly
	st.Zettel = dp.dirSrv.NumDirEntries()
	dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}







|



|







|










|





|













|






|
|












|







|
|




















|







|










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
		}
		dp.cdata.Enricher.Enrich(ctx, m, dp.number)
		handle(m)
	}
	return nil
}

func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool {
	return !dp.readonly
}

func (dp *dirBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	meta := zettel.Meta
	zid := meta.Zid
	if !zid.IsValid() {
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
	dp.dirSrv.UpdateDirEntry(entry)
	err := dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("UpdateZettel")
	return err
}

func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) {
	entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax)
}

func (dp *dirBox) AllowRenameZettel(context.Context, id.Zid) bool {
	return !dp.readonly
}

func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if curZid == newZid {
		return nil
	}
	curEntry := dp.dirSrv.GetDirEntry(curZid)
	if !curEntry.IsValid() {
		return box.ErrZettelNotFound{Zid: curZid}
	}
	if dp.readonly {
		return box.ErrReadOnly
	}

	// Check whether zettel with new ID already exists in this box.
	if dp.HasZettel(ctx, newZid) {
		return box.ErrInvalidZid{Zid: newZid.String()}
	}

	oldMeta, oldContent, err := dp.srvGetMetaContent(ctx, curEntry, curZid)
	if err != nil {
		return err
	}

	newEntry, err := dp.dirSrv.RenameDirEntry(curEntry, newZid)
	if err != nil {
		return err
	}
	oldMeta.Zid = newZid
	newZettel := zettel.Zettel{Meta: oldMeta, Content: zettel.NewContent(oldContent)}
	if err = dp.srvSetZettel(ctx, &newEntry, newZettel); err != nil {
		// "Rollback" rename. No error checking...
		dp.dirSrv.RenameDirEntry(&newEntry, curZid)
		return err
	}
	err = dp.srvDeleteZettel(ctx, curEntry, curZid)
	if err == nil {
		dp.notifyChanged(curZid)
		dp.notifyChanged(newZid)
	}
	dp.log.Trace().Zid(curZid).Zid(newZid).Err(err).Msg("RenameZettel")
	return err
}

func (dp *dirBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	if dp.readonly {
		return false
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	return entry.IsValid()
}

func (dp *dirBox) DeleteZettel(ctx context.Context, zid id.Zid) error {
	if dp.readonly {
		return box.ErrReadOnly
	}

	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return box.ErrZettelNotFound{Zid: zid}
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(zid)
	}
	dp.log.Trace().Zid(zid).Err(err).Msg("DeleteZettel")
	return err
}

func (dp *dirBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = dp.readonly
	st.Zettel = dp.dirSrv.NumDirEntries()
	dp.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

Changes to box/dirbox/dirbox_test.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package dirbox

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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 dirbox

Changes to box/dirbox/service.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package dirbox

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"time"

	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"



)

func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) {
	// Something may panic. Ensure a running service.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("FileService", r)
			go fileService(i, log, dirPath, cmds)
		}
	}()

	log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started")
	for cmd := range cmds {
		cmd.run(log, dirPath)

|



















<
<
<



>
>
>





|
|







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
//-----------------------------------------------------------------------------
// 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 dirbox

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"time"

	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/notify"



	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func fileService(i uint32, log *logger.Logger, dirPath string, cmds <-chan fileCmd) {
	// Something may panic. Ensure a running service.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("FileService", ri)
			go fileService(i, log, dirPath, cmds)
		}
	}()

	log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started")
	for cmd := range cmds {
		cmd.run(log, dirPath)
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
	cmd.rc <- resGetMetaContent{m, content, err}
}

// COMMAND: srvSetZettel ----------------------------------------
//
// Writes a new or exsting zettel.

func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel domain.Zettel) error {
	rc := make(chan resSetZettel, 1)
	dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc}
	ctx, cancel := context.WithTimeout(ctx, serviceTimeout)
	defer cancel()
	select {
	case err := <-rc:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}
}

type fileSetZettel struct {
	entry  *notify.DirEntry
	zettel domain.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) {
	entry := cmd.entry
	zid := entry.Zid







|














|







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
	cmd.rc <- resGetMetaContent{m, content, err}
}

// COMMAND: srvSetZettel ----------------------------------------
//
// Writes a new or exsting zettel.

func (dp *dirBox) srvSetZettel(ctx context.Context, entry *notify.DirEntry, zettel zettel.Zettel) error {
	rc := make(chan resSetZettel, 1)
	dp.getFileChan(zettel.Meta.Zid) <- &fileSetZettel{entry, zettel, rc}
	ctx, cancel := context.WithTimeout(ctx, serviceTimeout)
	defer cancel()
	select {
	case err := <-rc:
		return err
	case <-ctx.Done():
		return ctx.Err()
	}
}

type fileSetZettel struct {
	entry  *notify.DirEntry
	zettel zettel.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) {
	entry := cmd.entry
	zid := entry.Zid

Changes to box/filebox/filebox.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package filebox provides boxes that are stored in a file.
package filebox

import (
	"errors"
	"net/url"
	"path/filepath"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
)

func init() {
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {

|

















|


|
|
|







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
//-----------------------------------------------------------------------------
// 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 filebox provides boxes that are stored in a file.
package filebox

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"
)

func init() {
	manager.Register("file", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		path := getFilepathFromURL(u)
		ext := strings.ToLower(filepath.Ext(path))
		if ext != ".zip" {

Changes to box/filebox/zipbox.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package filebox

import (
	"archive/zip"
	"context"
	"io"
	"strings"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"



)

type zipBox struct {
	log      *logger.Logger
	number   int
	name     string
	enricher box.Enricher
	notify   chan<- box.UpdateInfo
	dirSrv   *notify.DirService
}

func (zb *zipBox) Location() string {
	if strings.HasPrefix(zb.name, "/") {
		return "file://" + zb.name
	}
	return "file:" + zb.name
}



















func (zb *zipBox) Start(context.Context) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	reader.Close()
	zipNotifier, err := notify.NewSimpleZipNotifier(zb.log, zb.name)
	if err != nil {
		return err
	}
	zb.dirSrv = notify.NewDirService(zb.log, zipNotifier, zb.notify)
	zb.dirSrv.Start()
	return nil
}

func (zb *zipBox) Refresh(_ context.Context) {
	zb.dirSrv.Refresh()
	zb.log.Trace().Msg("Refresh")
}

func (zb *zipBox) Stop(context.Context) {
	zb.dirSrv.Stop()

}

func (*zipBox) CanCreateZettel(context.Context) bool { return false }

func (zb *zipBox) CreateZettel(context.Context, domain.Zettel) (id.Zid, error) {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("CreateZettel")
	return id.Invalid, err
}

func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) {
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return domain.Zettel{}, box.ErrNotFound
	}
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return domain.Zettel{}, err
	}
	defer reader.Close()

	var m *meta.Meta
	var src []byte
	var inMeta bool

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel")
		}
		src, err = readZipFileContent(reader, entry.ContentName)
		if err != nil {
			return domain.Zettel{}, err
		}
		if entry.HasMetaInContent() {
			inp := input.NewInput(src)
			m = meta.NewFromInput(zid, inp)
			src = src[inp.Pos:]
		} else {
			m = CalcDefaultMeta(zid, entry.ContentExt)
		}
	} else {
		m, err = readZipMetaFile(reader, zid, metaName)
		if err != nil {
			return domain.Zettel{}, err
		}
		inMeta = true
		if contentName != "" {
			src, err = readZipFileContent(reader, entry.ContentName)
			if err != nil {
				return domain.Zettel{}, err
			}
		}
	}

	CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles)
	zb.log.Trace().Zid(zid).Msg("GetZettel")
	return domain.Zettel{Meta: m, Content: domain.NewContent(src)}, nil
}

func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return nil, box.ErrNotFound
	}
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return nil, err
	}
	defer reader.Close()
	m, err := zb.readZipMeta(reader, zid, entry)
	zb.log.Trace().Err(err).Zid(zid).Msg("GetMeta")
	return m, err
}

func (zb *zipBox) 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)

|


















<
<
<



>
>
>

















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







|
<
<
<
|











>




|





|


|



|














|











|





|






|


|
|
<
<
<
<
<
<
<
<
<
<
<







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
//-----------------------------------------------------------------------------
// 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 filebox

import (
	"archive/zip"
	"context"
	"io"
	"strings"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/notify"



	"zettelstore.de/z/input"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type zipBox struct {
	log      *logger.Logger
	number   int
	name     string
	enricher box.Enricher
	notify   chan<- box.UpdateInfo
	dirSrv   *notify.DirService
}

func (zb *zipBox) Location() string {
	if strings.HasPrefix(zb.name, "/") {
		return "file://" + zb.name
	}
	return "file:" + zb.name
}

func (zb *zipBox) State() box.StartState {
	if ds := zb.dirSrv; ds != nil {
		switch ds.State() {
		case notify.DsCreated:
			return box.StartStateStopped
		case notify.DsStarting:
			return box.StartStateStarting
		case notify.DsWorking:
			return box.StartStateStarted
		case notify.DsMissing:
			return box.StartStateStarted
		case notify.DsStopping:
			return box.StartStateStopping
		}
	}
	return box.StartStateStopped
}

func (zb *zipBox) Start(context.Context) error {
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return err
	}
	reader.Close()
	zipNotifier := notify.NewSimpleZipNotifier(zb.log, zb.name)



	zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify)
	zb.dirSrv.Start()
	return nil
}

func (zb *zipBox) Refresh(_ context.Context) {
	zb.dirSrv.Refresh()
	zb.log.Trace().Msg("Refresh")
}

func (zb *zipBox) Stop(context.Context) {
	zb.dirSrv.Stop()
	zb.dirSrv = nil
}

func (*zipBox) CanCreateZettel(context.Context) bool { return false }

func (zb *zipBox) CreateZettel(context.Context, zettel.Zettel) (id.Zid, error) {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("CreateZettel")
	return id.Invalid, err
}

func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return zettel.Zettel{}, err
	}
	defer reader.Close()

	var m *meta.Meta
	var src []byte
	var inMeta bool

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel")
		}
		src, err = readZipFileContent(reader, entry.ContentName)
		if err != nil {
			return zettel.Zettel{}, err
		}
		if entry.HasMetaInContent() {
			inp := input.NewInput(src)
			m = meta.NewFromInput(zid, inp)
			src = src[inp.Pos:]
		} else {
			m = CalcDefaultMeta(zid, entry.ContentExt)
		}
	} else {
		m, err = readZipMetaFile(reader, zid, metaName)
		if err != nil {
			return zettel.Zettel{}, err
		}
		inMeta = true
		if contentName != "" {
			src, err = readZipFileContent(reader, entry.ContentName)
			if err != nil {
				return zettel.Zettel{}, err
			}
		}
	}

	CleanupMeta(m, zid, entry.ContentExt, inMeta, entry.UselessFiles)
	zb.log.Trace().Zid(zid).Msg("GetZettel")
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil
}

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)
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
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

func (*zipBox) CanUpdateZettel(context.Context, domain.Zettel) bool { return false }

func (zb *zipBox) UpdateZettel(context.Context, domain.Zettel) error {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("UpdateZettel")
	return err
}

func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	entry := zb.dirSrv.GetDirEntry(zid)
	return !entry.IsValid()
}

func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error {
	err := box.ErrReadOnly
	if curZid == newZid {
		err = nil
	}
	curEntry := zb.dirSrv.GetDirEntry(curZid)
	if !curEntry.IsValid() {
		err = box.ErrNotFound
	}
	zb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	err := box.ErrReadOnly
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		err = box.ErrNotFound
	}
	zb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true







|

|

















|











|







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
		}
		zb.enricher.Enrich(ctx, m, zb.number)
		handle(m)
	}
	return nil
}

func (*zipBox) CanUpdateZettel(context.Context, zettel.Zettel) bool { return false }

func (zb *zipBox) UpdateZettel(context.Context, zettel.Zettel) error {
	err := box.ErrReadOnly
	zb.log.Trace().Err(err).Msg("UpdateZettel")
	return err
}

func (zb *zipBox) AllowRenameZettel(_ context.Context, zid id.Zid) bool {
	entry := zb.dirSrv.GetDirEntry(zid)
	return !entry.IsValid()
}

func (zb *zipBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error {
	err := box.ErrReadOnly
	if curZid == newZid {
		err = nil
	}
	curEntry := zb.dirSrv.GetDirEntry(curZid)
	if !curEntry.IsValid() {
		err = box.ErrZettelNotFound{Zid: curZid}
	}
	zb.log.Trace().Err(err).Msg("RenameZettel")
	return err
}

func (*zipBox) CanDeleteZettel(context.Context, id.Zid) bool { return false }

func (zb *zipBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	err := box.ErrReadOnly
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	zb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

func (zb *zipBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = true

Changes to box/helper.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) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package box

import (


	"time"

	"zettelstore.de/z/domain/id"
)

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
	withSeconds := false
	for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout)
		zid := id.New(withSeconds)
		found, err := testZid(zid)
		if err != nil {
			return id.Invalid, err
		}
		if found {
			return zid, nil
		}
		// TODO: do not wait here unconditionally.
		time.Sleep(100 * time.Millisecond)
		withSeconds = true
	}
	return id.Invalid, ErrConflict
}


























|

|









>
>


|




















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
//-----------------------------------------------------------------------------
// 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 box

import (
	"net/url"
	"strconv"
	"time"

	"zettelstore.de/z/zettel/id"
)

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
	withSeconds := false
	for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout)
		zid := id.New(withSeconds)
		found, err := testZid(zid)
		if err != nil {
			return id.Invalid, err
		}
		if found {
			return zid, nil
		}
		// TODO: do not wait here unconditionally.
		time.Sleep(100 * time.Millisecond)
		withSeconds = true
	}
	return id.Invalid, ErrConflict
}

// GetQueryBool is a helper function to extract bool values from a box URI.
func GetQueryBool(u *url.URL, key string) bool {
	_, ok := u.Query()[key]
	return ok
}

// GetQueryInt is a helper function to extract int values of a specified range from a box URI.
func GetQueryInt(u *url.URL, key string, min, def, max int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return def
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return def
	}
	if iVal < min {
		return min
	}
	if iVal > max {
		return max
	}
	return iVal
}

Changes to box/manager/anteroom.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"sync"

	"zettelstore.de/z/domain/id"
)

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

type anteroom struct {
	num     uint64
	next    *anteroom
	waiting map[id.Zid]arAction
	curLoad int
	reload  bool
}

type anterooms struct {
	mx      sync.Mutex
	nextNum uint64
	first   *anteroom
	last    *anteroom
	maxLoad int
}

func newAnterooms(maxLoad int) *anterooms {
	return &anterooms{maxLoad: maxLoad}
}

func (ar *anterooms) EnqueueZettel(zid id.Zid) {
	if !zid.IsValid() {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid, arZettel)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not put zettel in reload room
		}
		if _, ok := room.waiting[zid]; ok {
			// Zettel is already waiting.
			return
		}
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting[zid] = arZettel
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid, arZettel)
	ar.last.next = room
	ar.last = room
}

func (ar *anterooms) makeAnteroom(zid id.Zid, action arAction) *anteroom {




	c := ar.maxLoad
	if c == 0 {
		c = 100
	}
	waiting := make(map[id.Zid]arAction, c)
	waiting[zid] = action
	ar.nextNum++
	return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anterooms) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid, arReload)
	ar.last = ar.first
}

func (ar *anterooms) Reload(newZids id.Set) uint64 {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	newWaiting := createWaitingSet(newZids)
	ar.deleteReloadedRooms()

	if ns := len(newWaiting); ns > 0 {
		ar.nextNum++
		ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newWaiting, curLoad: ns}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return ar.nextNum
	}

	ar.first = nil
	ar.last = nil
	return 0
}

func createWaitingSet(zids id.Set) map[id.Zid]arAction {
	waitingSet := make(map[id.Zid]arAction, len(zids))
	for zid := range zids {
		if zid.IsValid() {
			waitingSet[zid] = arZettel
		}
	}
	return waitingSet
}

func (ar *anterooms) deleteReloadedRooms() {
	room := ar.first
	for room != nil && room.reload {
		room = room.next
	}
	ar.first = room
	if room == nil {
		ar.last = nil
	}
}

func (ar *anterooms) Dequeue() (arAction, id.Zid, uint64) {
	ar.mx.Lock()
	defer ar.mx.Unlock()

	if ar.first == nil {
		return arNothing, id.Invalid, 0
	}
	for zid, action := range ar.first.waiting {
		roomNo := ar.first.num





		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {









			ar.first = ar.first.next
			if ar.first == nil {
				ar.last = nil
			}
		}
		return action, zid, roomNo
	}
	return arNothing, id.Invalid, 0
}

|













|













|




|







|
<
|
<
|






|








|




|



|




|
>
>
>
>




|
<
<



|


|



|


<


|

|











<
<
<
<
<
<
<
<
<
<
|










|


>
|


<
|
>
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
|
|
|
|
|
<
<
<
<
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




//-----------------------------------------------------------------------------
// 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 manager

import (
	"sync"

	"zettelstore.de/z/zettel/id"
)

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

type anteroom struct {
	num     uint64
	next    *anteroom
	waiting id.Set
	curLoad int
	reload  bool
}

type anteroomQueue struct {
	mx      sync.Mutex
	nextNum uint64
	first   *anteroom
	last    *anteroom
	maxLoad int
}

func newAnteroomQueue(maxLoad int) *anteroomQueue { return &anteroomQueue{maxLoad: maxLoad} }



func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) {
	if !zid.IsValid() {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not put zettel in reload room
		}
		if _, ok := room.waiting[zid]; ok {
			// Zettel is already waiting. Nothing to do.
			return
		}
	}
	if room := ar.last; !room.reload && (ar.maxLoad == 0 || room.curLoad < ar.maxLoad) {
		room.waiting.Add(zid)
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid)
	ar.last.next = room
	ar.last = room
}

func (ar *anteroomQueue) makeAnteroom(zid id.Zid) *anteroom {
	ar.nextNum++
	if zid == id.Invalid {
		return &anteroom{num: ar.nextNum, next: nil, waiting: nil, curLoad: 0, reload: true}
	}
	c := ar.maxLoad
	if c == 0 {
		c = 100
	}
	waiting := id.NewSetCap(ar.maxLoad, zid)


	return &anteroom{num: ar.nextNum, next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anteroomQueue) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid)
	ar.last = ar.first
}

func (ar *anteroomQueue) Reload(allZids id.Set) uint64 {
	ar.mx.Lock()
	defer ar.mx.Unlock()

	ar.deleteReloadedRooms()

	if ns := len(allZids); ns > 0 {
		ar.nextNum++
		ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: allZids, curLoad: ns, reload: true}
		if ar.first.next == nil {
			ar.last = ar.first
		}
		return ar.nextNum
	}

	ar.first = nil
	ar.last = nil
	return 0
}











func (ar *anteroomQueue) deleteReloadedRooms() {
	room := ar.first
	for room != nil && room.reload {
		room = room.next
	}
	ar.first = room
	if room == nil {
		ar.last = nil
	}
}

func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, uint64) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	first := ar.first
	if first == nil {
		return arNothing, id.Invalid, 0
	}

	roomNo := first.num
	if first.waiting == nil && first.reload {
		ar.removeFirst()
		return arReload, id.Invalid, roomNo
	}
	for zid := range first.waiting {
		delete(first.waiting, zid)
		if len(first.waiting) == 0 {
			ar.removeFirst()
		}
		return arZettel, zid, roomNo
	}
	ar.removeFirst()
	return arNothing, id.Invalid, 0
}

func (ar *anteroomQueue) removeFirst() {
	ar.first = ar.first.next
	if ar.first == nil {
		ar.last = nil
	}
}




Changes to box/manager/anteroom_test.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnterooms(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, rno := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || rno != 1 {
		t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {

|













|




|







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) 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 manager

import (
	"testing"

	"zettelstore.de/z/zettel/id"
)

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, rno := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || rno != 1 {
		t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	t.Parallel()
	ar := newAnterooms(1)
	ar.EnqueueZettel(id.Zid(1))
	ar.Reset()
	action, zid, _ := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.NewSet(3, 4))







|







48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
	if count != 3 {
		t.Errorf("Expected 3 dequeues, but got %v", count)
	}
}

func TestReset(t *testing.T) {
	t.Parallel()
	ar := newAnteroomQueue(1)
	ar.EnqueueZettel(id.Zid(1))
	ar.Reset()
	action, zid, _ := ar.Dequeue()
	if action != arReload || zid != id.Invalid {
		t.Errorf("Expected reload & invalid Zid, but got %v/%v", action, zid)
	}
	ar.Reload(id.NewSet(3, 4))
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
		t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.Reload(id.NewSet(id.Zid(6)))
	action, zid, _ = ar.Dequeue()
	if zid != id.Zid(6) || action != arZettel {
		t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnterooms(1)
	ar.EnqueueZettel(id.Zid(8))
	ar.Reload(nil)
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}
}







|










|







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
		t.Errorf("Expected 5/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnteroomQueue(1)
	ar.Reload(id.NewSet(id.Zid(6)))
	action, zid, _ = ar.Dequeue()
	if zid != id.Zid(6) || action != arZettel {
		t.Errorf("Expected 6/arZettel, but got %v/%v", zid, action)
	}
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}

	ar = newAnteroomQueue(1)
	ar.EnqueueZettel(id.Zid(8))
	ar.Reload(nil)
	action, zid, _ = ar.Dequeue()
	if action != arNothing || zid != id.Invalid {
		t.Errorf("Expected nothing & invalid Zid, but got %v/%v", action, zid)
	}
}

Changes to box/manager/box.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
277
278
279
280
281
282
283
284
285
286
287


288
289
290
291
292

293
294
295
296
297
298
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"bytes"
	"context"
	"errors"


	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.
func (mgr *Manager) Location() string {
	if len(mgr.boxes) <= 2 {
		return "NONE"
	}
	var buf bytes.Buffer
	for i := 0; i < len(mgr.boxes)-2; i++ {
		if i > 0 {
			buf.WriteString(", ")
		}
		buf.WriteString(mgr.boxes[i].Location())
	}
	return buf.String()
}

// CanCreateZettel returns true, if box could possibly create a new zettel.
func (mgr *Manager) CanCreateZettel(ctx context.Context) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.started && mgr.boxes[0].CanCreateZettel(ctx)
}

// CreateZettel creates a new zettel.
func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
	mgr.mgrLog.Debug().Msg("CreateZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return id.Invalid, box.ErrStopped
	}


	return mgr.boxes[0].CreateZettel(ctx, zettel)
}

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel")



	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return domain.Zettel{}, box.ErrStopped
	}
	for i, p := range mgr.boxes {

		if z, err := p.GetZettel(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return domain.Zettel{}, box.ErrNotFound
}

// GetAllZettel retrieves a specific zettel from all managed boxes.
func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}


	var result []domain.Zettel
	for i, p := range mgr.boxes {
		if z, err := p.GetZettel(ctx, zid); err == nil {
			mgr.Enrich(ctx, z.Meta, i+1)
			result = append(result, z)
		}
	}
	return result, nil
}

// GetMeta retrieves just the meta data of a specific zettel.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	return mgr.doGetMeta(ctx, zid)
}

func (mgr *Manager) doGetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	for i, p := range mgr.boxes {
		if m, err := p.GetMeta(ctx, zid); err != box.ErrNotFound {
			if err == nil {
				mgr.Enrich(ctx, m, i+1)
			}
			return m, err
		}
	}
	return nil, box.ErrNotFound
}

// GetAllMeta retrieves the meta data of a specific zettel from all managed boxes.
func (mgr *Manager) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllMeta")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	var result []*meta.Meta
	for i, p := range mgr.boxes {
		if m, err := p.GetMeta(ctx, zid); err == nil {
			mgr.Enrich(ctx, m, i+1)
			result = append(result, m)
		}
	}
	return result, nil
}

// FetchZids returns the set of all zettel identifer managed by the box.
func (mgr *Manager) FetchZids(ctx context.Context) (id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZids")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	result := id.Set{}


	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
}















type metaMap map[id.Zid]*meta.Meta














// SelectMeta returns all zettel meta data that match the selection
// criteria. The result is ordered by descending zettel id.
func (mgr *Manager) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	if msg := mgr.mgrLog.Debug(); msg.Enabled() {
		msg.Str("query", q.String()).Msg("SelectMeta")
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}



	compSearch := q.RetrieveAndCompile(mgr)




	selected := metaMap{}
	for _, term := range compSearch.Terms {
		rejected := id.Set{}
		handleMeta := func(m *meta.Meta) {
			zid := m.Zid
			if rejected.Contains(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}
			if compSearch.PreMatch(m) && term.Match(m) {
				selected[zid] = m
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match")
			} else {
				rejected.Zid(zid)
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject")
			}
		}
		for _, p := range mgr.boxes {
			if err := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err != nil {
				return nil, err
			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}


	return q.Sort(result), nil
}

// CanUpdateZettel returns true, if box could possibly update the given zettel.
func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel)
}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
	}
	// Remove all (computed) properties from metadata before storing the zettel.
	zettel.Meta = zettel.Meta.Clone()
	for _, p := range zettel.Meta.ComputedPairsRest() {
		if mgr.propertyKeys.Has(p.Key) {
			zettel.Meta.Delete(p.Key)
		}
	}
	return mgr.boxes[0].UpdateZettel(ctx, zettel)
}

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}


	for _, p := range mgr.boxes {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
	}
	return true
}

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
	}


	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)

		if err != nil && !errors.Is(err, box.ErrNotFound) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	return nil
}

// CanDeleteZettel returns true, if box could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}


	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
	}


	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			return nil
		}

		if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrNotFound
}

|











<


>


|
|
|
|









|


|

|

|






|



|

<
|
<


>
>




|

>
>
>


<
<
<

>
|






|



|

<
|
<


>
>
|









<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<



<
|
<



>
>

|







>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
>
|
>
>
>
>
>
>
>
>
>


|



<
|
<


>
>

|
>
>
>
>
|




|











|




|
|







>
>
|



|


|



|

<
|
<














<
|
<


>
>











<
|
<


>
>


>
|











<
|
<


>
>











<
|
<


>
>





>
|



|

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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
//-----------------------------------------------------------------------------
// 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 manager

import (

	"context"
	"errors"
	"strings"

	"zettelstore.de/z/box"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Conatains all box.Box related functions

// Location returns some information where the box is located.
func (mgr *Manager) Location() string {
	if len(mgr.boxes) <= 2 {
		return "NONE"
	}
	var sb strings.Builder
	for i := 0; i < len(mgr.boxes)-2; i++ {
		if i > 0 {
			sb.WriteString(", ")
		}
		sb.WriteString(mgr.boxes[i].Location())
	}
	return sb.String()
}

// CanCreateZettel returns true, if box could possibly create a new zettel.
func (mgr *Manager) CanCreateZettel(ctx context.Context) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanCreateZettel(ctx)
}

// CreateZettel creates a new zettel.
func (mgr *Manager) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	mgr.mgrLog.Debug().Msg("CreateZettel")

	if mgr.State() != box.StartStateStarted {

		return id.Invalid, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.boxes[0].CreateZettel(ctx, zettel)
}

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetZettel")
	if mgr.State() != box.StartStateStarted {
		return zettel.Zettel{}, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()



	for i, p := range mgr.boxes {
		var errZNF box.ErrZettelNotFound
		if z, err := p.GetZettel(ctx, zid); !errors.As(err, &errZNF) {
			if err == nil {
				mgr.Enrich(ctx, z.Meta, i+1)
			}
			return z, err
		}
	}
	return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
}

// GetAllZettel retrieves a specific zettel from all managed boxes.
func (mgr *Manager) GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetAllZettel")

	if mgr.State() != box.StartStateStarted {

		return nil, box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	var result []zettel.Zettel
	for i, p := range mgr.boxes {
		if z, err := p.GetZettel(ctx, zid); err == nil {
			mgr.Enrich(ctx, z.Meta, i+1)
			result = append(result, z)
		}
	}
	return result, nil
}










































// 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.Add(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.ContainsOrNil(zid) {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadyRejected")
				return
			}
			if _, ok := selected[zid]; ok {
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/alreadySelected")
				return
			}
			if compSearch.PreMatch(m) && term.Match(m) {
				selected[zid] = m
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/match")
			} else {
				rejected.Add(zid)
				mgr.mgrLog.Trace().Zid(zid).Msg("SelectMeta/reject")
			}
		}
		for _, p := range mgr.boxes {
			if err2 := p.ApplyMeta(ctx, handleMeta, term.Retrieve); err2 != nil {
				return nil, err2
			}
		}
	}
	result := make([]*meta.Meta, 0, len(selected))
	for _, m := range selected {
		result = append(result, m)
	}
	result = compSearch.AfterSearch(result)
	mgr.mgrLog.Trace().Int("count", int64(len(result))).Msg("found with ApplyMeta")
	return result, nil
}

// CanUpdateZettel returns true, if box could possibly update the given zettel.
func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel zettel.Zettel) bool {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	return mgr.State() == box.StartStateStarted && mgr.boxes[0].CanUpdateZettel(ctx, zettel)
}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")

	if mgr.State() != box.StartStateStarted {

		return box.ErrStopped
	}
	// Remove all (computed) properties from metadata before storing the zettel.
	zettel.Meta = zettel.Meta.Clone()
	for _, p := range zettel.Meta.ComputedPairsRest() {
		if mgr.propertyKeys.Has(p.Key) {
			zettel.Meta.Delete(p.Key)
		}
	}
	return mgr.boxes[0].UpdateZettel(ctx, zettel)
}

// AllowRenameZettel returns true, if box will not disallow renaming the zettel.
func (mgr *Manager) AllowRenameZettel(ctx context.Context, zid id.Zid) bool {

	if mgr.State() != box.StartStateStarted {

		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
	}
	return true
}

// RenameZettel changes the current zid to a new zid.
func (mgr *Manager) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	mgr.mgrLog.Debug().Zid(curZid).Zid(newZid).Msg("RenameZettel")

	if mgr.State() != box.StartStateStarted {

		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for i, p := range mgr.boxes {
		err := p.RenameZettel(ctx, curZid, newZid)
		var errZNF box.ErrZettelNotFound
		if err != nil && !errors.As(err, &errZNF) {
			for j := 0; j < i; j++ {
				mgr.boxes[j].RenameZettel(ctx, newZid, curZid)
			}
			return err
		}
	}
	return nil
}

// CanDeleteZettel returns true, if box could possibly delete the given zettel.
func (mgr *Manager) CanDeleteZettel(ctx context.Context, zid id.Zid) bool {

	if mgr.State() != box.StartStateStarted {

		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel")

	if mgr.State() != box.StartStateStarted {

		return box.ErrStopped
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zid)
		if err == nil {
			return nil
		}
		var errZNF box.ErrZettelNotFound
		if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrZettelNotFound{Zid: zid}
}

Changes to box/manager/collect.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/strfun"
)

type collectData struct {
	refs  id.Set
	words store.WordSet
	urls  store.WordSet
	itags store.WordSet
}

func (data *collectData) initialize() {
	data.refs = id.NewSet()
	data.words = store.NewWordSet()
	data.urls = store.NewWordSet()
	data.itags = store.NewWordSet()
}

func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
	ast.Walk(data, &zn.Ast)
}

func collectInlineIndexData(is *ast.InlineSlice, data *collectData) {
	ast.Walk(data, is)
}

func (data *collectData) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		data.addText(string(n.Content))
	case *ast.TranscludeNode:
		data.addRef(n.Ref)
	case *ast.TextNode:
		data.addText(n.Text)
	case *ast.TagNode:
		data.addText(n.Tag)
		data.itags.Add("#" + strings.ToLower(n.Tag))
	case *ast.LinkNode:
		data.addRef(n.Ref)
	case *ast.EmbedRefNode:
		data.addRef(n.Ref)
	case *ast.CiteNode:
		data.addText(n.Key)
	case *ast.LiteralNode:

|















|
|






<






<


















<
<
<







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
//-----------------------------------------------------------------------------
// 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 manager

import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type collectData struct {
	refs  id.Set
	words store.WordSet
	urls  store.WordSet

}

func (data *collectData) initialize() {
	data.refs = id.NewSet()
	data.words = store.NewWordSet()
	data.urls = store.NewWordSet()

}

func collectZettelIndexData(zn *ast.ZettelNode, data *collectData) {
	ast.Walk(data, &zn.Ast)
}

func collectInlineIndexData(is *ast.InlineSlice, data *collectData) {
	ast.Walk(data, is)
}

func (data *collectData) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.VerbatimNode:
		data.addText(string(n.Content))
	case *ast.TranscludeNode:
		data.addRef(n.Ref)
	case *ast.TextNode:
		data.addText(n.Text)



	case *ast.LinkNode:
		data.addRef(n.Ref)
	case *ast.EmbedRefNode:
		data.addRef(n.Ref)
	case *ast.CiteNode:
		data.addText(n.Key)
	case *ast.LiteralNode:
77
78
79
80
81
82
83
84
85
86
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Zid(zid)
	}
}







|


72
73
74
75
76
77
78
79
80
81
	if ref.IsExternal() {
		data.urls.Add(strings.ToLower(ref.Value))
	}
	if !ref.IsZettel() {
		return
	}
	if zid, err := id.Parse(ref.URL.Path); err == nil {
		data.refs.Add(zid)
	}
}

Changes to box/manager/enrich.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"context"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// Enrich computes additional properties and updates the given metadata.
func (mgr *Manager) Enrich(ctx context.Context, m *meta.Meta, boxNumber int) {

	// 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)

	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.

|














|

|
|
















>
|
>







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
//-----------------------------------------------------------------------------
// 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 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.
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
	zid /= 100
	hours := zid % 100
	if hours > 23 {
		hours = 23
	}
	zid /= 100
	day := zid % 100









	if day < 1 {
		day = 1
	}
	zid /= 100
	month := zid % 100
	if month < 1 {
		month = 1
	}
	if month > 12 {
		month = 12
	}
	year := zid / 100
	switch month {
	case 1, 3, 5, 7, 8, 10, 12:
		if day > 31 {
			day = 32
		}
	case 4, 6, 9, 11:
		if day > 30 {
			day = 30
		}
	case 2:
		if year%4 != 0 || (year%100 == 0 && year%400 != 0) {
			if day > 28 {
				day = 28
			}
		} else {
			if day > 29 {
				day = 29
			}
		}
	}
	created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds
	return created.String()
}

func computePublished(m *meta.Meta) {
	if _, ok := m.Get(api.KeyPublished); ok {
		return
	}
	if modified, ok := m.Get(api.KeyModified); ok {







>
>
>
>
>
>
>
>
>



<
<






|



|
















<
|







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
	zid /= 100
	hours := zid % 100
	if hours > 23 {
		hours = 23
	}
	zid /= 100
	day := zid % 100
	zid /= 100
	month := zid % 100
	year := zid / 100
	month, day = sanitizeMonthDay(year, month, day)
	created := ((((year*100+month)*100+day)*100+hours)*100+minutes)*100 + seconds
	return created.String()
}

func sanitizeMonthDay(year, month, day id.Zid) (id.Zid, id.Zid) {
	if day < 1 {
		day = 1
	}


	if month < 1 {
		month = 1
	}
	if month > 12 {
		month = 12
	}

	switch month {
	case 1, 3, 5, 7, 8, 10, 12:
		if day > 31 {
			day = 31
		}
	case 4, 6, 9, 11:
		if day > 30 {
			day = 30
		}
	case 2:
		if year%4 != 0 || (year%100 == 0 && year%400 != 0) {
			if day > 28 {
				day = 28
			}
		} else {
			if day > 29 {
				day = 29
			}
		}
	}

	return month, day
}

func computePublished(m *meta.Meta) {
	if _, ok := m.Get(api.KeyPublished); ok {
		return
	}
	if modified, ok := m.Get(api.KeyModified); ok {

Changes to box/manager/indexer.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package manager

import (
	"context"
	"fmt"
	"net/url"
	"time"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"



)

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchEqual(word string) id.Set {
	found := mgr.idxStore.SearchEqual(word)
	mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual")

|


















<
<
<



>
>
>







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
//-----------------------------------------------------------------------------
// 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 manager

import (
	"context"
	"fmt"
	"net/url"
	"time"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"



	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchEqual(word string) id.Set {
	found := mgr.idxStore.SearchEqual(word)
	mgr.idxLog.Debug().Str("word", word).Int("found", int64(len(found))).Msg("SearchEqual")
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
}

// idxIndexer runs in the background and updates the index data structures.
// This is the main service of the idxIndexer.
func (mgr *Manager) idxIndexer() {
	// Something may panic. Ensure a running indexer.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("Indexer", r)
			go mgr.idxIndexer()
		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := box.NoEnrichContext(context.Background())







|
|







71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
}

// idxIndexer runs in the background and updates the index data structures.
// This is the main service of the idxIndexer.
func (mgr *Manager) idxIndexer() {
	// Something may panic. Ensure a running indexer.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("Indexer", ri)
			go mgr.idxIndexer()
		}
	}()

	timerDuration := 15 * time.Second
	timer := time.NewTimer(timerDuration)
	ctx := box.NoEnrichContext(context.Background())
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
			<-timer.C
		}
		return false
	}
	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel domain.Zettel) {
	var cData collectData
	cData.initialize()
	collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)

	m := zettel.Meta
	zi := store.NewZettelIndex(m.Zid)
	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) {







|





|







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
			<-timer.C
		}
		return false
	}
	return true
}

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
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
			}
		}
	}
}

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	for ref := range cData.refs {
		if _, err := mgr.GetMeta(ctx, ref); err == nil {
			zi.AddBackRef(ref)
		} else {
			zi.AddDeadRef(ref)
		}
	}
	zi.SetWords(cData.words)
	zi.SetUrls(cData.urls)
	zi.SetITags(cData.itags)
}

func (mgr *Manager) idxUpdateValue(ctx context.Context, inverseKey, value string, zi *store.ZettelIndex) {
	zid, err := id.Parse(value)
	if err != nil {
		return
	}
	if _, err = mgr.GetMeta(ctx, zid); err != nil {
		zi.AddDeadRef(zid)
		return
	}
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(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)
	}
}







|







<







|







|












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.

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-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package manager coordinates the various boxes and indexes of a Zettelstore.
package manager

import (
	"context"
	"io"
	"net/url"
	"sync"
	"time"

	"zettelstore.de/c/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/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"


)

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher

|


















|





<
<



>
>







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 manager coordinates the various boxes and indexes of a Zettelstore.
package manager

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"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher
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

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string { return maps.Keys(registry) }

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	mgrMx        sync.RWMutex
	started      bool

	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names

	// Indexer data
	idxLog   *logger.Logger
	idxStore store.Store
	idxAr    *anterooms
	idxReady chan struct{} // Signal a non-empty anteroom to background task

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
}














// New creates a new managing box.
func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := make(strfun.Set, len(descrs))
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Set(kd.Name)
		}
	}
	boxLog := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: memstore.New(),
		idxAr:    newAnterooms(10),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {







|
|
>











|








>
>
>
>
>
>
>
>
>
>
>
>
>



















|







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

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string { return maps.Keys(registry) }

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	stateMx      sync.RWMutex
	state        box.StartState
	mgrMx        sync.RWMutex
	rtConfig     config.Config
	boxes        []box.ManagedBox
	observers    []box.UpdateFunc
	mxObserver   sync.RWMutex
	done         chan struct{}
	infos        chan box.UpdateInfo
	propertyKeys strfun.Set // Set of property key names

	// Indexer data
	idxLog   *logger.Logger
	idxStore store.Store
	idxAr    *anteroomQueue
	idxReady chan struct{} // Signal a non-empty anteroom to background task

	// Indexer stats data
	idxMx          sync.RWMutex
	idxLastReload  time.Time
	idxDurReload   time.Duration
	idxSinceReload uint64
}

func (mgr *Manager) setState(newState box.StartState) {
	mgr.stateMx.Lock()
	mgr.state = newState
	mgr.stateMx.Unlock()
}

func (mgr *Manager) State() box.StartState {
	mgr.stateMx.RLock()
	state := mgr.state
	mgr.stateMx.RUnlock()
	return state
}

// New creates a new managing box.
func New(boxURIs []*url.URL, authManager auth.BaseManager, rtConfig config.Config) (*Manager, error) {
	descrs := meta.GetSortedKeyDescriptions()
	propertyKeys := make(strfun.Set, len(descrs))
	for _, kd := range descrs {
		if kd.IsProperty() {
			propertyKeys.Set(kd.Name)
		}
	}
	boxLog := kernel.Main.GetLogger(kernel.BoxService)
	mgr := &Manager{
		mgrLog:       boxLog.Clone().Str("box", "manager").Child(),
		rtConfig:     rtConfig,
		infos:        make(chan box.UpdateInfo, len(boxURIs)*10),
		propertyKeys: propertyKeys,

		idxLog:   boxLog.Clone().Str("box", "index").Child(),
		idxStore: memstore.New(),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}
	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
		mgr.mxObserver.Unlock()
	}
}

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if r := recover(); r != nil {
			kernel.Main.LogRecover("Notifier", r)
			go mgr.notifier()
		}
	}()

	tsLastEvent := time.Now()
	cache := destutterCache{}
	for {







|
|







179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
		mgr.mxObserver.Unlock()
	}
}

func (mgr *Manager) notifier() {
	// The call to notify may panic. Ensure a running notifier.
	defer func() {
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("Notifier", ri)
			go mgr.notifier()
		}
	}()

	tsLastEvent := time.Now()
	cache := destutterCache{}
	for {
191
192
193
194
195
196
197

198
199
200
201

202

203
204
205
206
207
208
209

				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}

				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
				}

				mgr.notifyObserver(&ci)

			}
		case <-mgr.done:
			return
		}
	}
}








>




>
|
>







205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226

				reason, zid := ci.Reason, ci.Zid
				mgr.mgrLog.Debug().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier")
				if ignoreUpdate(cache, now, reason, zid) {
					mgr.mgrLog.Trace().Uint("reason", uint64(reason)).Zid(zid).Msg("notifier ignored")
					continue
				}

				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
				}
				if mgr.State() == box.StartStateStarted {
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
}

224
225
226
227
228
229
230


231
232
233
234
235

236
237
238
239
240
241
242
		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {


	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	default:

		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}







>
>





>







241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Warn().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}
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
	}
}

// Start the box. Now all other functions of the box are allowed.
// Starting an already started box is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()
	if mgr.started {
		mgr.mgrMx.Unlock()

		return box.ErrStarted
	}

	for i := len(mgr.boxes) - 1; i >= 0; i-- {
		ssi, ok := mgr.boxes[i].(box.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}

		for j := i + 1; j < len(mgr.boxes); j++ {
			if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 {
				ssj.Stop(ctx)
			}
		}
		mgr.mgrMx.Unlock()
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()





	go mgr.idxIndexer()


	mgr.started = true














	mgr.mgrMx.Unlock()



	return nil
}




// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if !mgr.started {
		return
	}

	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.started = false
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if !mgr.started {
		return box.ErrStopped
	}


	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil







<
|
>


>









>





|





>
>
>
>
>

>
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
|
|
>
>
>





|


>






|





<
|
<


>
>







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
	}
}

// Start the box. Now all other functions of the box are allowed.
// Starting an already started box is not allowed.
func (mgr *Manager) Start(ctx context.Context) error {
	mgr.mgrMx.Lock()

	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStopped {
		return box.ErrStarted
	}
	mgr.setState(box.StartStateStarting)
	for i := len(mgr.boxes) - 1; i >= 0; i-- {
		ssi, ok := mgr.boxes[i].(box.StartStopper)
		if !ok {
			continue
		}
		err := ssi.Start(ctx)
		if err == nil {
			continue
		}
		mgr.setState(box.StartStateStopping)
		for j := i + 1; j < len(mgr.boxes); j++ {
			if ssj, ok2 := mgr.boxes[j].(box.StartStopper); ok2 {
				ssj.Stop(ctx)
			}
		}
		mgr.setState(box.StartStateStopped)
		return err
	}
	mgr.idxAr.Reset() // Ensure an initial index run
	mgr.done = make(chan struct{})
	go mgr.notifier()

	mgr.waitBoxesAreStarted()
	mgr.setState(box.StartStateStarted)
	mgr.notifyObserver(&box.UpdateInfo{Box: mgr, Reason: box.OnReady})

	go mgr.idxIndexer()
	return nil
}

func (mgr *Manager) waitBoxesAreStarted() {
	const waitTime = 10 * time.Millisecond
	const waitLoop = int(1 * time.Second / waitTime)
	for i := 1; !mgr.allBoxesStarted(); i++ {
		if i%waitLoop == 0 {
			if time.Duration(i)*waitTime > time.Minute {
				mgr.mgrLog.Warn().Msg("Waiting for more than one minute to start")
			} else {
				mgr.mgrLog.Trace().Msg("Wait for boxes to start")
			}
		}
		time.Sleep(waitTime)
	}
}

func (mgr *Manager) allBoxesStarted() bool {
	for _, bx := range mgr.boxes {
		if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted {
			return false
		}
	}
	return true
}

// Stop the started box. Now only the Start() function is allowed.
func (mgr *Manager) Stop(ctx context.Context) {
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	if mgr.State() != box.StartStateStarted {
		return
	}
	mgr.setState(box.StartStateStopping)
	close(mgr.done)
	for _, p := range mgr.boxes {
		if ss, ok := p.(box.StartStopper); ok {
			ss.Stop(ctx)
		}
	}
	mgr.setState(box.StartStateStopped)
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")

	if mgr.State() != box.StartStateStarted {

		return box.ErrStopped
	}
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil

Changes to box/manager/memstore/memstore.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
package memstore

import (
	"context"
	"fmt"
	"io"
	"sort"
	"strings"
	"sync"

	"zettelstore.de/c/api"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

type metaRefs struct {
	forward  id.Slice
	backward id.Slice
}

type zettelIndex struct {

	dead     id.Slice
	forward  id.Slice
	backward id.Slice
	meta     map[string]metaRefs
	words    []string
	urls     []string
	itags    string // Inline tags
}

func (zi *zettelIndex) isEmpty() bool {
	if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 {
		return false
	}
	return len(zi.meta) == 0
}

type stringRefs map[string]id.Slice

type memStore struct {
	mx    sync.RWMutex

	idx   map[id.Zid]*zettelIndex
	dead  map[id.Zid]id.Slice // map dead refs where they occur
	words stringRefs
	urls  stringRefs

	// Stats

	updates uint64
}

// New returns a new memory-based index store.
func New() store.Store {
	return &memStore{

		idx:   make(map[id.Zid]*zettelIndex),
		dead:  make(map[id.Zid]id.Slice),
		words: make(stringRefs),
		urls:  make(stringRefs),
	}










}

func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) {
	if ms.doEnrich(m) {
		ms.mx.Lock()
		ms.updates++
		ms.mx.Unlock()
	}
}

func (ms *memStore) doEnrich(m *meta.Meta) bool {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return false
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(api.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
		updated = true
	}
	for k, refs := range zi.meta {
		if len(refs.backward) > 0 {
			m.Set(k, refs.backward.String())
			back = remRefs(back, refs.backward)
			updated = true
		}
	}
	if len(back) > 0 {
		m.Set(api.KeyBack, back.String())
		updated = true
	}
	if itags := zi.itags; itags != "" {
		m.Set(api.KeyContentTags, itags)
		if tags, ok2 := m.Get(api.KeyTags); ok2 {
			m.Set(api.KeyAllTags, tags+" "+itags)
		} else {
			m.Set(api.KeyAllTags, itags)
		}
		updated = true
	} else if tags, ok2 := m.Get(api.KeyTags); ok2 {
		m.Set(api.KeyAllTags, tags)
		updated = true
	}
	return updated
}

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchEqual(word string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result.AddSlice(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result.AddSlice(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {

|



















|
>
|

|
|


|




|
>
|
|
|
|
|
|
<
<
<
<
<
<
<
<





|
>
|
|
|
|


>






>
|
|
|
|

>
>
>
>
>
>
>
>
>
>




|

|















|









|










<
<
<
<
<
<
<
<
<
<
<
<










|


|







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
//-----------------------------------------------------------------------------
// 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 memstore stored the index in main memory.
package memstore

import (
	"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 && zi.meta != nil {
		// zi.meta is nil, if zettel was referenced, but is not indexed yet.
		return zi.meta.Clone(), nil
	}
	return nil, box.ErrZettelNotFound{Zid: zid}
}

func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) {
	if ms.doEnrich(m) {
		ms.mxStats.Lock()
		ms.updates++
		ms.mxStats.Unlock()
	}
}

func (ms *memStore) doEnrich(m *meta.Meta) bool {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return false
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Clone())
	if len(zi.backward) > 0 {
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(api.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
		updated = true
	}
	for k, refs := range zi.otherRefs {
		if len(refs.backward) > 0 {
			m.Set(k, refs.backward.String())
			back = remRefs(back, refs.backward)
			updated = true
		}
	}
	if len(back) > 0 {
		m.Set(api.KeyBack, back.String())
		updated = true
	}












	return updated
}

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchEqual(word string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result.CopySlice(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result.CopySlice(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {
158
159
160
161
162
163
164
165
166
167
168




169
170
171

172
173
174
175
176
177
178
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
	minZid, err := id.Parse(prefix + "00000000000000"[:13-l] + "1")
	if err != nil {
		return result
	}




	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
	if err != nil {
		return result

	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			addBackwardZids(result, zid, zi)
		}
	}
	return result







|



>
>
>
>
|
|
|
>







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
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
	if err != nil {
		return result
	}
	var minZid id.Zid
	if l < 14 && prefix == "0000000000000"[:l] {
		minZid = id.Zid(1)
	} else {
		minZid, err = id.Parse(prefix + "00000000000000"[:14-l])
		if err != nil {
			return result
		}
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
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
func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set {
	// Must only be called if ms.mx is read-locked!
	result := id.NewSet()
	for word, refs := range ms.words {
		if !pred(word, s) {
			continue
		}
		result.AddSlice(refs)
	}
	for u, refs := range ms.urls {
		if !pred(u, s) {
			continue
		}
		result.AddSlice(refs)
	}
	return result
}

func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is read-locked!
	result.Zid(zid)
	result.AddSlice(zi.backward)
	for _, mref := range zi.meta {
		result.AddSlice(mref.backward)
	}
}

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:







|





|




|

|
|
|
|







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
func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set {
	// Must only be called if ms.mx is read-locked!
	result := id.NewSet()
	for word, refs := range ms.words {
		if !pred(word, s) {
			continue
		}
		result.CopySlice(refs)
	}
	for u, refs := range ms.urls {
		if !pred(u, s) {
			continue
		}
		result.CopySlice(refs)
	}
	return result
}

func addBackwardZids(result id.Set, zid id.Zid, zi *zettelData) {
	// Must only be called if ms.mx is read-locked!
	result.Add(zid)
	result.CopySlice(zi.backward)
	for _, mref := range zi.otherRefs {
		result.CopySlice(mref.backward)
	}
}

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:
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
			}
		}
	}
	return back
}

func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set {

	ms.mx.Lock()
	defer ms.mx.Unlock()
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelIndex{}
		ziExist = false
	}

	// Is this zettel an old dead reference mentioned in other zettel?
	var toCheck id.Set
	if refs, ok := ms.dead[zidx.Zid]; ok {
		// These must be checked later again
		toCheck = id.NewSet(refs...)
		delete(ms.dead, zidx.Zid)
	}


	ms.updateDeadReferences(zidx, zi)
	ms.updateForwardBackwardReferences(zidx, zi)

	ms.updateMetadataReferences(zidx, zi)

	zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords())
	zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls())
	zi.itags = setITags(zidx.GetITags())

	// Check if zi must be inserted into ms.idx
	if !ziExist && !zi.isEmpty() {
		ms.idx[zidx.Zid] = zi
	}

	return toCheck
}






































func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	drefs := zidx.GetDeadRefs()
	newRefs, remRefs := refsDiff(drefs, zi.dead)
	zi.dead = drefs
	for _, ref := range remRefs {
		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
	}
	for _, ref := range newRefs {
		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
	}
}

func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	brefs := zidx.GetBackRefs()
	newRefs, remRefs := refsDiff(brefs, zi.forward)
	zi.forward = brefs


	for _, ref := range remRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)


	}

	for _, ref := range newRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)


	}
}



func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	metarefs := zidx.GetMetaRefs()
	for key, mr := range zi.meta {
		if _, ok := metarefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.meta == nil {
		zi.meta = make(map[string]metaRefs)
	}

	for key, mrefs := range metarefs {
		mr := zi.meta[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.meta[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getEntry(ref)
			if bzi.meta == nil {
				bzi.meta = make(map[string]metaRefs)
			}
			bmr := bzi.meta[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.meta[key] = bmr



		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}

}

func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	// Must only be called if ms.mx is write-locked!
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}







>




|











>

|
>
|
>


<


|






>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|












|




>
>

|

>
>
|
>

|

>
>
|
|
>
|
>
|

|
|
|




|
|

>
|
|


|


|
|
|

|

|
>
>
>



>



<







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
			}
		}
	}
	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)
	ids := ms.updateForwardBackwardReferences(zidx, zi)
	toCheck = toCheck.Copy(ids)
	ids = ms.updateMetadataReferences(zidx, zi)
	toCheck = toCheck.Copy(ids)
	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) id.Set {
	// Must only be called if ms.mx is write-locked!
	brefs := zidx.GetBackRefs()
	newRefs, remRefs := refsDiff(brefs, zi.forward)
	zi.forward = brefs

	var toCheck id.Set
	for _, ref := range remRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	for _, ref := range newRefs {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	}
	return toCheck
}

func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelData) id.Set {
	// Must only be called if ms.mx is write-locked!
	inverseRefs := zidx.GetInverseRefs()
	for key, mr := range zi.otherRefs {
		if _, ok := inverseRefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.otherRefs == nil {
		zi.otherRefs = make(map[string]bidiRefs)
	}
	var toCheck id.Set
	for key, mrefs := range inverseRefs {
		mr := zi.otherRefs[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getOrCreateEntry(ref)
			if bzi.otherRefs == nil {
				bzi.otherRefs = make(map[string]bidiRefs)
			}
			bmr := bzi.otherRefs[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.otherRefs[key] = bmr
			if bzi.meta == nil {
				toCheck = toCheck.Add(ref)
			}
		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}
	return toCheck
}

func 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}
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
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
477
478
479
480
481
482
483
484
485
486
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

func setITags(next store.WordSet) string {
	itags := next.Words()
	if len(itags) == 0 {
		return ""
	}
	sort.Strings(itags)
	return strings.Join(itags, " ")
}

func (ms *memStore) getEntry(zid id.Zid) *zettelIndex {
	// Must only be called if ms.mx is write-locked!
	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelIndex{}
	ms.idx[zid] = zi
	return zi
}

func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	zi, ok := ms.idx[zid]
	if !ok {
		return nil
	}

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)
	if len(zi.meta) > 0 {
		for key, mrefs := range zi.meta {
			ms.removeInverseMeta(zid, key, mrefs.forward)
		}
	}
	ms.deleteWords(zid, zi.words)
	delete(ms.idx, zid)
	return toCheck
}

func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range zi.dead {
		if drefs, ok := ms.dead[ref]; ok {
			drefs = remRef(drefs, zid)
			if len(drefs) > 0 {
				ms.dead[ref] = drefs
			} else {
				delete(ms.dead, ref)
			}
		}
	}
}

func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set {
	// Must only be called if ms.mx is write-locked!
	var toCheck id.Set
	for _, ref := range zi.forward {
		if fzi, ok := ms.idx[ref]; ok {
			fzi.backward = remRef(fzi.backward, zid)
		}
	}
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)
			if toCheck == nil {
				toCheck = id.NewSet()
			}
			toCheck.Zid(ref)
		}
	}
	return toCheck
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range forward {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.meta == nil {
			continue
		}
		bmr, ok := bzi.meta[key]
		if !ok {
			continue
		}
		bmr.backward = remRef(bmr.backward, zid)
		if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
			bzi.meta[key] = bmr
		} else {
			delete(bzi.meta, key)
			if len(bzi.meta) == 0 {
				bzi.meta = nil
			}
		}
	}
}

func (ms *memStore) deleteWords(zid id.Zid, words []string) {
	// Must only be called if ms.mx is write-locked!







<
<
<
<
<
<
<
<
<
|




|















|
|








|













|













|









|


|





|

|
|
|







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
477
478
479
480
481
482
483
484
485
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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}










func (ms *memStore) getOrCreateEntry(zid id.Zid) *zettelData {
	// Must only be called if ms.mx is write-locked!
	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelData{}
	ms.idx[zid] = zi
	return zi
}

func (ms *memStore) 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)
		}
	}
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)
			if toCheck == nil {
				toCheck = id.NewSet()
			}
			toCheck.Add(ref)
		}
	}
	return toCheck
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range forward {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.otherRefs == nil {
			continue
		}
		bmr, ok := bzi.otherRefs[key]
		if !ok {
			continue
		}
		bmr.backward = remRef(bmr.backward, zid)
		if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
			bzi.otherRefs[key] = bmr
		} else {
			delete(bzi.otherRefs, key)
			if len(bzi.otherRefs) == 0 {
				bzi.otherRefs = nil
			}
		}
	}
}

func (ms *memStore) deleteWords(zid id.Zid, words []string) {
	// Must only be called if ms.mx is write-locked!
497
498
499
500
501
502
503
504
505
506
507



508
509
510
511
512
513
514
		ms.words[word] = refs2
	}
}

func (ms *memStore) ReadStats(st *store.Stats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Updates = ms.updates
	st.Words = uint64(len(ms.words))
	st.Urls = uint64(len(ms.urls))
	ms.mx.RUnlock()



}

func (ms *memStore) Dump(w io.Writer) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()

	io.WriteString(w, "=== Dump\n")







<



>
>
>







541
542
543
544
545
546
547

548
549
550
551
552
553
554
555
556
557
558
559
560
		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")
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
		fmt.Fprintln(w, "=====", id)
		zi := ms.idx[id]
		if len(zi.dead) > 0 {
			fmt.Fprintln(w, "* Dead:", zi.dead)
		}
		dumpZids(w, "* Forward:", zi.forward)
		dumpZids(w, "* Backward:", zi.backward)
		for k, fb := range zi.meta {
			fmt.Fprintln(w, "* Meta", k)
			dumpZids(w, "** Forward:", fb.forward)
			dumpZids(w, "** Backward:", fb.backward)
		}
		dumpStrings(w, "* Words", "", "", zi.words)
		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
	}







|







578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
		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/memstore/refs.go.

1
2
3
4
5
6
7
8
9
10
11
12



13

14
15
16
17
18
19
20
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package memstore




import "zettelstore.de/z/domain/id"


func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++

|

|








>
>
>
|
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//-----------------------------------------------------------------------------
// 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 memstore

import (
	"slices"

	"zettelstore.de/z/zettel/id"
)

func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	refs = append(refs, id.Invalid)
	copy(refs[hi+1:], refs[hi:])
	refs[hi] = ref
	return refs
}

func remRefs(refs, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}







|
<
<







50
51
52
53
54
55
56
57


58
59
60
61
62
63
64
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	refs = slices.Insert(refs, hi, ref)


	return refs
}

func remRefs(refs, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}

Changes to box/manager/memstore/refs_test.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package memstore

import (
	"testing"

	"zettelstore.de/z/domain/id"
)

func assertRefs(t *testing.T, i int, got, exp id.Slice) {
	t.Helper()
	if got == nil && exp != nil {
		t.Errorf("%d: got nil, but expected %v", i, exp)
		return

|

|











|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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 memstore

import (
	"testing"

	"zettelstore.de/z/zettel/id"
)

func assertRefs(t *testing.T, i int, got, exp id.Slice) {
	t.Helper()
	if got == nil && exp != nil {
		t.Errorf("%d: got nil, but expected %v", i, exp)
		return

Changes to box/manager/store/store.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
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package store contains general index data for storing a zettel index.
package store

import (
	"context"
	"io"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
)

// Stats records statistics about the store.
type Stats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int


|

|













|
|
|







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
//-----------------------------------------------------------------------------
// 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 contains general index data for storing a zettel index.
package store

import (
	"context"
	"io"

	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Stats records statistics about the store.
type Stats struct {
	// Zettel is the number of zettel managed by the indexer.
	Zettel int

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/wordset.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package store

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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

Changes to box/manager/store/wordset_test.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package store_test

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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_test

Changes to box/manager/store/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
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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package store


import "zettelstore.de/z/domain/id"



// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid      id.Zid            // zid of the indexed zettel

	backrefs id.Set            // set of back references
	metarefs map[string]id.Set // references to inverse keys
	deadrefs id.Set            // set of dead references
	words    WordSet
	urls     WordSet
	itags    WordSet
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(zid id.Zid) *ZettelIndex {
	return &ZettelIndex{
		Zid:      zid,

		backrefs: id.NewSet(),
		metarefs: make(map[string]id.Set),
		deadrefs: id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
	zi.backrefs.Zid(zid)
}

// AddMetaRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
	if zids, ok := zi.metarefs[key]; ok {
		zids.Zid(zid)
		return
	}
	zi.metarefs[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 }

// SetITags sets the words to the given value.
func (zi *ZettelIndex) SetITags(itags WordSet) { zi.itags = itags }

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() id.Slice {
	return zi.deadrefs.Sorted()
}



// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() id.Slice {
	return zi.backrefs.Sorted()
}

// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice {
	if len(zi.metarefs) == 0 {
		return nil
	}
	result := make(map[string]id.Slice, len(zi.metarefs))
	for key, refs := range zi.metarefs {
		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 }

// GetITags returns a reference to the set of internal tags. It must not be modified.
func (zi *ZettelIndex) GetITags() WordSet { return zi.itags }

|










>
|
>
>



|
>
|
|
|
|
|
<



|

|
>
|
|
|






|


|

|
|
|


|




|








<
<
<

|
<
|
>
>


|
<
|
<
|
|
|


|
|










<
<
<
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.Add(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.Add(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.Add(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.

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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package membox stores zettel volatile in main memory.
package membox

import (
	"context"
	"net/url"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"


)

func init() {
	manager.Register(
		"mem",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return &memBox{

|


















<
<
<



>
>







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
//-----------------------------------------------------------------------------
// 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 membox stores zettel volatile in main memory.
package membox

import (
	"context"
	"net/url"
	"sync"

	"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{
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
type memBox struct {
	log       *logger.Logger
	u         *url.URL
	cdata     manager.ConnectData
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]domain.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(reason box.UpdateReason, zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
	}
}

func (mb *memBox) Location() string {
	return mb.u.String()
}










func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]domain.Zettel)
	mb.curBytes = 0
	mb.mx.Unlock()
	mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box")
	return nil
}

func (mb *memBox) Stop(context.Context) {
	mb.mx.Lock()
	mb.zettel = nil
	mb.mx.Unlock()
}

func (mb *memBox) CanCreateZettel(context.Context) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	return len(mb.zettel) < mb.maxZettel
}

func (mb *memBox) CreateZettel(_ context.Context, zettel domain.Zettel) (id.Zid, error) {
	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return id.Invalid, box.ErrCapacity
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mb.zettel[zid]
		return !ok, nil
	})
	if err != nil {
		mb.mx.Unlock()
		return id.Invalid, err
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(box.OnZettel, zid)
	mb.log.Trace().Zid(zid).Msg("CreateZettel")
	return zid, nil
}

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) {
	mb.mx.RLock()
	zettel, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return domain.Zettel{}, box.ErrNotFound
	}
	zettel.Meta = zettel.Meta.Clone()
	mb.log.Trace().Msg("GetZettel")
	return zettel, nil
}

func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	mb.mx.RLock()
	zettel, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return nil, box.ErrNotFound
	}
	mb.log.Trace().Msg("GetMeta")
	return zettel.Meta.Clone(), nil
}

func (mb *memBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	mb.log.Trace().Int("entries", int64(len(mb.zettel))).Msg("ApplyZid")
	for zid := range mb.zettel {







|



|

|






>
>
>
>
>
>
>
>
>



|


















|




















|




|

|


|

|

|


|

|

<
|
<
<
<







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
type memBox struct {
	log       *logger.Logger
	u         *url.URL
	cdata     manager.ConnectData
	maxZettel int
	maxBytes  int
	mx        sync.RWMutex // Protects the following fields
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Box: mb, Reason: box.OnZettel, Zid: zid}
	}
}

func (mb *memBox) Location() string {
	return mb.u.String()
}

func (mb *memBox) State() box.StartState {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	if mb.zettel == nil {
		return box.StartStateStopped
	}
	return box.StartStateStarted
}

func (mb *memBox) Start(context.Context) error {
	mb.mx.Lock()
	mb.zettel = make(map[id.Zid]zettel.Zettel)
	mb.curBytes = 0
	mb.mx.Unlock()
	mb.log.Trace().Int("max-zettel", int64(mb.maxZettel)).Int("max-bytes", int64(mb.maxBytes)).Msg("Start Box")
	return nil
}

func (mb *memBox) Stop(context.Context) {
	mb.mx.Lock()
	mb.zettel = nil
	mb.mx.Unlock()
}

func (mb *memBox) CanCreateZettel(context.Context) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	return len(mb.zettel) < mb.maxZettel
}

func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) {
	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return id.Invalid, box.ErrCapacity
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mb.zettel[zid]
		return !ok, nil
	})
	if err != nil {
		mb.mx.Unlock()
		return id.Invalid, err
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Zid(zid).Msg("CreateZettel")
	return zid, nil
}

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	z, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	z.Meta = z.Meta.Clone()
	mb.log.Trace().Msg("GetZettel")
	return z, nil
}

func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, found := mb.zettel[zid]
	mb.mx.RUnlock()

	return found



}

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 {
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
			mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number)
			handle(m)
		}
	}
	return nil
}

func (mb *memBox) CanUpdateZettel(_ context.Context, zettel domain.Zettel) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	zid := zettel.Meta.Zid
	if !zid.IsValid() {
		return false
	}

	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[zid]; found {
		newBytes -= prevZettel.Length()
	}
	return newBytes < mb.maxBytes
}

func (mb *memBox) UpdateZettel(_ context.Context, zettel domain.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return &box.ErrInvalidID{Zid: m.Zid}
	}

	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.Length()
	}
	if mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(box.OnZettel, m.Zid)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}

func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true }

func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error {
	mb.mx.Lock()
	zettel, ok := mb.zettel[curZid]
	if !ok {
		mb.mx.Unlock()
		return box.ErrNotFound
	}

	// Check that there is no zettel with newZid
	if _, ok = mb.zettel[newZid]; ok {
		mb.mx.Unlock()
		return &box.ErrInvalidID{Zid: newZid}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mb.zettel[newZid] = zettel
	delete(mb.zettel, curZid)
	mb.mx.Unlock()
	mb.notifyChanged(box.OnZettel, curZid)
	mb.notifyChanged(box.OnZettel, newZid)
	mb.log.Trace().Msg("RenameZettel")
	return nil
}

func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	return ok
}

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrNotFound
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(box.OnZettel, zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}







|














|


|
















|











|





|








|
|
















|




|











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
			mb.cdata.Enricher.Enrich(ctx, m, mb.cdata.Number)
			handle(m)
		}
	}
	return nil
}

func (mb *memBox) CanUpdateZettel(_ context.Context, zettel zettel.Zettel) bool {
	mb.mx.RLock()
	defer mb.mx.RUnlock()
	zid := zettel.Meta.Zid
	if !zid.IsValid() {
		return false
	}

	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[zid]; found {
		newBytes -= prevZettel.Length()
	}
	return newBytes < mb.maxBytes
}

func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return box.ErrInvalidZid{Zid: m.Zid.String()}
	}

	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.Length()
	}
	if mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid)
	mb.log.Trace().Msg("UpdateZettel")
	return nil
}

func (*memBox) AllowRenameZettel(context.Context, id.Zid) bool { return true }

func (mb *memBox) RenameZettel(_ context.Context, curZid, newZid id.Zid) error {
	mb.mx.Lock()
	zettel, ok := mb.zettel[curZid]
	if !ok {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: curZid}
	}

	// Check that there is no zettel with newZid
	if _, ok = mb.zettel[newZid]; ok {
		mb.mx.Unlock()
		return box.ErrInvalidZid{Zid: newZid.String()}
	}

	meta := zettel.Meta.Clone()
	meta.Zid = newZid
	zettel.Meta = meta
	mb.zettel[newZid] = zettel
	delete(mb.zettel, curZid)
	mb.mx.Unlock()
	mb.notifyChanged(curZid)
	mb.notifyChanged(newZid)
	mb.log.Trace().Msg("RenameZettel")
	return nil
}

func (mb *memBox) CanDeleteZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	_, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	return ok
}

func (mb *memBox) DeleteZettel(_ context.Context, zid id.Zid) error {
	mb.mx.Lock()
	oldZettel, found := mb.zettel[zid]
	if !found {
		mb.mx.Unlock()
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	mb.log.Trace().Msg("DeleteZettel")
	return nil
}

func (mb *memBox) ReadStats(st *box.ManagedBoxStats) {
	st.ReadOnly = false
	mb.mx.RLock()
	st.Zettel = len(mb.zettel)
	mb.mx.RUnlock()
	mb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

Changes to box/notify/directory.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package notify

import (
	"errors"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"

)

type entrySet map[id.Zid]*DirEntry

// directoryState signal the internal state of the service.
//
// The following state transitions are possible:
// --newDirService--> dsCreated
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type directoryState uint8

const (
	dsCreated  directoryState = iota
	dsStarting                // Reading inital scan
	dsWorking                 // Initial scan complete, fully operational
	dsMissing                 // Directory is missing
	dsStopping                // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {

	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	mx       sync.RWMutex // protects status, entries
	state    directoryState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{

		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    dsCreated,
	}
}









// Start the directory service.
func (ds *DirService) Start() {
	ds.mx.Lock()
	ds.state = dsStarting
	ds.mx.Unlock()

	go ds.updateEvents()
}

// Refresh the directory entries.
func (ds *DirService) Refresh() {
	ds.notifier.Refresh()
}

// Stop the directory service.
func (ds *DirService) Stop() {
	ds.mx.Lock()
	ds.state = dsStopping
	ds.mx.Unlock()
	ds.notifier.Close()
}

func (ds *DirService) logMissingEntry(action string) error {
	err := ErrNoDirectory
	ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information")

|



















|




>




|








|


|
|
|
|
|




>





|







|

>



|


>
>
>
>
>
>
>
>




|

>
|










|







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
//-----------------------------------------------------------------------------
// 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 notify

import (
	"errors"
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"sync"

	"zettelstore.de/z/box"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type entrySet map[id.Zid]*DirEntry

// DirServiceState signal the internal state of the service.
//
// The following state transitions are possible:
// --newDirService--> dsCreated
// dsCreated --Start--> dsStarting
// dsStarting --last list notification--> dsWorking
// dsWorking --directory missing--> dsMissing
// dsMissing --last list notification--> dsWorking
// --Stop--> dsStopping
type DirServiceState uint8

const (
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	mx       sync.RWMutex // protects status, entries
	state    DirServiceState
	entries  entrySet
}

// ErrNoDirectory signals missing directory data.
var ErrNoDirectory = errors.New("unable to retrieve zettel directory information")

// NewDirService creates a new directory service.
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    DsCreated,
	}
}

// State the current service state.
func (ds *DirService) State() DirServiceState {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()
	return state
}

// Start the directory service.
func (ds *DirService) Start() {
	ds.mx.Lock()
	ds.state = DsStarting
	ds.mx.Unlock()
	var newEntries entrySet
	go ds.updateEvents(newEntries)
}

// Refresh the directory entries.
func (ds *DirService) Refresh() {
	ds.notifier.Refresh()
}

// Stop the directory service.
func (ds *DirService) Stop() {
	ds.mx.Lock()
	ds.state = DsStopping
	ds.mx.Unlock()
	ds.notifier.Close()
}

func (ds *DirService) logMissingEntry(action string) error {
	err := ErrNoDirectory
	ds.log.Info().Err(err).Str("action", action).Msg("Unable to get directory information")
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return DirEntry{}, ds.logMissingEntry("rename")
	}
	if _, found := ds.entries[newZid]; found {
		return DirEntry{}, &box.ErrInvalidID{Zid: newZid}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,







|







187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
func (ds *DirService) RenameDirEntry(oldEntry *DirEntry, newZid id.Zid) (DirEntry, error) {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	if ds.entries == nil {
		return DirEntry{}, ds.logMissingEntry("rename")
	}
	if _, found := ds.entries[newZid]; found {
		return DirEntry{}, box.ErrInvalidZid{Zid: newZid.String()}
	}
	oldZid := oldEntry.Zid
	newEntry := DirEntry{
		Zid:         newZid,
		MetaName:    renameFilename(oldEntry.MetaName, oldZid, newZid),
		ContentName: renameFilename(oldEntry.ContentName, oldZid, newZid),
		ContentExt:  oldEntry.ContentExt,
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
	if ds.entries == nil {
		return ds.logMissingEntry("delete")
	}
	delete(ds.entries, zid)
	return nil
}

func (ds *DirService) updateEvents() {
	var newEntries entrySet
	for ev := range ds.notifier.Events() {
		ds.mx.RLock()
		state := ds.state

		ds.mx.RUnlock()

		if msg := ds.log.Trace(); msg.Enabled() {
			msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent")
		}


		if state == dsStopping {
			break
		}















		switch ev.Op {
		case Error:
			newEntries = nil
			if state != dsMissing {
				ds.log.Warn().Err(ev.Err).Msg("Notifier confused")
			}
		case Make:
			newEntries = make(entrySet)
		case List:
			if ev.Name == "" {
				zids := getNewZids(newEntries)
				ds.mx.Lock()
				fromMissing := ds.state == dsMissing
				prevEntries := ds.entries
				ds.entries = newEntries
				ds.state = dsWorking
				ds.mx.Unlock()
				newEntries = nil
				ds.onCreateDirectory(zids, prevEntries)
				if fromMissing {
					ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found")
				}


			} else if newEntries != nil {
				ds.onUpdateFileEvent(newEntries, ev.Name)
			}
		case Destroy:
			newEntries = nil
			ds.onDestroyDirectory()
			ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")

		case Update:
			ds.mx.Lock()
			zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
			ds.mx.Unlock()
			if zid != id.Invalid {
				ds.notifyChange(box.OnZettel, zid)
			}
		case Delete:
			ds.mx.Lock()
			zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
			ds.mx.Unlock()
			if zid != id.Invalid {
				ds.notifyChange(box.OnZettel, zid)
			}
		default:
			ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
		}
	}

}

func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(box.OnZettel, zid)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
	for zid := range prevEntries {
		ds.notifyChange(box.OnZettel, zid)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = dsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(box.OnZettel, zid)
	}
}

var validFileName = regexp.MustCompile(`^(\d{14})`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)







|
|
|
<
|
>
|
|
|
<
|
>
>
|


>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
|
|
|
|
>
>
|
|
|
|
<
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<
>












|






|







|


|







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
	if ds.entries == nil {
		return ds.logMissingEntry("delete")
	}
	delete(ds.entries, zid)
	return nil
}

func (ds *DirService) updateEvents(newEntries entrySet) {
	// Something may panic. Ensure a running service.
	defer func() {

		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("DirectoryService", ri)
			go ds.updateEvents(newEntries)
		}
	}()


	for ev := range ds.notifier.Events() {
		e, ok := ds.handleEvent(ev, newEntries)
		if !ok {
			break
		}
		newEntries = e
	}
}
func (ds *DirService) handleEvent(ev Event, newEntries entrySet) (entrySet, bool) {
	ds.mx.RLock()
	state := ds.state
	ds.mx.RUnlock()

	if msg := ds.log.Trace(); msg.Enabled() {
		msg.Uint("state", uint64(state)).Str("op", ev.Op.String()).Str("name", ev.Name).Msg("notifyEvent")
	}
	if state == DsStopping {
		return nil, false
	}

	switch ev.Op {
	case Error:
		newEntries = nil
		if state != DsMissing {
			ds.log.Warn().Err(ev.Err).Msg("Notifier confused")
		}
	case Make:
		newEntries = make(entrySet)
	case List:
		if ev.Name == "" {
			zids := getNewZids(newEntries)
			ds.mx.Lock()
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = DsWorking
			ds.mx.Unlock()

			ds.onCreateDirectory(zids, prevEntries)
			if fromMissing {
				ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found")
			}
			return nil, true
		}
		if newEntries != nil {
			ds.onUpdateFileEvent(newEntries, ev.Name)
		}
	case Destroy:

		ds.onDestroyDirectory()
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid)
		}
	default:
		ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
	}

	return newEntries, true
}

func getNewZids(entries entrySet) id.Slice {
	zids := make(id.Slice, 0, len(entries))
	for zid := range entries {
		zids = append(zids, zid)
	}
	return zids
}

func (ds *DirService) onCreateDirectory(zids id.Slice, prevEntries entrySet) {
	for _, zid := range zids {
		ds.notifyChange(zid)
		delete(prevEntries, zid)
	}

	// These were previously stored, by are not found now.
	// Notify system that these were deleted, e.g. for updating the index.
	for zid := range prevEntries {
		ds.notifyChange(zid)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid)
	}
}

var validFileName = regexp.MustCompile(`^(\d{14})`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
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
			return false
		}
		if newExt == "zmk" {
			return true
		}
		oldInfo := parser.Get(oldExt)
		newInfo := parser.Get(newExt)
		if oldTextParser := oldInfo.IsTextParser; oldTextParser != newInfo.IsTextParser {



			return !oldTextParser
		}
		if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat {
			return oldImageFormat
		}
		if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) {
			return !oldPrimary
		}
	}

	oldLen := len(oldExt)
	newLen := len(newExt)
	if oldLen != newLen {
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(reason box.UpdateReason, zid id.Zid) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange")
		chci <- box.UpdateInfo{Reason: reason, Zid: zid}
	}
}







|
>
>
>
|

















|

|
|


576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
			return false
		}
		if newExt == "zmk" {
			return true
		}
		oldInfo := parser.Get(oldExt)
		newInfo := parser.Get(newExt)
		if oldASTParser := oldInfo.IsASTParser; oldASTParser != newInfo.IsASTParser {
			return !oldASTParser
		}
		if oldTextFormat := oldInfo.IsTextFormat; oldTextFormat != newInfo.IsTextFormat {
			return !oldTextFormat
		}
		if oldImageFormat := oldInfo.IsImageFormat; oldImageFormat != newInfo.IsImageFormat {
			return oldImageFormat
		}
		if oldPrimary := primarySyntax.Has(oldExt); oldPrimary != primarySyntax.Has(newExt) {
			return !oldPrimary
		}
	}

	oldLen := len(oldExt)
	newLen := len(newExt)
	if oldLen != newLen {
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Msg("notifyChange")
		chci <- box.UpdateInfo{Box: ds.box, Reason: box.OnZettel, Zid: zid}
	}
}

Changes to box/notify/directory_test.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
//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package notify

import (
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.

	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/pikchr"     // Allow to use pikchr parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.


)

func TestSeekZid(t *testing.T) {
	testcases := []struct {
		name string
		zid  id.Zid
	}{

|













<
<

>


<


>
>







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
//-----------------------------------------------------------------------------
// 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 notify

import (
	"testing"



	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.

	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestSeekZid(t *testing.T) {
	testcases := []struct {
		name string
		zid  id.Zid
	}{
45
46
47
48
49
50
51
52
53

54
55




56
57
58
59
60
61
62
63
		}
	}
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		api.ValueSyntaxZmk, "pikchr", "markdown", "md",
		// Other supported text formats

		"css", "txt", api.ValueSyntaxHTML, api.ValueSyntaxNone, "mustache", api.ValueSyntaxText, "plain",
		// Supported graphics formats




		api.ValueSyntaxGif, "png", api.ValueSyntaxSVG, "jpeg", "jpg",
		// Unsupported syntax values
		"gz", "cpp", "tar", "cppc",
	}
	for oldI, oldExt := range extVals {
		for newI, newExt := range extVals {
			if oldI <= newI {
				continue







|

>
|
|
>
>
>
>
|







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
		}
	}
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD,
		// Other supported text formats
		meta.SyntaxCSS, meta.SyntaxSxn, meta.SyntaxTxt, meta.SyntaxHTML,
		meta.SyntaxText, meta.SyntaxPlain,
		// Supported text graphics formats
		meta.SyntaxSVG,
		meta.SyntaxNone,
		// Supported binary graphic formats
		meta.SyntaxGif, meta.SyntaxPNG, meta.SyntaxJPEG, meta.SyntaxWebp, meta.SyntaxJPG,

		// Unsupported syntax values
		"gz", "cpp", "tar", "cppc",
	}
	for oldI, oldExt := range extVals {
		for newI, newExt := range extVals {
			if oldI <= newI {
				continue

Changes to box/notify/entry.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package notify

import (
	"path/filepath"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
)

const (
	extZettel = "zettel" // file contains metadata and content
	extBin    = "bin"    // file contains binary content
	extTxt    = "txt"    // file contains non-binary content
)

|













|
|
|
|
|







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
//-----------------------------------------------------------------------------
// 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 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 (
	extZettel = "zettel" // file contains metadata and content
	extBin    = "bin"    // file contains binary content
	extTxt    = "txt"    // file contains non-binary content
)
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

// HasMetaInContent returns true, if metadata will be stored in the content file.
func (e *DirEntry) HasMetaInContent() bool {
	return e.IsValid() && extIsMetaAndContent(e.ContentExt)
}

// SetupFromMetaContent fills entry data based on metadata and zettel content.
func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content domain.Content, getZettelFileSyntax func() []string) {
	if e.Zid != m.Zid {
		panic("Zid differ")
	}
	if contentName := e.ContentName; contentName != "" {
		if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" {
			e.MetaName = e.calcBaseName(contentName)
		}







|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

// HasMetaInContent returns true, if metadata will be stored in the content file.
func (e *DirEntry) HasMetaInContent() bool {
	return e.IsValid() && extIsMetaAndContent(e.ContentExt)
}

// SetupFromMetaContent fills entry data based on metadata and zettel content.
func (e *DirEntry) SetupFromMetaContent(m *meta.Meta, content zettel.Content, getZettelFileSyntax func() []string) {
	if e.Zid != m.Zid {
		panic("Zid differ")
	}
	if contentName := e.ContentName; contentName != "" {
		if !extIsMetaAndContent(e.ContentExt) && e.MetaName == "" {
			e.MetaName = e.calcBaseName(contentName)
		}
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
		}
		if metaName == "" {
			e.MetaName = e.calcBaseName(e.ContentName)
		}
	}
}

func contentExtWithMeta(syntax string, content domain.Content) string {
	p := parser.Get(syntax)
	if content.IsBinary() {
		if p.IsImageFormat {
			return syntax
		}
		return extBin
	}
	if p.IsImageFormat {
		return extTxt
	}
	return syntax
}

func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string {
	if yamlSep {
		return extZettel
	}
	switch syntax {
	case api.ValueSyntaxNone, api.ValueSyntaxZmk:
		return extZettel
	}
	for _, s := range getZettelFileSyntax() {
		if s == syntax {
			return extZettel
		}
	}







|


















|







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
		}
		if metaName == "" {
			e.MetaName = e.calcBaseName(e.ContentName)
		}
	}
}

func contentExtWithMeta(syntax string, content zettel.Content) string {
	p := parser.Get(syntax)
	if content.IsBinary() {
		if p.IsImageFormat {
			return syntax
		}
		return extBin
	}
	if p.IsImageFormat {
		return extTxt
	}
	return syntax
}

func calcContentExt(syntax string, yamlSep bool, getZettelFileSyntax func() []string) string {
	if yamlSep {
		return extZettel
	}
	switch syntax {
	case meta.SyntaxNone, meta.SyntaxZmk:
		return extZettel
	}
	for _, s := range getZettelFileSyntax() {
		if s == syntax {
			return extZettel
		}
	}

Changes to box/notify/fsdir.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------
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
func (fsdn *fsdirNotifier) eventLoop() {
	defer fsdn.base.Close()
	defer close(fsdn.events)
	defer close(fsdn.refresh)
	if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) {
		return
	}

	for fsdn.readAndProcessEvent() {
	}
}

func (fsdn *fsdirNotifier) readAndProcessEvent() bool {
	select {
	case <-fsdn.done:

		return false
	default:
	}
	select {
	case <-fsdn.done:

		return false
	case <-fsdn.refresh:

		listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	case err, ok := <-fsdn.base.Errors:

		if !ok {
			return false
		}
		select {
		case fsdn.events <- Event{Op: Error, Err: err}:
		case <-fsdn.done:

			return false
		}
	case ev, ok := <-fsdn.base.Events:

		if !ok {
			return false
		}
		if !fsdn.processEvent(&ev) {
			return false
		}
	}
	return true
}





func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool {
	if strings.HasPrefix(ev.Name, fsdn.path) {
		if len(ev.Name) == len(fsdn.path) {
			return fsdn.processDirEvent(ev)
		}
		return fsdn.processFileEvent(ev)
	}

	return true
}

func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool {
	const deleteFsDirOps = fsnotify.Remove | fsnotify.Rename

	if ev.Op&deleteFsDirOps != 0 {
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed")
		fsdn.base.Remove(fsdn.path)
		select {
		case fsdn.events <- Event{Op: Destroy}:
		case <-fsdn.done:

			return false
		}



	} else if ev.Op&fsnotify.Create != 0 {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory")
			select {
			case fsdn.events <- Event{Op: Error, Err: err}:
			case <-fsdn.done:

				return false
			}
		}
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added")
		return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)

	} else {
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed")
	}
	return true
}

func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool {
	const deleteFsFileOps = fsnotify.Remove
	const updateFsFileOps = fsnotify.Create | fsnotify.Write | fsnotify.Rename

	if ev.Op&updateFsFileOps != 0 {
		if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() {


			return true
		}
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")


		select {




		case fsdn.events <- Event{Op: Update, Name: filepath.Base(ev.Name)}:



		case <-fsdn.done:


			return false
		}
	} else if ev.Op&deleteFsFileOps != 0 {

		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")








		select {
		case fsdn.events <- Event{Op: Delete, Name: filepath.Base(ev.Name)}:
		case <-fsdn.done:

			return false
		}
	} else {
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed")
	}
	return true
}

func (fsdn *fsdirNotifier) Close() {
	close(fsdn.done)
}







>







>





>


>


>






>



>









>
>
>
>








>




<
|
<





>


>
>
>
|






>





>
|
|
<




<
<
|
<

>
>



>
>
|
>
>
>
>
|
>
>
>
|
>
>
|
|
|
>

>
>
>
>
>
>
>
>
|
|
|
>
|
<
<
<







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
func (fsdn *fsdirNotifier) eventLoop() {
	defer fsdn.base.Close()
	defer close(fsdn.events)
	defer close(fsdn.refresh)
	if !listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done) {
		return
	}

	for fsdn.readAndProcessEvent() {
	}
}

func (fsdn *fsdirNotifier) readAndProcessEvent() bool {
	select {
	case <-fsdn.done:
		fsdn.traceDone(1)
		return false
	default:
	}
	select {
	case <-fsdn.done:
		fsdn.traceDone(2)
		return false
	case <-fsdn.refresh:
		fsdn.log.Trace().Msg("refresh")
		listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	case err, ok := <-fsdn.base.Errors:
		fsdn.log.Trace().Err(err).Bool("ok", ok).Msg("got errors")
		if !ok {
			return false
		}
		select {
		case fsdn.events <- Event{Op: Error, Err: err}:
		case <-fsdn.done:
			fsdn.traceDone(3)
			return false
		}
	case ev, ok := <-fsdn.base.Events:
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Bool("ok", ok).Msg("file event")
		if !ok {
			return false
		}
		if !fsdn.processEvent(&ev) {
			return false
		}
	}
	return true
}

func (fsdn *fsdirNotifier) traceDone(pos int64) {
	fsdn.log.Trace().Int("i", pos).Msg("done with read and process events")
}

func (fsdn *fsdirNotifier) processEvent(ev *fsnotify.Event) bool {
	if strings.HasPrefix(ev.Name, fsdn.path) {
		if len(ev.Name) == len(fsdn.path) {
			return fsdn.processDirEvent(ev)
		}
		return fsdn.processFileEvent(ev)
	}
	fsdn.log.Trace().Str("path", fsdn.path).Str("name", ev.Name).Str("op", ev.Op.String()).Msg("event does not match")
	return true
}

func (fsdn *fsdirNotifier) processDirEvent(ev *fsnotify.Event) bool {

	if ev.Has(fsnotify.Remove) || ev.Has(fsnotify.Rename) {

		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory removed")
		fsdn.base.Remove(fsdn.path)
		select {
		case fsdn.events <- Event{Op: Destroy}:
		case <-fsdn.done:
			fsdn.log.Trace().Int("i", 1).Msg("done dir event processing")
			return false
		}
		return true
	}

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory")
			select {
			case fsdn.events <- Event{Op: Error, Err: err}:
			case <-fsdn.done:
				fsdn.log.Trace().Int("i", 2).Msg("done dir event processing")
				return false
			}
		}
		fsdn.log.Debug().Str("name", fsdn.path).Msg("Directory added")
		return listDirElements(fsdn.log, fsdn.fetcher, fsdn.events, fsdn.done)
	}

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("Directory processed")

	return true
}

func (fsdn *fsdirNotifier) processFileEvent(ev *fsnotify.Event) bool {


	if ev.Has(fsnotify.Create) || ev.Has(fsnotify.Write) {

		if fi, err := os.Lstat(ev.Name); err != nil || !fi.Mode().IsRegular() {
			regular := err == nil && fi.Mode().IsRegular()
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Err(err).Bool("regular", regular).Msg("error with file")
			return true
		}
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
		return fsdn.sendEvent(Update, filepath.Base(ev.Name))
	}

	if ev.Has(fsnotify.Rename) {
		fi, err := os.Lstat(ev.Name)
		if err != nil {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
			return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
		}
		if fi.Mode().IsRegular() {
			fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File updated")
			return fsdn.sendEvent(Update, filepath.Base(ev.Name))
		}
		fsdn.log.Trace().Str("name", ev.Name).Msg("File not regular")
		return true
	}

	if ev.Has(fsnotify.Remove) {
		fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File deleted")
		return fsdn.sendEvent(Delete, filepath.Base(ev.Name))
	}

	fsdn.log.Trace().Str("name", ev.Name).Str("op", ev.Op.String()).Msg("File processed")
	return true
}

func (fsdn *fsdirNotifier) sendEvent(op EventOp, filename string) bool {
	select {
	case fsdn.events <- Event{Op: op, Name: filename}:
	case <-fsdn.done:
		fsdn.log.Trace().Msg("done file event processing")
		return false



	}
	return true
}

func (fsdn *fsdirNotifier) Close() {
	close(fsdn.done)
}

Changes to box/notify/helper.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package notify

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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 notify

Changes to box/notify/notify.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package notify provides some notification services to be used by box services.

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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 notify provides some notification services to be used by box services.
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Valid constants for event operations.
//
// Error signals a detected error. Details are in Event.Err.
//
// Make signals that the container is detected. List events will follow.
//
// List signals a found file, if Event.Name is not empty. Otherwise it signals
//      the end of files within the container.
//
// Destroy signals that the container is not there any more. It might me Make later again.
//
// Update signals that file Event.Name was created/updated. File name is relative
//        to the container.
//
// Delete signals that file Event.Name was removed. File name is relative to
//        the container's name.
const (
	_       EventOp = iota
	Error           // Error while operating
	Make            // Make container
	List            // List container
	Destroy         // Destroy container
	Update          // Update element







|



|
|

|
|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Valid constants for event operations.
//
// Error signals a detected error. Details are in Event.Err.
//
// Make signals that the container is detected. List events will follow.
//
// List signals a found file, if Event.Name is not empty. Otherwise it signals
// the end of files within the container.
//
// Destroy signals that the container is not there any more. It might me Make later again.
//
// Update signals that file Event.Name was created/updated.
// File name is relative to the container.
//
// Delete signals that file Event.Name was removed.
// File name is relative to the container's name.
const (
	_       EventOp = iota
	Error           // Error while operating
	Make            // Make container
	List            // List container
	Destroy         // Destroy container
	Update          // Update element

Changes to box/notify/simpledir.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package notify

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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 notify
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
	}
	go sdn.eventLoop()
	return sdn, nil
}

// NewSimpleZipNotifier creates a zip-file based notifier that will not receive
// any notifications from the operating system.
func NewSimpleZipNotifier(log *logger.Logger, zipPath string) (Notifier, error) {
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newZipPathFetcher(zipPath),
	}
	go sdn.eventLoop()
	return sdn, nil
}

func (sdn *simpleDirNotifier) Events() <-chan Event {
	return sdn.events
}

func (sdn *simpleDirNotifier) Refresh() {







|








|







40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
	}
	go sdn.eventLoop()
	return sdn, nil
}

// NewSimpleZipNotifier creates a zip-file based notifier that will not receive
// any notifications from the operating system.
func NewSimpleZipNotifier(log *logger.Logger, zipPath string) Notifier {
	sdn := &simpleDirNotifier{
		log:     log,
		events:  make(chan Event),
		done:    make(chan struct{}),
		refresh: make(chan struct{}),
		fetcher: newZipPathFetcher(zipPath),
	}
	go sdn.eventLoop()
	return sdn
}

func (sdn *simpleDirNotifier) Events() <-chan Event {
	return sdn.events
}

func (sdn *simpleDirNotifier) Refresh() {

Changes to cmd/cmd_file.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package cmd

import (
	"context"
	"flag"
	"fmt"
	"io"
	"os"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"



)

// ---------- Subcommand: file -----------------------------------------------

func cmdFile(fs *flag.FlagSet) (int, error) {
	enc := fs.Lookup("t").Value.String()
	m, inp, err := getInput(fs.Args())
	if m == nil {
		return 2, err
	}
	z := parser.ParseZettel(
		context.Background(),
		domain.Zettel{
			Meta:    m,
			Content: domain.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, api.ValueSyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}

|

















|
<
<
<



>
>
>












|

|

|







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
//-----------------------------------------------------------------------------
// 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 cmd

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"
)

// ---------- Subcommand: file -----------------------------------------------

func cmdFile(fs *flag.FlagSet) (int, error) {
	enc := fs.Lookup("t").Value.String()
	m, inp, err := getInput(fs.Args())
	if m == nil {
		return 2, err
	}
	z := parser.ParseZettel(
		context.Background(),
		zettel.Zettel{
			Meta:    m,
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, meta.SyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}

Changes to cmd/cmd_password.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) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
)

// ---------- Subcommand: password -------------------------------------------

func cmdPassword(fs *flag.FlagSet) (int, error) {
	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "User name and user zettel identification missing")

|

|















|

|







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) 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 cmd

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) {
	if fs.NArg() == 0 {
		fmt.Fprintln(os.Stderr, "User name and user zettel identification missing")

Changes to cmd/cmd_run.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package cmd

import (
	"context"
	"flag"
	"net/http"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter/api"
	"zettelstore.de/z/web/adapter/webui"
	"zettelstore.de/z/web/server"

)

// ---------- Subcommand: run ------------------------------------------------

func flgRun(fs *flag.FlagSet) {
	fs.String("c", "", "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")

|


















<





>







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
//-----------------------------------------------------------------------------
// 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 cmd

import (
	"context"
	"flag"
	"net/http"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"

	"zettelstore.de/z/kernel"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter/api"
	"zettelstore.de/z/web/adapter/webui"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

// ---------- Subcommand: run ------------------------------------------------

func flgRun(fs *flag.FlagSet) {
	fs.String("c", "", "configuration file")
	fs.Uint("a", 0, "port number kernel service (0=disable)")
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
	return exitCode, err
}

func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	webLog := kern.GetLogger(kernel.WebService)
	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)

	var getUser getUserImpl
	logAuth := kern.GetLogger(kernel.AuthService)
	logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser)

	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
	ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucListMeta := usecase.NewListMeta(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta)

	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucListTags := usecase.NewListTags(protectedBoxManager)
	ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)
	ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager)
	ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig)
	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))








	webSrv.Handle("/", wui.MakeGetRootHandler(protectedBoxManager))
	if assetDir := kern.GetConfig(kernel.WebService, kernel.WebAssetDir).(string); assetDir != "" {
		const assetPrefix = "/assets/"
		webSrv.Handle(assetPrefix, http.StripPrefix(assetPrefix, http.FileServer(http.Dir(assetDir))))

	}

	// Web user interface
	if !authManager.IsReadonly() {
		webSrv.AddZettelRoute('b', server.MethodGet, wui.MakeGetRenameZettelHandler(
			ucGetMeta, &ucEvaluate))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))


		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(
			ucGetMeta, ucGetAllMeta, &ucEvaluate))
		webSrv.AddZettelRoute('d', server.MethodPost, wui.MakePostDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('e', server.MethodGet, wui.MakeEditGetZettelHandler(ucGetZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('e', server.MethodPost, wui.MakeEditSetZettelHandler(&ucUpdate))
	}
	webSrv.AddListRoute('g', server.MethodGet, wui.MakeGetGoActionHandler(&ucRefresh))
	webSrv.AddListRoute('h', server.MethodGet, wui.MakeListHTMLMetaHandler(ucListMeta, &ucEvaluate))
	webSrv.AddZettelRoute('h', server.MethodGet, wui.MakeGetHTMLZettelHandler(
		&ucEvaluate, ucGetMeta))
	webSrv.AddListRoute('i', server.MethodGet, wui.MakeGetLoginOutHandler())
	webSrv.AddListRoute('i', server.MethodPost, wui.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddZettelRoute('i', server.MethodGet, wui.MakeGetInfoHandler(
		ucParseZettel, &ucEvaluate, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs))
	webSrv.AddZettelRoute('k', server.MethodGet, wui.MakeZettelContextHandler(
		ucZettelContext, &ucEvaluate))

	// API
	webSrv.AddListRoute('a', server.MethodPost, a.MakePostLoginHandler(&ucAuthenticate))
	webSrv.AddListRoute('a', server.MethodPut, a.MakeRenewAuthHandler())
	webSrv.AddListRoute('j', server.MethodGet, a.MakeListMetaHandler(ucListMeta))
	webSrv.AddZettelRoute('j', server.MethodGet, a.MakeGetZettelHandler(ucGetZettel))
	webSrv.AddListRoute('m', server.MethodGet, a.MakeListMapMetaHandler(ucListRoles, ucListTags))
	webSrv.AddZettelRoute('m', server.MethodGet, a.MakeGetMetaHandler(ucGetMeta))
	webSrv.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucEvaluate)))
	webSrv.AddZettelRoute('p', server.MethodGet, a.MakeGetParsedZettelHandler(ucParseZettel))
	webSrv.AddListRoute('q', server.MethodGet, a.MakeQueryHandler(ucListMeta))
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(
		ucGetMeta, ucUnlinkedRefs, &ucEvaluate))
	webSrv.AddZettelRoute('v', server.MethodGet, a.MakeGetEvalZettelHandler(ucEvaluate))
	webSrv.AddListRoute('x', server.MethodGet, a.MakeGetDataHandler(ucVersion))
	webSrv.AddListRoute('x', server.MethodPost, a.MakePostCommandHandler(&ucIsAuth, &ucRefresh))
	webSrv.AddZettelRoute('x', server.MethodGet, a.MakeZettelContextHandler(ucZettelContext))
	webSrv.AddListRoute('z', server.MethodGet, a.MakeListPlainHandler(ucListMeta))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetPlainZettelHandler(ucGetZettel))
	if !authManager.IsReadonly() {
		webSrv.AddListRoute('j', server.MethodPost, a.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('j', server.MethodPut, a.MakeUpdateZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('j', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('j', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
		webSrv.AddListRoute('z', server.MethodPost, a.MakePostCreatePlainZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('z', server.MethodPut, a.MakeUpdatePlainZettelHandler(&ucUpdate))
		webSrv.AddZettelRoute('z', server.MethodDelete, a.MakeDeleteZettelHandler(&ucDelete))
		webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
	}

	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) }







<
<
<
<
<
<




>
|


|
<


|
|
>


<
<



<


>
>
>
>
>
>
>





>




|
<

>
>



|
<





|
|
<



|
<
<




<
<
<
<
<
<
<
<
<
<
<


<
|
|

<
<
<
<
|
|












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
	return exitCode, err
}

func setupRouting(webSrv server.Server, boxManager box.Manager, authManager auth.Manager, rtConfig config.Config) {
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	webLog := kern.GetLogger(kernel.WebService)







	var getUser getUserImpl
	logAuth := kern.GetLogger(kernel.AuthService)
	logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser)
	ucGetUser := usecase.NewGetUser(authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)

	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	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))
	}

	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) }

Changes to cmd/command.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"

	"zettelstore.de/c/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

|













|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//-----------------------------------------------------------------------------
// 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 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	if cmd.Name == "" || cmd.Func == nil {
		panic("Required command values missing")
	}
	if _, ok := commands[cmd.Name]; ok {
		panic("Command already registered: " + cmd.Name)
	}
	cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError)
	cmd.flags.String("l", logger.InfoLevel.String(), "global log level")

	if cmd.SetFlags != nil {
		cmd.SetFlags(cmd.flags)
	}
	commands[cmd.Name] = cmd
}








|







44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
	if cmd.Name == "" || cmd.Func == nil {
		panic("Required command values missing")
	}
	if _, ok := commands[cmd.Name]; ok {
		panic("Command already registered: " + cmd.Name)
	}
	cmd.flags = flag.NewFlagSet(cmd.Name, flag.ExitOnError)
	cmd.flags.String("l", logger.InfoLevel.String(), "log level specification")

	if cmd.SetFlags != nil {
		cmd.SetFlags(cmd.flags)
	}
	commands[cmd.Name] = cmd
}

Changes to cmd/main.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

package cmd

import (
	"crypto/sha256"
	"errors"
	"flag"
	"fmt"
	"net"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/impl"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/compbox"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"


)

const strRunSimple = "run-simple"

func init() {
	RegisterCommand(Command{
		Name: "help",

|












<










|






<
<




>
>







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
//-----------------------------------------------------------------------------
// 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 cmd

import (
	"crypto/sha256"

	"flag"
	"fmt"
	"net"
	"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"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const strRunSimple = "run-simple"

func init() {
	RegisterCommand(Command{
		Name: "help",
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
	})
	RegisterCommand(Command{
		Name: "password",
		Func: cmdPassword,
	})
}

func fetchStartupConfiguration(fs *flag.FlagSet) (cfg *meta.Meta) {
	if configFlag := fs.Lookup("c"); configFlag != nil {
		if filename := configFlag.Value.String(); filename != "" {
			content, err := readConfiguration(filename)
			return createConfiguration(content, err)
		}
	}
	content, err := searchAndReadConfiguration()
	return createConfiguration(content, err)
}

func createConfiguration(content []byte, err error) *meta.Meta {
	if err != nil {
		return meta.New(id.Invalid)
	}
	return meta.NewFromInput(id.Invalid, input.NewInput(content))
}

func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) }

func searchAndReadConfiguration() ([]byte, error) {
	for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} {
		if content, err := readConfiguration(filename); err == nil {
			return content, nil
		}
	}
	return readConfiguration(".zscfg")
}

func getConfig(fs *flag.FlagSet) *meta.Meta {
	cfg := fetchStartupConfiguration(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			if portStr, err := parsePort(flg.Value.String()); err == nil {
				cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", portStr))
			}
		case "a":
			if portStr, err := parsePort(flg.Value.String()); err == nil {
				cfg.Set(keyAdminPort, portStr)
			}
		case "d":
			val := flg.Value.String()
			if strings.HasPrefix(val, "/") {
				val = "dir://" + val
			} else {
				val = "dir:" + val
			}
			deleteConfiguredBoxes(cfg)
			cfg.Set(keyBoxOneURI, val)
		case "l":
			cfg.Set(keyLogLevel, flg.Value.String())
		case "debug":
			cfg.Set(keyDebug, flg.Value.String())
		case "r":
			cfg.Set(keyReadOnly, flg.Value.String())
		case "v":
			cfg.Set(keyVerbose, flg.Value.String())
		}
	})
	return cfg
}

func parsePort(s string) (string, error) {
	port, err := net.LookupPort("tcp", s)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Wrong port specification: %q", s)
		return "", err
	}
	return strconv.Itoa(port), nil
}

func deleteConfiguredBoxes(cfg *meta.Meta) {
	for _, p := range cfg.PairsRest() {
		if key := p.Key; strings.HasPrefix(key, kernel.BoxURIs) {
			cfg.Delete(key)
		}
	}
}

const (
	keyAdminPort         = "admin-port"
	keyAssetDir          = "asset-dir"
	keyBaseURL           = "base-url"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"

	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) error {
	debugMode := cfg.GetBool(keyDebug)
	if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel {
		kernel.Main.SetGlobalLogLevel(logger.DebugLevel)
	}
	if strLevel, found := cfg.Get(keyLogLevel); found {
		if level := logger.ParseLevel(strLevel); level.IsValid() {
			kernel.Main.SetGlobalLogLevel(level)
		}
	}
	ok := setConfigValue(true, kernel.CoreService, kernel.CoreDebug, debugMode)
	ok = setConfigValue(ok, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
		ok = setConfigValue(ok, kernel.CoreService, kernel.CorePort, val)
	}

	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
	ok = setConfigValue(ok, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))

	ok = setConfigValue(
		ok, kernel.BoxService, kernel.BoxDefaultDirType,
		cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify))
	ok = setConfigValue(ok, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel")
	for i := 1; ; i++ {
		key := kernel.BoxURIs + strconv.Itoa(i)
		val, found := cfg.Get(key)
		if !found {
			break
		}
		ok = setConfigValue(ok, kernel.BoxService, key, val)
	}

	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebListenAddress,
		cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	if val, found := cfg.Get(keyBaseURL); found {
		ok = setConfigValue(ok, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		ok = setConfigValue(ok, kernel.WebService, kernel.WebURLPrefix, val)
	}
	ok = setConfigValue(ok, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	ok = setConfigValue(ok, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		ok = setConfigValue(ok, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	ok = setConfigValue(
		ok, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	if val, found := cfg.Get(keyAssetDir); found {
		ok = setConfigValue(ok, kernel.WebService, kernel.WebAssetDir, val)
	}

	if !ok {
		return errors.New("unable to set configuration")
	}
	return nil
}

func setConfigValue(ok bool, subsys kernel.Service, key string, val interface{}) bool {

	done := kernel.Main.SetConfig(subsys, key, fmt.Sprintf("%v", val))
	if !done {
		kernel.Main.GetKernelLogger().Error().Str(key, fmt.Sprint(val)).Msg("Unable to set configuration")
	}

	return ok && done
}

func executeCommand(name string, args ...string) int {
	command, ok := Get(name)
	if !ok {
		fmt.Fprintf(os.Stderr, "Unknown command %q\n", name)
		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
	}
	cfg := getConfig(fs)
	if err := setServiceConfig(cfg); err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
		return 2
	}

	kern := kernel.Main
	var createManager kernel.CreateBoxManagerFunc
	if command.Boxes {
		createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) {







|



|


|
|











|
|

|


|


|
|



<
|
<

<
|
<



















|
<
<
<
<
<
<
<
<
<

















>













|


|

|
<
|
|
<
|
|

|


|
|

|
|

|






|


|
|
|

|


|

|
|

|

|
|
|
|

|

<
<
<
<
|


|
>
|
|
|
|
>
|













|
|
|







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
	})
	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":
			val := flg.Value.String()
			if strings.HasPrefix(val, "/") {
				val = "dir://" + val
			} else {
				val = "dir:" + val
			}
			deleteConfiguredBoxes(cfg)
			cfg.Set(keyBoxOneURI, val)
		case "l":
			cfg.Set(keyLogLevel, flg.Value.String())
		case "debug":
			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)
		}
	}
}

const (
	keyAdminPort         = "admin-port"
	keyAssetDir          = "asset-dir"
	keyBaseURL           = "base-url"
	keyDebug             = "debug-mode"
	keyDefaultDirBoxType = "default-dir-box-type"
	keyInsecureCookie    = "insecure-cookie"
	keyInsecureHTML      = "insecure-html"
	keyListenAddr        = "listen-addr"
	keyLogLevel          = "log-level"
	keyMaxRequestSize    = "max-request-size"
	keyOwner             = "owner"
	keyPersistentCookie  = "persistent-cookie"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	keyReadOnly          = "read-only-mode"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
	debugMode := cfg.GetBool(keyDebug)
	if debugMode && kernel.Main.GetKernelLogger().Level() > logger.DebugLevel {
		kernel.Main.SetLogLevel(logger.DebugLevel.String())
	}
	if logLevel, found := cfg.Get(keyLogLevel); found {

		kernel.Main.SetLogLevel(logLevel)
	}

	err := setConfigValue(nil, kernel.CoreService, kernel.CoreDebug, debugMode)
	err = setConfigValue(err, kernel.CoreService, kernel.CoreVerbose, cfg.GetBool(keyVerbose))
	if val, found := cfg.Get(keyAdminPort); found {
		err = setConfigValue(err, kernel.CoreService, kernel.CorePort, val)
	}

	err = setConfigValue(err, kernel.AuthService, kernel.AuthOwner, cfg.GetDefault(keyOwner, ""))
	err = setConfigValue(err, kernel.AuthService, kernel.AuthReadonly, cfg.GetBool(keyReadOnly))

	err = setConfigValue(
		err, kernel.BoxService, kernel.BoxDefaultDirType,
		cfg.GetDefault(keyDefaultDirBoxType, kernel.BoxDirTypeNotify))
	err = setConfigValue(err, kernel.BoxService, kernel.BoxURIs+"1", "dir:./zettel")
	for i := 1; ; i++ {
		key := kernel.BoxURIs + strconv.Itoa(i)
		val, found := cfg.Get(key)
		if !found {
			break
		}
		err = setConfigValue(err, kernel.BoxService, key, val)
	}

	err = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
	if val, found := cfg.Get(keyBaseURL); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebBaseURL, val)
	}
	if val, found := cfg.Get(keyURLPrefix); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebURLPrefix, val)
	}
	err = setConfigValue(err, kernel.WebService, kernel.WebSecureCookie, !cfg.GetBool(keyInsecureCookie))
	err = setConfigValue(err, kernel.WebService, kernel.WebPersistentCookie, cfg.GetBool(keyPersistentCookie))
	if val, found := cfg.Get(keyMaxRequestSize); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebMaxRequestSize, val)
	}
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeAPI, cfg.GetDefault(keyTokenLifetimeAPI, ""))
	err = setConfigValue(
		err, kernel.WebService, kernel.WebTokenLifetimeHTML, cfg.GetDefault(keyTokenLifetimeHTML, ""))
	if val, found := cfg.Get(keyAssetDir); found {
		err = setConfigValue(err, kernel.WebService, kernel.WebAssetDir, val)
	}




	return err == nil
}

func setConfigValue(err error, subsys kernel.Service, key string, val any) error {
	if err == nil {
		err = kernel.Main.SetConfig(subsys, key, fmt.Sprint(val))
		if err != nil {
			kernel.Main.GetKernelLogger().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration")
		}
	}
	return err
}

func executeCommand(name string, args ...string) int {
	command, ok := Get(name)
	if !ok {
		fmt.Fprintf(os.Stderr, "Unknown command %q\n", name)
		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
	if command.Boxes {
		createManager = func(boxURIs []*url.URL, authManager auth.Manager, rtConfig config.Config) (box.Manager, error) {
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
			return nil
		},
	)

	if command.Simple {
		kern.SetConfig(kernel.ConfigService, kernel.ConfigSimpleMode, "true")
	}
	kern.Start(command.Header, command.LineServer)
	exitCode, err := command.Func(fs)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s: %v\n", name, err)
	}
	kern.Shutdown(true)
	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
	}







|











|







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 cmd/register.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package cmd provides command generic functions.
package cmd

// Mention all needed encoders, parsers and stores to have them registered.
import (
	_ "zettelstore.de/z/box/compbox"       // Allow to use computed box.
	_ "zettelstore.de/z/box/constbox"      // Allow to use global internal box.
	_ "zettelstore.de/z/box/dirbox"        // Allow to use directory box.
	_ "zettelstore.de/z/box/filebox"       // Allow to use file box.
	_ "zettelstore.de/z/box/membox"        // Allow to use in-memory box.
	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.


	_ "zettelstore.de/z/encoder/sexprenc"  // Allow to use sexpr encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zjsonenc"  // Allow to use ZJSON encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.

	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/pikchr"     // Allow to use PIC/Pikchr parser.
	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

|



















>
>
|

<



>


<



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) 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 cmd provides command generic functions.
package cmd

// Mention all needed encoders, parsers and stores to have them registered.
import (
	_ "zettelstore.de/z/box/compbox"       // Allow to use computed box.
	_ "zettelstore.de/z/box/constbox"      // Allow to use global internal box.
	_ "zettelstore.de/z/box/dirbox"        // Allow to use directory box.
	_ "zettelstore.de/z/box/filebox"       // Allow to use file box.
	_ "zettelstore.de/z/box/membox"        // Allow to use in-memory box.
	_ "zettelstore.de/z/encoder/htmlenc"   // Allow to use HTML encoder.
	_ "zettelstore.de/z/encoder/mdenc"     // Allow to use markdown encoder.
	_ "zettelstore.de/z/encoder/shtmlenc"  // Allow to use SHTML encoder.
	_ "zettelstore.de/z/encoder/szenc"     // Allow to use Sz encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.

	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.

	_ "zettelstore.de/z/parser/plain"      // Allow to use plain parser.
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

Changes to cmd/zettelstore/main.go.

1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2021 Detlef Stern
//
// This file is part of zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package main is the starting point for the zettelstore command.

|

|







1
2
3
4
5
6
7
8
9
10
11
//-----------------------------------------------------------------------------
// 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 main is the starting point for the zettelstore command.

Changes to collect/collect.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to collect/collect_test.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to collect/order.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to collect/split.go.

1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

|







1
2
3
4
5
6
7
8
9
//-----------------------------------------------------------------------------
// 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.
//-----------------------------------------------------------------------------

Changes to config/config.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






































//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package config provides functions to retrieve runtime configuration data.
package config

import (
	"context"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
)

// Key values that are supported by Config.Get
const (
	KeyFooterHTML = "footer-html"

	// api.KeyLang
	KeyMarkerExternal = "marker-external"
)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig

	// Get returns the value of the given key. It searches first in the given metadata,
	// then in the data of the current user, and at last in the system-wide data.
	Get(ctx context.Context, m *meta.Meta, key string) string

	// AddDefaultValues enriches the given meta data with its default values.
	AddDefaultValues(context.Context, *meta.Meta) *meta.Meta

	// GetSiteName returns the current value of the "site-name" key.
	GetSiteName() string

	// GetHomeZettel returns the value of the "home-zettel" key.
	GetHomeZettel() id.Zid

	// GetMaxTransclusions return the maximum number of indirect transclusions.
	GetMaxTransclusions() int

	// GetYAMLHeader returns the current value of the "yaml-header" key.
	GetYAMLHeader() bool

	// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
	GetZettelFileSyntax() []string
}

// AuthConfig are relevant configuration values for authentication.
type AuthConfig interface {
	// GetSimpleMode returns true if system tuns in simple-mode.
	GetSimpleMode() bool

	// GetExpertMode returns the current value of the "expert-mode" key.
	GetExpertMode() bool

	// GetVisibility returns the visibility value of the metadata.
	GetVisibility(m *meta.Meta) meta.Visibility
}







































|














|
<




|
>

<
















|
|

|




















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
//-----------------------------------------------------------------------------
// 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 config provides functions to retrieve runtime configuration data.
package config

import (
	"context"

	"zettelstore.de/z/zettel/meta"

)

// Key values that are supported by Config.Get
const (
	KeyFooterZettel = "footer-zettel"
	KeyHomeZettel   = "home-zettel"
	// api.KeyLang

)

// Config allows to retrieve all defined configuration values that can be changed during runtime.
type Config interface {
	AuthConfig

	// Get returns the value of the given key. It searches first in the given metadata,
	// then in the data of the current user, and at last in the system-wide data.
	Get(ctx context.Context, m *meta.Meta, key string) string

	// AddDefaultValues enriches the given meta data with its default values.
	AddDefaultValues(context.Context, *meta.Meta) *meta.Meta

	// GetSiteName returns the current value of the "site-name" key.
	GetSiteName() string

	// GetHTMLInsecurity returns the current
	GetHTMLInsecurity() HTMLInsecurity

	// GetMaxTransclusions returns the maximum number of indirect transclusions.
	GetMaxTransclusions() int

	// GetYAMLHeader returns the current value of the "yaml-header" key.
	GetYAMLHeader() bool

	// GetZettelFileSyntax returns the current value of the "zettel-file-syntax" key.
	GetZettelFileSyntax() []string
}

// AuthConfig are relevant configuration values for authentication.
type AuthConfig interface {
	// GetSimpleMode returns true if system tuns in simple-mode.
	GetSimpleMode() bool

	// GetExpertMode returns the current value of the "expert-mode" key.
	GetExpertMode() bool

	// GetVisibility returns the visibility value of the metadata.
	GetVisibility(m *meta.Meta) meta.Visibility
}

// HTMLInsecurity states what kind of insecure HTML is allowed.
// The lowest value is the most secure one (disallowing any HTML)
type HTMLInsecurity uint8

// Constant values for HTMLInsecurity:
const (
	NoHTML HTMLInsecurity = iota
	SyntaxHTML
	MarkdownHTML
	ZettelmarkupHTML
)

func (hi HTMLInsecurity) String() string {
	switch hi {
	case SyntaxHTML:
		return "html"
	case MarkdownHTML:
		return "markdown"
	case ZettelmarkupHTML:
		return "zettelmarkup"
	}
	return "secure"
}

// AllowHTML returns true, if the given HTML insecurity level matches the given syntax value.
func (hi HTMLInsecurity) AllowHTML(syntax string) bool {
	switch hi {
	case SyntaxHTML:
		return syntax == meta.SyntaxHTML
	case MarkdownHTML:
		return syntax == meta.SyntaxHTML || syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
	case ZettelmarkupHTML:
		return syntax == meta.SyntaxZmk || syntax == meta.SyntaxHTML ||
			syntax == meta.SyntaxMarkdown || syntax == meta.SyntaxMD
	}
	return false
}

Changes to docs/development/00010000000000.zettel.

1
2
3
4

5
6
7

8
id: 00010000000000
title: Developments Notes
role: zettel
syntax: zmk

modified: 20210916194954

* [[Required Software|20210916193200]]

* [[Checklist for Release|20210916194900]]




>
|


>

1
2
3
4
5
6
7
8
9
10
id: 00010000000000
title: Developments Notes
role: zettel
syntax: zmk
created: 00010101000000
modified: 20221026184905

* [[Required Software|20210916193200]]
* [[Fuzzing tests|20221026184300]]
* [[Checklist for Release|20210916194900]]

Changes to docs/development/20210916193200.zettel.

1
2
3
4

5
6
7
8
9
10
11
12
13
14
15
16
17
18
19










id: 20210916193200
title: Required Software
role: zettel
syntax: zmk

modified: 20211213190428

The following software must be installed:

* A current, supported [[release of Go|https://golang.org/doc/devel/release.html]],
* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``

Make sure that the software is in your path, e.g. via:

```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```














>
|



|
<
|
|


<




>
>
>
>
>
>
>
>
>
>
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
id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20230405150541

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],

* [[Fossil|https://fossil-scm.org/]],
* [[Git|https://git-scm.org/]] (most dependencies are accessible via Git only).

Make sure that the software is in your path, e.g. via:

```sh
export PATH=$PATH:/usr/local/go/bin
export PATH=$PATH:$(go env GOPATH)/bin
```

The internal build tool need the following software.
It can be installed / updated via the build tool itself: ``go run tools/build.go tools``.

Otherwise you can install the software by hand:

* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,
* [[govulncheck|https://golang.org/x/vuln/cmd/govulncheck]] via ``go install golang.org/x/vuln/cmd/govulncheck@latest``,

Changes to docs/development/20210916194900.zettel.

1
2
3
4

5
6
7
8
9
10
11
12
id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk

modified: 20220309105459

# Sync with the official repository
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date.
#* ``cat go.mod``




>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20230402181229

# Sync with the official repository
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date.
#* ``cat go.mod``
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd release``
#* ``fossil uv add *.zip``
#* ``cd ..``
#* Synchronize with main repository:
#* ``fossil sync -u``
# Enable autosync:
#* ``fossil setting autosync on``







|






46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd releases``
#* ``fossil uv add *.zip``
#* ``cd ..``
#* Synchronize with main repository:
#* ``fossil sync -u``
# Enable autosync:
#* ``fossil setting autosync on``

Added docs/development/20221026184300.zettel.





























>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 20221026184300
title: Fuzzing Tests
role: zettel
syntax: zmk
created: 20221026184320
modified: 20221102140156

The source code contains some simple [[fuzzing tests|https://go.dev/security/fuzz/]].
You should call them regularly to make sure that the software will cope with unusual input.

```sh
go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/draw
go test -fuzz=FuzzParseBlocks zettelstore.de/z/parser/zettelmark
```

Changes to docs/manual/00000000000100.zettel.

1
2
3
4

5
6
7
8
9
10
11
12
13
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none

default-copyright: (c) 2020-2022 by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-html: <hr><p><a href="/home/doc/trunk/www/impri.wiki">Imprint / Privacy</a></p>
home-zettel: 00001000000000
modified: 20220215171041
site-name: Zettelstore Manual
visibility: owner





>
|


|

|



1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 00010101000000
default-copyright: (c) 2020-present by Detlef Stern <ds@zettelstore.de>
default-license: EUPL-1.2-or-later
default-visibility: public
footer-zettel: 00001000000100
home-zettel: 00001000000000
modified: 20221205173642
site-name: Zettelstore Manual
visibility: owner

Added docs/manual/00000000025001.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
id: 00000000025001
title: Zettelstore User CSS
role: configuration
syntax: css
created: 20210622110143
modified: 20220926183101
visibility: public

Added docs/manual/00000000025001.css.





>
>
1
2
/* User-defined CSS */
.example { border-style: dotted !important }

Added docs/manual/00001000000100.zettel.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
id: 00001000000100
title: Footer Zettel
role: configuration
syntax: zmk
created: 20221205173520
modified: 20221207175927

[[Imprint / Privacy|/home/doc/trunk/www/impri.wiki]]

Changes to docs/manual/00001002000000.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






id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk

modified: 20211124131628

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.




; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If your device 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.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
: There is only one executable for Zettelstore and one directory, where your zettel are stored.
: If you decide to use multiple directories, you are free to configure Zettelstore appropriately.
; Multiple modes of operation
: You can use Zettelstore as a standalone software on your device, but you are not restricted to it.
: You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel.
; Multiple user interfaces
: Zettelstore provides a default [[web-based user interface|00001014000000]].
  Anybody can provide alternative user interfaces, e.g. for special purposes.
; 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.











>
|






>
>
>
>



|
















>
>
>
>
>
>
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
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.
: Upgrading the software is done just by replacing the executable with a newer one.
; Ease of operation
: There is only one executable for Zettelstore and one directory, where your zettel are stored.
: If you decide to use multiple directories, you are free to configure Zettelstore appropriately.
; Multiple modes of operation
: You can use Zettelstore as a standalone software on your device, but you are not restricted to it.
: You can install the software on a central server, or you can install it on all your devices with no restrictions how to synchronize your zettel.
; Multiple user interfaces
: Zettelstore provides a default [[web-based user interface|00001014000000]].
  Anybody can provide alternative user interfaces, e.g. for special purposes.
; 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/00001004010000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220914183434

The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221128155143

The configuration file, as specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
These options cannot be stored in a [[configuration zettel|00001004020000]] because either they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore need to know in advance, on which network address is must listen or where zettel are stored.
An attacker that is able to change the owner can do anything.
Therefore only the owner of the computer on which Zettelstore runs can change this information.

64
65
66
67
68
69
70










71
72
73
74
75
76
77
78
79



80


81
82
83
84
85
86
87

  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
  Otherwise web browser are free to ignore the authentication cookie.

  Default: ""false""










; [!listen-addr|''listen-addr'']
: Configures the network address, where the Zettelstore service is listening for requests.
  Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port.

  Default value: ""127.0.0.1:23123""
; [!log-level|''log-level'']
: Specify the global [[logging level|00001004059700]] for the whole application, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].
  



  Default: ""info"".



  When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore.
; [!max-request-size|''max-request-size'']
: Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 







>
>
>
>
>
>
>
>
>
>






|

|
>
>
>

>
>







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

  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]], if authentication is enabled and Zettelstore is not accessible not via HTTPS (but via HTTP).
  Otherwise web browser are free to ignore the authentication cookie.

  Default: ""false""
; [!insecure-html|''insecure-html'']
: Allows to use HTML, e.g. within supported markup languages, even if this might introduce security-related problems.
  However, HTML containing the ``<script>`` or the ``<iframe>`` tag is always ignored.
  But due to ""clever"" ways of combining HTML, CSS, JavaScript, there might be some negative security consequences.
  Please be aware of this!

  Allowed values: ""html"" (allow zettel with [[syntax ""html""|00001008000000#html]]), ""markdown"" (""html"", plus allow inline HTML for Markdown markup only), ""zettelmarkup"" (""markdown"", plus allow inline HTML for Zettelmarkup).
  Any other value is interpreted as ""secure"".

  Default: ""secure"".
; [!listen-addr|''listen-addr'']
: Configures the network address, where the Zettelstore service is listening for requests.
  Syntax is: ''[NETWORKIP]:PORT'', where ''NETWORKIP'' is the IP-address of the networking interface (or something like ""0.0.0.0"" if you want to listen on all network interfaces, and ''PORT'' is the TCP port.

  Default value: ""127.0.0.1:23123""
; [!log-level|''log-level'']
: Specify the [[logging level|00001004059700]] for the whole application or for a given (internal) service, overwriting the level ""debug"" set by configuration [[''debug-mode''|#debug-mode]].
  Can be changed at runtime, even for specific internal services, with the ''log-level'' command of the [[administrator console|00001004101000#log-level]].

  Several specifications are separated by the semicolon character (""'';''"", U+003B).
  Each specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.

  Default: ""info"".

  Examples: ""sense"" will produce sensing messages (e.g. a little more than ""info""); ""sense;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing sensing messages for all other components.

  When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore.
; [!max-request-size|''max-request-size'']
: Limits the maximum byte size of a web request body to prevent clients from accidentally or maliciously sending a large request and wasting server resources.
  The minimum value is 1024.

  Default: 16777216 (16 MiB). 

Changes to docs/manual/00001004020000.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
45
46
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220827180953

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called __configuration zettel__.
The following metadata keys change the appearance / behavior of Zettelstore:



; [!default-copyright|''default-copyright'']
: Copyright value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''.

  Default: (the empty string).
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.
  Default: (the empty string).
; [!default-visibility|''default-visibility'']
: Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key.
  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.
  Default: ""False"".
; [!footer-html|''footer-html'']
: Contains some HTML code that will be included into the footer of each Zettelstore web page.
  It only affects the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as JSON, etc. is not affected.

  Default: (the empty string).



; [!home-zettel|''home-zettel'']
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.
; [!marker-external|''marker-external'']
: Some HTML code that is displayed after a [[reference to external material|00001007040310]].
  Default: ""&\#10138;"", to display a ""&#10138;"" sign.
; [!lang|''lang'']
: Language to be used when displaying content.

  Default: ""en"".

  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.






|



|
>
>

















|
<
|
|
>
|
>
>
>



|
|
|







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
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807171016

You can configure a running Zettelstore by modifying the special zettel with the ID [[00000000000100]].
This zettel is called __configuration zettel__.
The following metadata keys change the appearance / behavior of Zettelstore.
Some of them can be overwritten in an [[user zettel|00001010040200]], a subset of those may be overwritten in zettel that is currently used.
See the full list of [[metadata that may be overwritten|00001004020200]].

; [!default-copyright|''default-copyright'']
: Copyright value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''copyright''.

  Default: (the empty string).
; [!default-license|''default-license'']
: License value to be used when rendering content.
  Can be overwritten in a zettel with [[meta key|00001006020000]] ''license''.
  Default: (the empty string).
; [!default-visibility|''default-visibility'']
: Visibility to be used, if zettel does not specify a value for the [[''visibility''|00001006020000#visibility]] metadata key.
  Default: ""login"".
; [!expert-mode|''expert-mode'']
: If set to a [[boolean true value|00001006030500]], all zettel with [[visibility ""expert""|00001010070200]] will be shown (to the owner, if [[authentication is enabled|00001010040100]]; to all, otherwise).
  This affects most computed zettel.
  Default: ""False"".
; [!footer-zettel|''footer-zettel'']

: Identifier of a zettel that is rendered as HTML and will be placed as the footer of every zettel in the [[web user interface|00001014000000]].
  Zettel content, delivered via the [[API|00001012000000]] as symbolic expressions, etc. is not affected.
  If the zettel identifier is invalid or references a zettel that could not be read (possibly because of a limited [[visibility setting|00001010070200]]), nothing is written as the footer.

  May be [[overwritten|00001004020200]] in a user zettel.

  Default: (an invalid zettel identifier)
; [!home-zettel|''home-zettel'']
: Specifies the identifier of the zettel, that should be presented for the default view / home view.
  If not given or if the identifier does not identify a zettel, the zettel with the identifier ''00010000000000'' is shown.

  May be [[overwritten|00001004020200]] in a user zettel.

; [!lang|''lang'']
: Language to be used when displaying content.

  Default: ""en"".

  This value is used as a default value, if it is not set in an user's zettel or in a zettel.
  It is also used to specify the language for all non-zettel content, e.g. lists or search results.

Added docs/manual/00001004020200.zettel.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id: 00001004020200
title: Runtime configuration data that may be user specific or zettel specific
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20221205155521
modified: 20230317183403

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful

Changes to docs/manual/00001004050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20220805174626

Zettelstore is not just a service that provides services of a zettelkasten.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004050000
title: Command line parameters
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221128161932

Zettelstore is not just a service that provides services of a zettelkasten.
It allows to some tasks to be executed at the command line.
Typically, the task (""sub-command"") will be given at the command line as the first parameter.

If no parameter is given, the Zettelstore is called as
```
23
24
25
26
27
28
29







30
* [[``zettelstore help``|00001004050200]] lists all available sub-commands.
* [[``zettelstore version``|00001004050400]] to display version information of Zettelstore.
* [[``zettelstore run``|00001004051000]] to start the Zettelstore service.
* [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI.
* [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services.
* [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]].








To measure potential bottlenecks within the software Zettelstore, there are some [[command line flags for profiling the application|00001004059900]].







>
>
>
>
>
>
>

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
* [[``zettelstore help``|00001004050200]] lists all available sub-commands.
* [[``zettelstore version``|00001004050400]] to display version information of Zettelstore.
* [[``zettelstore run``|00001004051000]] to start the Zettelstore service.
* [[``zettelstore run-simple``|00001004051100]] is typically called, when you start Zettelstore by a double.click in your GUI.
* [[``zettelstore file``|00001004051200]] to render files manually without activated/running Zettelstore services.
* [[``zettelstore password``|00001004051400]] to calculate data for [[user authentication|00001010040200]].

Every sub-command allows the following command line options:
; [!h|''-h''] (or ''--help'')
: Does not execute the sub-command, but shows allowed command line options (except ''-h'' / ''--help'').
; [!l|''-l LOGSPEC'']
: Makes the given logging level specification effective for this command.
  Details, including syntax, can be found in the description for the [[''log-level''|00001004010000#log-level]] key of the startup configuration.

To measure potential bottlenecks within the software Zettelstore, there are some [[command line flags for profiling the application|00001004059900]].

Changes to docs/manual/00001004051100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20220724162843

=== ``zettelstore run-simple``
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]].
If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.]





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004051100
title: The ''run-simple'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221128161922

=== ``zettelstore run-simple``
This sub-command is implicitly called, when an user starts Zettelstore by double-clicking on its GUI icon.
It is s simplified variant of the [[''run'' sub-command|00001004051000]].

First, this sub-command checks if it can read a [[Zettelstore startup configuration|00001004010000]] file by trying the [[default values|00001004051000#c]].
If this is the case, ''run-simple'' just continues as the [[''run'' sub-command|00001004051000]], but ignores any command line options (including ''-d DIR'').[^This allows a [[curious user|00001003000000]] to become an intermediate user.]

Changes to docs/manual/00001004051200.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
id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20220423131738

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```

; ''-t FORMAT''
: Specifies the output format.
  Supported values are:
  [[''html''|00001012920510]] (default),


  [[''sexpr''|00001012920516]],
  [[''text''|00001012920519]],
  [[''zjson''|00001012920503]],
  and [[''zmk''|00001012920522]].
; ''file-1''
: Specifies the file name, where at least metadata is read.
  If ''file-2'' is not given, the zettel content is also read from here.
; ''file-2''
: File name where the zettel content is stored.

If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin.





>
|











>
>
|

<








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: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230316182711

Reads zettel data from a file (or from standard input / stdin) and renders it to standard output / stdout.
This allows Zettelstore to render files manually.
```
zettelstore file [-t FORMAT] [file-1 [file-2]]
```

; ''-t FORMAT''
: Specifies the output format.
  Supported values are:
  [[''html''|00001012920510]] (default),
  [[''md''|00001012920513]],
  [[''shtml''|00001012920525]],
  [[''sz''|00001012920516]],
  [[''text''|00001012920519]],

  and [[''zmk''|00001012920522]].
; ''file-1''
: Specifies the file name, where at least metadata is read.
  If ''file-2'' is not given, the zettel content is also read from here.
; ''file-2''
: File name where the zettel content is stored.

If neither ''file-1'' nor ''file-2'' are given, metadata and zettel content are read from standard input / stdin.

Changes to docs/manual/00001005090000.zettel.

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: 20220909180240

The following table lists all predefined zettel with their purpose.

|= Identifier :|= Title | Purpose
| [[00000000000001]] | Zettelstore Version | Contains the version string of the running Zettelstore
| [[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






|







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: 20230827212840

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
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]

| [[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/00001006000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001006000000
title: Layout of a Zettel
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20220724165931

A zettel consists of two parts: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.
The zettel content is, well, the actual content.
In many cases, the content is in plain text form.
Plain text is long-lasting.
However, content in binary format is also possible.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006000000
title: Layout of a Zettel
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230403123541

A zettel consists of two parts: the metadata and the zettel content.
Metadata gives some information mostly about the zettel content, how it should be interpreted, how it is sorted within Zettelstore.
The zettel content is, well, the actual content.
In many cases, the content is in plain text form.
Plain text is long-lasting.
However, content in binary format is also possible.
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Other text formats are also supported, like CSS and HTML templates.
Plain text content is always Unicode, encoded as UTF-8.
Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.].
There is support for a graphical format with a text representation: SVG.
And there is support for some binary image formats, like GIF, PNG, and JPEG.

=== Plain, parsed, and evaluated zettel
Zettelstore may present your zettel in various forms.
One way is to present the zettel as it was read by Zettelstore.
This is called ""[[plain zettel|00001003000000#plain]]"", typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''.

The second way is to present the zettel as it was recognized by Zettelstore.
This is called ""[[parsed zettel|00001012053600]]"", typically retrieved with the [[endpoint|00001012920000]] ''/p/{ID}''.
Such a zettel was read and analyzed.
It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.]

However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", typically retrieved with the [[endpoint|00001012920000]] ''/v/{ID}''.
The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel.
It can also be presented in various encoding, including the ""zmk"" encoding.
Evaluations also applies to metadata of a zettel, if appropriate.

Please note, that searching for content is based on parsed zettel.
Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content.
However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel.







|

|


|



|







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Other text formats are also supported, like CSS and HTML templates.
Plain text content is always Unicode, encoded as UTF-8.
Other character encodings are not supported and will never be[^This is not a real problem, since every modern software should support UTF-8 as an encoding.].
There is support for a graphical format with a text representation: SVG.
And there is support for some binary image formats, like GIF, PNG, and JPEG.

=== Plain, parsed, and evaluated zettel
Zettelstore may present your zettel in various forms, typically retrieved with the [[endpoint|00001012920000]] ''/z/{ID}''.
One way is to present the zettel as it was read by Zettelstore.
This is called ""[[plain zettel|00001012053300]]"".

The second way is to present the zettel as it was recognized by Zettelstore.
This is called ""[[parsed zettel|00001012053600]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'', but with the additional query parameter ''parseonly''.
Such a zettel was read and analyzed.
It can be presented in various [[encodings|00001012920500]].[^The [[zmk encoding|00001012920522]] allows you to compare the plain, the parsed, and the evaluated form of a zettel.]

However, a zettel such as this one you are currently reading, is a ""[[evaluated zettel|00001012053500]]"", also retrieved with the [[endpoint|00001012920000]] ''/z/{ID}'' and specifying an encoding.
The biggest difference to a parsed zettel is the inclusion of [[block transclusions|00001007031100]] or [[inline transclusions|00001007040324]] for an evaluated zettel.
It can also be presented in various encoding, including the ""zmk"" encoding.
Evaluations also applies to metadata of a zettel, if appropriate.

Please note, that searching for content is based on parsed zettel.
Transcluded content will only be found in transcluded zettel, but not in the zettel that transcluded the content.
However, you will easily pick up that zettel by follow the [[backward|00001006020000#backward]] metadata key of the transcluded zettel.

Changes to docs/manual/00001006020000.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
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220915181826

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]].

; [!all-tags|''all-tags'']
: A property (a computed values that is not stored) that contains both the value of [[''tags''|#tags]] and the value of [[''content-tags''|#content-tags]].
; [!author|''author'']
: A string value describing the author of a zettel.
  If given, it will be shown in the [[web user interface|00001014000000]] for the zettel.
; [!back|''back'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel.
  Basically, it is the value of [[''backward''|#backward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward|''backward'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata.
  References within invertible values are not included here, e.g. [[''precursor''|#precursor]].
; [!box-number|''box-number'']
: Is a computed value and contains the number of the box where the zettel was found.
  For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]].
; [!content-tags|''content-tags'']
: A property that contains all [[inline tags|00001007040000#tag]] defined within the content.
; [!copyright|''copyright'']
: Defines a copyright string that will be encoded.
  If not given, the value ''default-copyright'' from the  [[configuration zettel|00001004020000#default-copyright]] will be used.
; [!created|''created'']
: Date and time when a zettel was created through Zettelstore.
  If you create a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.







|






<
<












<
<







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: 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'']
: A string value describing the author of a zettel.
  If given, it will be shown in the [[web user interface|00001014000000]] for the zettel.
; [!back|''back'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata, that are not referenced by this zettel.
  Basically, it is the value of [[''backward''|#backward]], but without any zettel identifier that is contained in [[''forward''|#forward]].
; [!backward|''backward'']
: Is a property that contains the identifier of all zettel that reference the zettel of this metadata.
  References within invertible values are not included here, e.g. [[''precursor''|#precursor]].
; [!box-number|''box-number'']
: Is a computed value and contains the number of the box where the zettel was found.
  For all but the [[predefined zettel|00001005090000]], this number is equal to the number __X__ specified in startup configuration key [[''box-uri-__X__''|00001004010000#box-uri-x]].


; [!copyright|''copyright'']
: Defines a copyright string that will be encoded.
  If not given, the value ''default-copyright'' from the  [[configuration zettel|00001004020000#default-copyright]] will be used.
; [!created|''created'']
: Date and time when a zettel was created through Zettelstore.
  If you create a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

44
45
46
47
48
49
50







51
52


53
54
55
56
57
58
59
; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead|''dead'']
: Property that contains all references that does __not__ identify a zettel.







; [!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.







>
>
>
>
>
>
>


>
>







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
; [!credential|''credential'']
: Contains the hashed password, as it was emitted by [[``zettelstore password``|00001004051400]].
  It is internally created by hashing the password, the [[zettel identifier|00001006050000]], and the value of the ''ident'' key.

  It is only used for zettel with a ''role'' value of ""user"".
; [!dead|''dead'']
: Property that contains all references that does __not__ identify a zettel.
; [!expire|''expire'']
: A user-entered time stamp that document the point in time when the zettel should expire.
  When a zettel is expires, Zettelstore does nothing.
  It is up to you to define required actions.
  ''expire'' is just a documentation.
  You could define a query and execute it regularly, for example [[query:expire? ORDER expire]].
  Alternatively, a Zettelstore client software could define some actions when it detects expired zettel.
; [!folge|''folge'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''precursor''|#precursor]] value.
; [!folge-role|''folge-role'']
: Specifies a suggested [[''role''|#role]] the zettel should use in the future, if zettel currently has a preliminary role.
; [!forward|''forward'']
: Property that contains all references that identify another zettel within the content of the zettel.
; [!id|''id'']
: Contains the [[zettel identifier|00001006050000]], as given by the Zettelstore.
  It cannot be set manually, because it is a computed value.
; [!lang|''lang'']
: Language for the zettel.
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
  If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

  This is a computed value.
  There is no need to set it via Zettelstore.
; [!precursor|''precursor'']
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].




; [!published|''published'']
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value.
  Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.

  It can be used for [[sorting|00001007700000]] zettel based on their publication date.

  It is a computed value.
  There is no need to set it via Zettelstore.



; [!read-only|''read-only'']
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.














; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.

  You can use all [[inline-structured elements|00001007040000]] of Zettelmarkup.
; [!url|''url'']
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].







>
>
>
>











>
>
>








>
>
>
>
>
>
>
>
>
>
>
>
>
>










<
<







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
  If you edit a zettel with an editor software outside Zettelstore, you should set it manually to an appropriate value.

  This is a computed value.
  There is no need to set it via Zettelstore.
; [!precursor|''precursor'']
: References zettel for which this zettel is a ""Folgezettel"" / follow-up zettel.
  Basically the inverse of key [[''folge''|#folge]].
; [!predecessor|''predecessor'']
: References the zettel that contains a previous version of the content.
  In contrast to [[''precursor''|#precurso]] / [[''folge''|#folge]], this is a reference because of technical reasons, not because of content-related reasons.
  Basically the inverse of key [[''successors''|#successors]].
; [!published|''published'']
: This property contains the timestamp of the mast modification / creation of the zettel.
  If [[''modified''|#modified]] is set with a valid timestamp, it contains the its value.
  Otherwise, if [[''created''|#created]] is set with a valid timestamp, it contains the its value.
  Otherwise, if the zettel identifier contains a valid timestamp, the identifier is used.
  In all other cases, this property is not set.

  It can be used for [[sorting|00001007700000]] zettel based on their publication date.

  It is a computed value.
  There is no need to set it via Zettelstore.
; [!query|''query'']
: Stores the [[query|00001007031140]] that was used to create the zettel.
  This is for future reference.
; [!read-only|''read-only'']
: Marks a zettel as read-only.
  The interpretation of [[supported values|00001006020400]] for this key depends, whether authentication is [[enabled|00001010040100]] or not.
; [!role|''role'']
: Defines the role of the zettel.
  Can be used for selecting zettel.
  See [[supported zettel roles|00001006020100]].
  If not given, it is ignored.
; [!subordinates|''subordinates'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''superior''|#superior]] value.
; [!successors|''successors'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''predecessor''|#predecessor]] value.
  Therefore, it references all zettel that contain a new version of the content and/or metadata.
  In contrast to [[''folge''|#folge]], these are references because of technical reasons, not because of content-related reasons.
  In most cases, zettel referencing the current zettel should be updated to reference a successor zettel.
  The [[query reference|00001007040310]] [[query:backward? successors?]] lists all such zettel.
; [!summary|''summary'']
: Summarizes the content of the zettel.
  You may use all [[inline-structued elements|00001007040000]] of Zettelmarkup.
; [!superior|''superior'']
: Specifies a zettel that is conceptually a superior zettel.
  This might be a more abstract zettel, or a zettel that should be higher in a hierarchy.
; [!syntax|''syntax'']
: Specifies the syntax that should be used for interpreting the zettel.
  The zettel about [[other markup languages|00001008000000]] defines supported values.
  If it is not given, it defaults to ''plain''.
; [!tags|''tags'']
: Contains a space separated list of tags to describe the zettel further.
  Each Tag must begin with the number sign character (""''#''"", U+0023).
; [!title|''title'']
: Specifies the title of the zettel.
  If not given, the value of [[''id''|#id]] will be used.


; [!url|''url'']
: Defines an URL / URI for this zettel that possibly references external material.
  One use case is to specify the document that the current zettel comments on.
  The URL will be rendered special in the [[web user interface|00001014000000]] if you use the default template.
; [!useless-files|''useless-files'']
: Contains the file names that are rejected to serve the content of a zettel.
  Is used for [[directory boxes|00001004011400]] and [[file boxes|00001004011200#file]].

Changes to docs/manual/00001006020100.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: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

modified: 20220623183234

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is only used in this specific Zettelstore.




If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles:

; [!note|''note'']
: A small note, to remember something.
  Notes are not real zettel, they just help to create a real zettel.
  Think of them as Post-it notes.





>
|













>
>
>







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: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 00010101000000
modified: 20230829233016

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is only used in this specific Zettelstore.
; [!tag|''tag'']
: A zettel with the role ""tag"" and a title, which names a [[tag|00001006020000#tags]], is treated as a __tag zettel__.
  Basically, tag zettel describe the tag, and form a hierarchiy of meta-tags.

If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles:

; [!note|''note'']
: A small note, to remember something.
  Notes are not real zettel, they just help to create a real zettel.
  Think of them as Post-it notes.

Changes to docs/manual/00001006030000.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
id: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

modified: 20220304114106

All [[supported metadata keys|00001006020000]] conform to a type.

User-defined metadata keys conform also to a type, based on the suffix of the key.

|=Suffix|Type

| ''-number'' | [[Number|00001006033000]]
| ''-role'' | [[Word|00001006035500]]
| ''-set'' | [[WordSet|00001006036000]]

| ''-title'' | [[Zettelmarkup|00001006036500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-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 term when selecting some zettel.
And there is a rule how values compare for sorting.

* [[Credential|00001006031000]]
* [[EString|00001006031500]]
* [[Identifier|00001006032000]]
* [[IdentifierSet|00001006032500]]
* [[Number|00001006033000]]





>
|






>



>










|







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
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]]
| ''-number'' | [[Number|00001006033000]]
| ''-role'' | [[Word|00001006035500]]
| ''-set'' | [[WordSet|00001006036000]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-title'' | [[Zettelmarkup|00001006036500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-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/00001006031000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006031000
title: Credential Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130324

Values of this type denote a credential value, e.g. an encrypted password.

=== Allowed values
All printable characters are allowed.
Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore.

=== Query operators
A credential never compares to any other value.
A comparison will never match in any way.

=== Sorting
If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead.






|







|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006031000
title: Credential Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175515

Values of this type denote a credential value, e.g. an encrypted password.

=== Allowed values
All printable characters are allowed.
Since a credential contains some kind of secret, the sequence of characters might have some hidden syntax to be interpreted by other parts of Zettelstore.

=== Query comparison
A credential never compares to any other value.
A comparison will never match in any way.

=== Sorting
If a list of zettel should be sorted based on a credential value, the identifier of the respective zettel is used instead.

Changes to docs/manual/00001006031500.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
id: 00001006031500
title: EString Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130448

Values of this type are just a sequence of character, possibly an empty sequence.

An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].]

=== Allowed values
All printable characters are allowed.

=== Query operator
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.






|








|









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
id: 00001006031500
title: EString Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175525

Values of this type are just a sequence of character, possibly an empty sequence.

An EString is the most general metadata key type, as it places no restrictions to the character sequence.[^Well, there are some minor restrictions that follow from the [[metadata syntax|00001006010000]].]

=== Allowed values
All printable characters are allowed.

=== Query comparison
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006032000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15









16
17
18
19
20
21
id: 00001006032000
title: Identifier Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914134914

Values of this type denote a [[zettel identifier|00001006050000]].

=== Allowed values
Must be a sequence of 14 digits (""0""--""9"").

=== Query operator

Comparison is done with the string representation of the identifiers.









For example, ""000010"" matches ""[[00001006032000]]"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

If both values are identifiers, this works well because both have the same length.






|






|
>
|
>
>
>
>
>
>
>
>
>






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/00001006032500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001006032500
title: IdentifierSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914131354

Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]].

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters.

=== Query operator
A value matches an identifier set value, if the value matches any of the identifier set values.

For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.






|








|






1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001006032500
title: IdentifierSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175551

Values of this type denote a (sorted) set of [[zettel identifier|00001006050000]].

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Must be at least one sequence of 14 digits (""0""--""9""), separated by space characters.

=== Query comparison
A value matches an identifier set value, if the value matches any of the identifier set values.

For example, ""000010060325"" is a prefix ""[[00001006032000]] [[00001006032500]]"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006033000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14




15


16
17
18
id: 00001006033000
title: Number Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914131211

Values of this type denote a numeric integer value.

=== Allowed values
Must be a sequence of digits (""0""--""9""), optionally prefixed with a ""-"" or a ""+"" character.

=== Query operator




All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"".



=== Sorting
Sorting is done by comparing the numeric values.






|






|
>
>
>
>
|
>
>



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 &hellip; 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/00001006033500.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
id: 00001006033500
title: String Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130505

Values of this type are just a sequence of character, but not an empty sequence.

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Query operator
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.






|







|









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: 00001006033500
title: String Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175633

Values of this type are just a sequence of character, but not an empty sequence.

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Query comparison
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001006034000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001006034000
title: TagSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914131048

Values of this type denote a (sorted) set of tags.

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character.
Tags are separated by space characters.

All characters are mapped to their lower case values.

=== Query operator
All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.






|











|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001006034000
title: TagSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175642

Values of this type denote a (sorted) set of tags.

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Every tag must must begin with the number sign character (""''#''"", U+0023), followed by at least one printable character.
Tags are separated by space characters.

All characters are mapped to their lower case values.

=== Query comparison
All comparisons are done case-sensitive, i.e. ""#hell"" will not be the prefix of ""#Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006034500.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
id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130919

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 operator





All comparisons assume that up to 14 digits are given.




=== 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.






|













|
>
>
>
>
>
|
>
>
>





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/00001006035000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001006035000
title: URL Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130809

Values of this type denote an URL.

=== Allowed values
All characters of an URL / URI are allowed.

=== Query operator
All comparisons are done case-insensitive.
For example, ""hello"" is the suffix of ""http://example.com/Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.






|






|





1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id: 00001006035000
title: URL Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175725

Values of this type denote an URL.

=== Allowed values
All characters of an URL / URI are allowed.

=== Query comparison
All comparisons are done case-insensitive.
For example, ""hello"" is the suffix of ""http://example.com/Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006035500.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006035500
title: Word Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130655

Values of this type denote a single word.

=== Allowed values
Must be a non-empty sequence of characters, but without the space character.

All characters are mapped to their lower case values.

=== Query operator
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.






|








|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006035500
title: Word Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175735

Values of this type denote a single word.

=== Allowed values
Must be a non-empty sequence of characters, but without the space character.

All characters are mapped to their lower case values.

=== Query comparison
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006036000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006036000
title: WordSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914130725

Values of this type denote a (sorted) set of [[words|00001006035500]].

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Must be a sequence of at least one word, separated by space characters.

=== Query operator
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.






|








|




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
id: 00001006036000
title: WordSet Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175745

Values of this type denote a (sorted) set of [[words|00001006035500]].

A set is different to a list, as no duplicate values are allowed.

=== Allowed values
Must be a sequence of at least one word, separated by space characters.

=== Query comparison
All comparisons are done case-insensitive, i.e. ""hell"" will be the prefix of ""World, Hello"".

=== Sorting
Sorting is done by comparing the [[String|00001006033500]] values.

Changes to docs/manual/00001006036500.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
id: 00001006036500
title: Zettelmarkup Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220914135405

Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]].

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Query operator
Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers.
Letters are mapped to the corresponding lower-case value.

For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.






|







|












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
id: 00001006036500
title: Zettelmarkup Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20230419175441

Values of this type are [[String|00001006033500]] values, interpreted as [[Zettelmarkup|00001007000000]].

=== Allowed values
All printable characters are allowed.
There must be at least one such character.

=== Query comparison
Comparison is done similar to the full-text search: both the value to compare and the metadata value are normalized according to Unicode NKFD, ignoring everything except letters and numbers.
Letters are mapped to the corresponding lower-case value.

For example, ""Brücke"" will be the prefix of ""(Bruckenpfeiler,"".

=== Sorting
To sort two values, the underlying encoding is used to determine which value is less than the other.

Uppercase letters are typically interpreted as less than their corresponding lowercase letters, i.e. ``A < a``.

Comparison is done character-wise by finding the first difference in the respective character sequence.
For example, ``abc > aBc``.

Changes to docs/manual/00001007000000.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

id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220913135505

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.

Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages.
Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging.
[[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.

Zettelmarkup is a markup language on its own.
This is in contrast to Markdown, which is basically a super-set of HTML.
While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX.
Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript.
This could create problems with longevity as well as security problems.

Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content.
It allows embedding other content, simple tables, quotations, description lists, and images.
It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text.
Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys.


Zettelmarkup might be seen as a proprietary markup language.
But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions.
However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds.

* [[General principles|00001007010000]]
* [[Basic definitions|00001007020000]]
* [[Block-structured elements|00001007030000]]
* [[Inline-structured element|00001007040000]]
* [[Attributes|00001007050000]]
* [[Query expressions|00001007700000]]
* [[Summary of formatting characters|00001007800000]]
* [[Tutorial|00001007900000]]







|











|








>













>
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
id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221209192105

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.

Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages.
Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging.
[[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.

Zettelmarkup is a markup language on its own.
This is in contrast to Markdown, which is basically a super-set of HTML: every HTML document is a valid Markdown document.[^To be precise: the content of the ``<body>`` of each HTML document is a valid Markdown document.]
While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX.
Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript.
This could create problems with longevity as well as security problems.

Zettelmarkup is a rich markup language, but it focuses on relatively short zettel content.
It allows embedding other content, simple tables, quotations, description lists, and images.
It provides a broad range of inline formatting, including __emphasized__, **strong**, ~~deleted~~{-} and >>inserted>> text.
Footnotes[^like this] are supported, links to other zettel and to external material, as well as citation keys.
Zettelmarkup allows to include content from other zettel and to embed the result of a search query.

Zettelmarkup might be seen as a proprietary markup language.
But if you want to use [[Markdown/CommonMark|00001008010000]] and you need support for footnotes or tables, you'll end up with proprietary extensions.
However, the Zettelstore supports CommonMark as a zettel syntax, so you can mix both Zettelmarkup zettel and CommonMark zettel in one store to get the best of both worlds.

* [[General principles|00001007010000]]
* [[Basic definitions|00001007020000]]
* [[Block-structured elements|00001007030000]]
* [[Inline-structured element|00001007040000]]
* [[Attributes|00001007050000]]
* [[Query expressions|00001007700000]]
* [[Summary of formatting characters|00001007800000]]
* [[Tutorial|00001007900000]]
* [[Cheat Sheet|00001007990000]]

Changes to docs/manual/00001007030900.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130330

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to JSON, the comment block will not be ignored but it will output some JSON text.
Same for other renderer.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.






>
|








|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001007030900
title: Zettelmarkup: Comment Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170858

Comment blocks are quite similar to [[verbatim blocks|00001007030500]]: both are used to enter text that should not be interpreted.
While the text entered inside a verbatim block will be processed somehow, text inside a comment block will be ignored[^Well, not completely ignored: text is read, but it will typically not rendered visible.].
Comment blocks are typically used to give some internal comments, e.g. the license of a text or some internal remarks.

Comment blocks begin with at least three percent sign characters (""''%''"", U+0025) at the first position of a line.
You can add some [[attributes|00001007050000]] on the beginning line of a comment block, following the initiating characters.
The comment block supports the default attribute: when given, the text will be rendered, e.g. as an HTML comment.
When rendered to a symbolic expression, the comment block will not be ignored but it will output some text.
Same for other renderer.

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some percent sign characters in the text that should not be interpreted.

Changes to docs/manual/00001007031100.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
id: 00001007031100
title: Zettelmarkup: Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220131151022
modified: 20220913135545

A transclusion allows to include the content of other zettel into the current zettel.

The transclusion specification begins with three consecutive left curly bracket characters (""''{''"", U+007B) at the first position of a line and ends with three consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimit either a [[zettel identifier|00001006050000]] or a searched zettel list.
You can add some [[attributes|00001007050000]], although a transclusion does not support the default attribute.
Any other characters in this line will be ignored.

This leads to two variants of transclusion:
# Transclusion of the content of another zettel into the current zettel.
  This is done if you specify a zettel identifier, and is called ""zettel transclusion"".
# Transclusion of the list of zettel references that satisfy a [[query expression|00001007700000]].
  This is called ""query transclusion"".


The variants are described on separate zettel:
* [[Zettel transclusion|00001007031110]]
* [[Query transclusion|00001007031140]]






|








|

|

|
>

|


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
id: 00001007031100
title: Zettelmarkup: Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220131151022
modified: 20220922155031

A transclusion allows to include the content of other zettel into the current zettel.

The transclusion specification begins with three consecutive left curly bracket characters (""''{''"", U+007B) at the first position of a line and ends with three consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimit either a [[zettel identifier|00001006050000]] or a searched zettel list.
You can add some [[attributes|00001007050000]], although a transclusion does not support the default attribute.
Any other characters in this line will be ignored.

This leads to three variants of transclusions:
# Transclusion of the content of another zettel into the current zettel.
  This is done if you specify a zettel identifier, and is called __zettel transclusion__.
# Transclusion of the list of zettel references that satisfy a [[query expression|00001007700000]].
  This is called __query transclusion__.
# Transclusion of the content of an image, referenced by a [[hosted or based|00001007040310#link-specifications]] link / URL.

The first two variants are described on separate zettel:
* [[Zettel transclusion|00001007031110]]
* [[Query transclusion|00001007031140]]

Changes to docs/manual/00001007031110.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
id: 00001007031110
title: Zettelmarkup: Zettel Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20220825190116

A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''.

When evaluated, the referenced zettel is read.
If it contains some transclusions itself, these will be expanded, recursively.
When a recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

An error message is also given, if the zettel cannot be read or if too many transclusions are made.
The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

If everything went well, the referenced, expanded zettel will replace the transclusion element.

For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclude element:
```zmk
{{{00001006050000}}}
```
This will result in:
:::zs-example
{{{00001006050000}}}
:::

Please note: if the referenced zettel is changed, all transclusions will also change.

This allows, for example, to create a bigger document just by transcluding smaller 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
id: 00001007031110
title: Zettelmarkup: Zettel Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20220926183331

A zettel transclusion is specified by the following sequence, starting at the first position in a line: ''{{{zettel-identifier}}}''.

When evaluated, the referenced zettel is read.
If it contains some transclusions itself, these will be expanded, recursively.
When a recursion is detected, expansion does not take place.
Instead an error message replaces the transclude specification.

An error message is also given, if the zettel cannot be read or if too many transclusions are made.
The maximum number of transclusion can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

If everything went well, the referenced, expanded zettel will replace the transclusion element.

For example, to include the text of the Zettel titled ""Zettel identifier"", just specify its identifier [[''00001006050000''|00001006050000]] in the transclude element:
```zmk
{{{00001006050000}}}
```
This will result in:
:::example
{{{00001006050000}}}
:::

Please note: if the referenced zettel is changed, all transclusions will also change.

This allows, for example, to create a bigger document just by transcluding smaller zettel.

Changes to docs/manual/00001007031140.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
45
46


47
48
49
50

51
52
53
54
55
56
57
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20220913145104

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.

For example, to include the list of all zettel with the [[all-tags|00001006020000#all-tags]] ""#search"", ordered by title specify the following query transclude element:
```zmk
{{{query:all-tags:#search ORDER title}}}
```
This will result in:
:::zs-example
{{{query:all-tags:#search ORDER title}}}
:::

For example, this allows to create a dynamic list of zettel inside a zettel, maybe to provide some introductory text followed by a list of child zettel.

The query will deliver only those zettel, which the current user is allowed to read.

In the above example, the action list is empty.
This leads to the described list of zettel.

The following actions are supported, parameter and aggregate actions:
; ''N'' (or any word that starts with ""''N''"" (parameter)
: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''RSS'' action.



; ''RSS'' (aggregate)
: Transform the zettel list into an [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document.
  The document is embedded into the referencing zettel.


; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.


```zmk
{{{query:all-tags:#search | all-tags}}}
```
This in a tag cloud of all tags that are used together with the tag #search:
:::zs-example
{{{query:all-tags:#search | all-tags}}}
:::






|








|

|


|
|




















|
>
>
>

|

>
>


|

>

|

|
|
|

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: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20230116183656

A query transclusion is specified by the following sequence, starting at the first position in a line: ''{{{query:query-expression}}}''.
The line must literally start with the sequence ''{{{query:''.
Everything after this prefix is interpreted as a [[query expression|00001007700000]].

When evaluated, the query expression is evaluated, often resulting in a list of [[links|00001007040310]] to zettel, matching the query expression.
The result replaces the query transclusion element.

For example, to include the list of all zettel with the [[tags|00001006020000#tags]] ""#search"", ordered by title specify the following query transclude element:
```zmk
{{{query:tags:#search ORDER title}}}
```
This will result in:
:::example
{{{query:tags:#search ORDER title}}}
:::

For example, this allows to create a dynamic list of zettel inside a zettel, maybe to provide some introductory text followed by a list of child zettel.

The query will deliver only those zettel, which the current user is allowed to read.

In the above example, the action list is empty.
This leads to the described list of zettel.

The following actions are supported, parameter and aggregate actions:
; ''N'' (or any word that starts with ""''N''"" (parameter)
: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''ATOM'' and ''RSS'' action.
; ''ATOM'' (aggregate)
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case[^Except if the key name collides with one of the above names. In this case use at least one lower case letter.].

Example:
```zmk
{{{query:tags:#search | tags}}}
```
This is a tag cloud of all tags that are used together with the tag #search:
:::example
{{{query:tags:#search | tags}}}
:::

Changes to docs/manual/00001007031200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007031200
title: Zettelmarkup: Inline-Zettel Block
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311112247

An inline-zettel block allows to specify some content with another syntax without creating a new zettel.
This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.
Another example is to specify HTML code to use it for some kind of web front-end framework.

As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031200
title: Zettelmarkup: Inline-Zettel Block
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220201142439
modified: 20221018121251

An inline-zettel block allows to specify some content with another syntax without creating a new zettel.
This is useful, for example, if you want to embed some [[Markdown|00001008010500]] content, because you are too lazy to translate Markdown into Zettelmarkup.
Another example is to specify HTML code to use it for some kind of web front-end framework.

As all other [[line-range blocks|00001007030000#line-range-blocks]], an inline-zettel block begins with at least three identical characters, starting at the first position of a line.
For inline-zettel blocks, the at-sign character (""''@''"", U+0040) is used.
31
32
33
34
35
36
37

38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
will be rendered as:
:::example
@@@markdown
A link to [this](00001007031200) zettel.
@@@
:::


Using HTML:
```zmk
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
```
will a section heading of level 1, which is not allowed within Zettelmarkup:
:::example
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
:::
:::note







>
|






|







32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
will be rendered as:
:::example
@@@markdown
A link to [this](00001007031200) zettel.
@@@
:::

If you have set [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"", the following markup is not ignored:

```zmk
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
```
will render a section heading of level 1, which is not allowed within Zettelmarkup:
:::example
@@@html
<h1>H1 Heading</h1>
Alea iacta est
@@@
:::
:::note

Changes to docs/manual/00001007031300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
id: 00001007031300
title: Zettelmarkup: Evaluation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311120658

Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software.
They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".]
The main reason for an evaluation block is to be used with external software via the [[ZJSON encoding|00001012920503]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some tilde characters in the text that should not be interpreted.

For example:





>
|









|







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: 00001007031300
title: Zettelmarkup: Evaluation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220310184916
modified: 20230109105402

Evaluation blocks are used to enter text that could be evaluated by either Zettelstore or external software.
They begin with at least three tilde characters (""''~''"", U+007E) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
The evaluation block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Not all syntax values are supported by Zettelstore.[^Currently just ""[[draw|00001008050000]]"".]
The main reason for an evaluation block is to be used with external software via the [[sz encoding|00001012920516]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some tilde characters in the text that should not be interpreted.

For example:

Changes to docs/manual/00001007031400.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
id: 00001007031400
title: Zettelmarkup: Math-mode Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311182505

Math-mode blocks are used to enter mathematical formulas / equations in a display style mode.
Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software.
They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax.
Not all syntax values are supported by Zettelstore.[^Currently: none.]
External software might support several values via the [[ZJSON encoding|00001012920503]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some dollar-sign characters in the text that should not be interpreted.

For example:





>
|











|







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: 00001007031400
title: Zettelmarkup: Math-mode Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20220311173226
modified: 20230109105340

Math-mode blocks are used to enter mathematical formulas / equations in a display style mode.
Similar to a [[evaluation blocks|00001007031300]], the block content will be interpreted by either Zettelstore or an external software.
They begin with at least three dollar sign characters (""''$''"", U+0024) at the first position of a line.

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.
A math-mode block supports the default attribute[^Depending on the syntax value.]: when given, all spaces in the text are rendered in HTML as open box characters (U+2423).
If you want to give only one attribute and this attribute is the generic attribute, you can omit the most of the attribute syntax and just specify the value.
It will be interpreted as a [[syntax|00001008000000]] value to evaluate its content.
Alternatively, you could provide an attribute with the key ""syntax"" and use the value to specify the syntax.
Not all syntax values are supported by Zettelstore.[^Currently: none.]
External software might support several values via the [[sz encoding|00001012920516]].

Any other character in this line will be ignored

Text following the beginning line will not be interpreted, until a line begins with at least the same number of the same characters given at the beginning line.
This allows to enter some dollar-sign characters in the text that should not be interpreted.

For example:

Changes to docs/manual/00001007040000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220913144717

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

; Text formatting






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007040000
title: Zettelmarkup: Inline-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220920143243

Most characters you type is concerned with inline-structured elements.
The content of a zettel contains is many cases just ordinary text, lightly formatted.
Inline-structured elements allow to format your text and add some helpful links or images.
Sometimes, you want to enter characters that have no representation on your keyboard.

; Text formatting
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
==== Backslash
The backslash character (""''\\''"", U+005C) gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (U+00A0).
* If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a [[footnote text|00001007040330]], you should escape it with a backslash.

==== Tag
Any text that begins with a number sign character (""''#''"", U+0023), followed by a non-empty sequence of Unicode letters, Unicode digits, the hyphen-minus character (""''-''"", U+002D), or the low line character (""''_''"", U+005F) is interpreted as an __inline tag__.
They are be considered equivalent to tags in metadata.

**This element is deprecated in version 0.7 and will be removed in version 0.8!**

The use of inline tags is problematic, because:
* The number sign is often used as, well, a number sign, esp. in the English language.
  This introduces unintended tags.
* An inline tag is rendered in HTML as a link.
  However, an inline tag may be the contained in the text part of a [[link element|00001007040310]].
  This will produce a HTML link within a HTML link.
* Similar, an inline tag may be part of the title of a zettel.
  When a zettel list is rendered in HTML, this also produces a HTML link for each zettel, which contains the inline tag HTML link.
* The naming of metadata, [[''tags''|00001006020000#tags]] (names the tags within the metadata section of a zettel), [[''content-tags''|00001006020000#content-tags]] (all inline tags), and [[''all-tags''|00001006020000#all-tags]], is confusing for some users.
  For example, if you follow the link of a tag, it is converted into a [[query|00001007700000]] for the key ''all-tags''.
  A search for ''tags'' will most likely produce different results.

To find all zettel with inline tags, please use the query [[::query:content-tags?::|query:content-tags?]].
There are two, non-exclusive options for migration:
# Move inline tags into the metadata section of a zettel, under the key ''tags''.
  This will allow you to find the zettel via a search for tags in the future.
# Replace the inline tag with a link to a search for that tag: ``#TAG`` could be replace with ``[[#TAG|query:tags:TAG]]``, where ''TAG'' is the placeholder for the actual tag.

==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always begins with an ampersand character (""''&''"", U+0026) and ends with a semicolon character (""'';''"", U+003B).
If you know the HTML name of the character you want to enter, put it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character.
Example: ``&#x26;`` is rendered in HTML as ::&#x26;::{=example}.




Since some Unicode character are used quite often, a special notation is introduced for them:

* Two consecutive hyphen-minus characters result in an __en-dash__ character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<














>
>
>






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
==== Backslash
The backslash character (""''\\''"", U+005C) gives the next character another meaning.
* If a space character follows, it is converted in a non-breaking space (U+00A0).
* If a line ending follows the backslash character, the line break is converted from a __soft break__ into a __hard break__.
* Every other character is taken as itself, but without the interpretation of a Zettelmarkup element.
  For example, if you want to enter a ""'']''"" into a [[footnote text|00001007040330]], you should escape it with a backslash.

























==== Entities & more
Sometimes it is not easy to enter special characters.
If you know the Unicode code point of that character, or its name according to the [[HTML standard|https://html.spec.whatwg.org/multipage/named-characters.html]], you can enter it by number or by name.

Regardless which method you use, an entity always begins with an ampersand character (""''&''"", U+0026) and ends with a semicolon character (""'';''"", U+003B).
If you know the HTML name of the character you want to enter, put it between these two character.
Example: ``&amp;`` is rendered as ::&amp;::{=example}.

If you want to enter its numeric code point, a number sign character must follow the ampersand character, followed by digits to base 10.
Example: ``&#38;`` is rendered in HTML as ::&#38;::{=example}.

You also can enter its numeric code point as a hex number, if you put the letter ""x"" after the numeric sign character.
Example: ``&#x26;`` is rendered in HTML as ::&#x26;::{=example}.

According to the [[HTML Standard|https://html.spec.whatwg.org/multipage/syntax.html#character-references]], some numeric code points are not allowed.
These are all code point below the numeric value 32 (decimal) or 0x20 (hex) and all code points for [[noncharacter|https://infra.spec.whatwg.org/#noncharacter]] values.

Since some Unicode character are used quite often, a special notation is introduced for them:

* Two consecutive hyphen-minus characters result in an __en-dash__ character.
  It is typically used in numeric ranges.
  ``pages 4--7`` will be rendered in HTML as: ::pages 4--7::{=example}.
  Alternative specifications are: ``&ndash;``, ``&x8211``, and ``&#x2013``.

Changes to docs/manual/00001007040310.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: 00001007040310
title: Zettelmarkup: Links
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220913144754

There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D).


The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``.
The text is a sequence of [[inline elements|00001007040000]].
However, it should not contain links itself.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

=== Link specifications
The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]].
The resulting reference is called ""query reference"".
When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression.

A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character,
will be interpreted as a local reference, called ""hosted reference"".
Such references will be interpreted relative to the web server hosting the Zettelstore.

If a link specification begins with two slash characters, it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].

=== Other topics
If the link references another zettel, and this zettel is not readable for the current user, because of a missing access rights, then only the associated text is presented.






|



>


















|


|





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
id: 00001007040310
title: Zettelmarkup: Links
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20221024173849

There are two kinds of links, regardless of links to (internal) other zettel or to (external) material.
Both kinds begin with two consecutive left square bracket characters (""''[''"", U+005B) and ends with two consecutive right square bracket characters (""'']''"", U+005D).
If the content starts with more than two left square bracket characters, all but the last two will be treated as text.

The first form provides some text plus the link specification, delimited by a vertical bar character (""''|''"", U+007C): ``[[text|linkspecification]]``.
The text is a sequence of [[inline elements|00001007040000]].
However, it should not contain links itself.

The second form just provides a link specification between the square brackets.
Its text is derived from the link specification, e.g. by interpreting the link specification as text: ``[[linkspecification]]``.

=== Link specifications
The link specification for another zettel within the same Zettelstore is just the [[zettel identifier|00001006050000]].
To reference some content within a zettel, you can append a number sign character (""''#''"", U+0023) and the name of the mark to the zettel identifier.
The resulting reference is called ""zettel reference"".

If the link specification begins with the string ''query:'', the text following this string will be interpreted as a [[query expression|00001007700000]].
The resulting reference is called ""query reference"".
When this type of references is rendered, it will typically reference a list of all zettel that fulfills the query expression.

A link specification starting with one slash character (""''/''"", U+002F), or one or two full stop characters (""''.''"", U+002E) followed by a slash character,
will be interpreted as a local reference, called __hosted reference__.
Such references will be interpreted relative to the web server hosting the Zettelstore.

If a link specification begins with two slash characters (called __based reference__), it will be interpreted relative to the value of [[''url-prefix''|00001004010000#url-prefix]].

To specify some material outside the Zettelstore, just use an normal Uniform Resource Identifier (URI) as defined by [[RFC\ 3986|https://tools.ietf.org/html/rfc3986]].

=== Other topics
If the link references another zettel, and this zettel is not readable for the current user, because of a missing access rights, then only the associated text is presented.

Changes to docs/manual/00001007040320.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: 00001007040320
title: Zettelmarkup: Inline Embedding / Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220803183936

To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]].
Both contain a reference specification and optionally some text.
In contrast to a link, the specification of embedded material must currently resolve to some kind of real content.
This content replaces the embed specification.

An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link.


One difference to a link: if the text was not given, an empty string is assumed.

The reference must point to some content, either zettel content or URL-referenced content.
If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored.
If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint:
 
Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.

There are two kind of content:
# [[image content|00001007040322]],
# [[textual content|00001007040324]].





>
|








>












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
id: 00001007040320
title: Zettelmarkup: Inline Embedding / Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20221024173926

To some degree, an specification for embedded material is conceptually not too far away from a specification for [[linked material|00001007040310]].
Both contain a reference specification and optionally some text.
In contrast to a link, the specification of embedded material must currently resolve to some kind of real content.
This content replaces the embed specification.

An embed specification begins with two consecutive left curly bracket characters (""''{''"", U+007B) and ends with two consecutive right curly bracket characters (""''}''"", U+007D).
The curly brackets delimits either a reference specification or some text, a vertical bar character and the link specification, similar to a link.
If the content starts with more than two left curly bracket characters, all but the last two will be treated as text.

One difference to a link: if the text was not given, an empty string is assumed.

The reference must point to some content, either zettel content or URL-referenced content.
If the current user is not allowed to read the referenced zettel, the inline transclusion / embedding is ignored.
If the referenced zettel does not exist, or is not readable because of other reasons, a [[spinning emoji|00000000040001]] is presented as a visual hint:
 
Example: ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.

There are two kind of content:
# [[image content|00001007040322]],
# [[textual content|00001007040324]].

Changes to docs/manual/00001007040322.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: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220214180955

Image content is assumed, if an URL is used or if the referenced zettel contains an image.

Supported formats are:

* Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]].
* Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]].
* JPEG / JPG, defined by the __Joint Photographic Experts Group__.
* Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]]


If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities.

[[Attributes|00001007050000]] are supported.
They must follow the last right curly bracket character immediately.
One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML:

Examples:
* [!spin|``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}``] is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}.
* The above image is also the placeholder for a non-existent zettel:
** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}.





>
|









>











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
id: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20221112111054

Image content is assumed, if an URL is used or if the referenced zettel contains an image.

Supported formats are:

* Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]].
* Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]].
* JPEG / JPG, defined by the __Joint Photographic Experts Group__.
* Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]]
* WebP, defined by [[Google|https://developers.google.com/speed/webp]]

If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities.

[[Attributes|00001007050000]] are supported.
They must follow the last right curly bracket character immediately.
One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML:

Examples:
* [!spin|``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}``] is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}.
* The above image is also the placeholder for a non-existent zettel:
** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}.

Changes to docs/manual/00001007040324.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007040324
title: Zettelmarkup: Inline-mode Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311110814

Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content.
For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]).

Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements.

First, the referenced zettel is read.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007040324
title: Zettelmarkup: Inline-mode Transclusion
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20221116165428

Inline-mode transclusion applies to all zettel that are parsed in a non-trivial way, e.g. as structured textual content.
For example, textual content is assumed if the [[syntax|00001006020000#syntax]] of a zettel is ""zmk"" ([[Zettelmarkup|00001007000000]]), or ""markdown"" / ""md"" ([[Markdown|00001008010000]]).

Since this type of transclusion is at the level of [[inline-structured elements|00001007040000]], the transclude specification must be replaced with some inline-structured elements.

First, the referenced zettel is read.
30
31
32
33
34
35
36




37
38
39
40
41
42
43
44
** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used.
   Initial spaces and line breaks are ignored in this case.

   Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}.

** Just specifying the fragment identifier will reference something in the current page.
   This is not allowed, to prevent a possible endless recursion.





If no inline-structured elements are found, the transclude specification is replaced by an error message.

To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited.
The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

=== See also
[[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]].







>
>
>
>








31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
** In case the fragment names a [[mark|00001007040350]], the inline-structured elements after the mark are used.
   Initial spaces and line breaks are ignored in this case.

   Example: ``{{00001007040322#spin}}`` is rendered as ::{{00001007040322#spin}}::{=example}.

** Just specifying the fragment identifier will reference something in the current page.
   This is not allowed, to prevent a possible endless recursion.

* If the reference is a [[hosted or based|00001007040310#link-specifications]] link / URL to an image, that image will be rendered.

  Example: ``{{//z/00000000040001}}`` is rendered as ::{{//z/00000000040001}}::{=example}

If no inline-structured elements are found, the transclude specification is replaced by an error message.

To avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]] (also known as ""XML bomb""), the total number of transclusions / expansions is limited.
The limit can be controlled by setting the value [[''max-transclusions''|00001004020000#max-transclusions]] of the runtime configuration zettel.

=== See also
[[Full transclusion|00001007031100]] does not work inside some text, but is used for [[block-structured elements|00001007030000]].

Changes to docs/manual/00001007700000.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
45
46
47
48
49
id: 00001007700000
title: Query expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20220913135434

A query expression allows you to search for specific zettel and to perform some actions on them.
You may select zettel based on a full-text search, based on specific metadata values, or both.

A query expression consists of a __search expression__ and of an optional __action list__.
Both are separated by a vertical bar character (""''|''"", U+007C).

A query expression follows a [[formal syntax|00001007780000]].

=== Search expression

In its simplest form, a search expression just contains a string to be search for with the help of a full-text search.
For example, the string ''syntax'' will search for all zettel containing the word ""syntax"".

If you want to search for all zettel with a title containing the word ""syntax"", you must specify ''title:syntax''.
""title"" names the [[metadata key|00001006010000]], in this case the [[supported metadata key ""title""|00001006020000#title]].
The colon character (""'':''"") is a [[search operator|00001007705000]], in this example to specify a match.
""syntax"" is the [[search value|00001007706000]] that must match to the value of the given metadata key, here ""title"".

A search expression may contain more than one search term, such as ''title:syntax''.
Search terms must be separated by one or more space characters, for example ''title:syntax title:search''.
All terms of a select expression must be true so that a zettel is selected.

* [[Search terms|00001007702000]]
* [[Search operator|00001007705000]]
* [[Search value|00001007706000]]


Here are [[some examples|00001007790000]] of search expressions, which can be used to manage a Zettelstore:
{{{00001007790000}}}

=== Action List

With a search expression, a list of zettel is selected.
Actions allow to modify this list to a certain degree.

Which actions are allowed depends on the context.
However, actions are further separated into __parameter action__ and __aggregate actions__.
A parameter action just sets a parameter for an aggregate action.
An aggregate action transforms the list of selected zettel into a different, aggregate form.
Only the first aggregate form is executed, following aggregate actions are ignored.

In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]].

|




|


|

|
|



<
|
<
<
|
<
|
|
|
|
<
<
<
|
|
|
|
>

|

<
<
<
<
<
<
<
<
<
<
<
<
<
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
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
id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk

modified: 20220821163727

A search term allows you to specify one search restriction.
The result [[search expression|00001007700000]], which contains more than one search term, will be the applications of all restrictions.

A search term can be one of the following (the first three term are collectively called __search literals__):
* 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.





  **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 ""''!?''"".

  Is true, if zettel metadata contains / does not contain the given key.
* The string ''OR'' signals that following search literals may occur alternatively in the result.

  Since search literals may be negated, it is possible to form any boolean search expression.
  Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]].

  It has no effect on the following search terms initiated with a special uppercase word.







* The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list.
  If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed.

  Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.






>
|













>
>
>
>














>
>
>
>
>
>
>







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
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 ""''!?''"".

  Is true, if zettel metadata contains / does not contain the given key.
* The string ''OR'' signals that following search literals may occur alternatively in the result.

  Since search literals may be negated, it is possible to form any boolean search expression.
  Any search expression will be in a [[disjunctive normal form|https://en.wikipedia.org/wiki/Disjunctive_normal_form]].

  It has no effect on the following search terms initiated with a special uppercase word.
* The string ''PICK'', followed by a non-empty sequence of spaces and a number greater zero (called ""N"").

  This will pick randomly N elements of the result list, preserving the order of that list.
  A zero value of N will produce the same result as if nothing was specified.
  If specified multiple times, the lower value takes precedence.

  Example: ''PICK 5 PICK 3'' will be interpreted as ''PICK 3''.
* The string ''ORDER'', followed by a non-empty sequence of spaces and the name of a metadata key, will specify an ordering of the result list.
  If you include the string ''REVERSE'' after ''ORDER'' but before the metadata key, the ordering will be reversed.

  Example: ''ORDER published'' will order the resulting list based on the publishing data, while ''ORDER REVERSED published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.

72
73
74
75
76
77
78
79

You may have noted that the specifications of first two items overlap somehow.
This is resolved by the following rule:
* A search term containing no [[search operator character|00001007705000]] is treated as a full-text search.
* The first search operator character found in a search term divides the term into two pieces.
  If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search.
* Otherwise, the search term is treated as a full-text search.

If a term like ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search. For example, ''ORDER 123'' will search for a zettel conatining the strings ""ORDER"" (case-insensitive) and ""123"".








|
>
84
85
86
87
88
89
90
91
92
You may have noted that the specifications of first two items overlap somehow.
This is resolved by the following rule:
* A search term containing no [[search operator character|00001007705000]] is treated as a full-text search.
* The first search operator character found in a search term divides the term into two pieces.
  If the first piece, from the beginning of the search term to the search operator character, is syntactically a metadata key, the search term is treated as a metadata-based search.
* Otherwise, the search term is treated as a full-text search.

If a term like ''PICK'', ''ORDER'', ''ORDER REVERSE'', ''OFFSET'', or ''LIMIT'' is not followed by an appropriate value, it is interpreted as a search value for a full-text search.
For example, ''ORDER 123'' will search for a zettel containing the strings ""ORDER"" (case-insensitive) and ""123"".

Changes to docs/manual/00001007705000.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
id: 00001007705000
title: Search operator
role: manual
tags: #manual #search #zettelstore
syntax: zmk

modified: 20220819194709

A search operator specifies how the comparison of a search value and a zettel should be executed.
Every comparison is done case-insensitive, treating all uppercase letters the same as lowercase letters.

The following are allowed search operator characters:
* The exclamation mark character (""!"", U+0021) negates the meaning

* The tilde character (""''~''"", U+007E) compares on matching (""match operator"")
* The greater-than sign character (""''>''"", U+003E) matches if there is some prefix (""prefix operator"")
* The less-than sign character (""''<''"", U+003C) compares a suffix relationship (""suffix operator"")
* The colon character (""'':''"", U+003A) compares on equal words (""has operator"")



* The question mark (""''?''"", U+003F) checks for an existing metadata key (""exist operator"")


Since the exclamation mark character can be combined with the other, there are 10 possible combinations:
# ""''!''"": is an abbreviation of the ""''!~''"" operator.
# ""''~''"": is successful if the search value matched the value to be compared.
# ""''!~''"": is successful if the search value does not match the value to be compared.
# ""'':''"": is successful if the search value is equal to one word of the value to be compared.
# ""''!:''"": is successful if the search value is not equal to any word of the value to be compared.
# ""''>''"": is successful if the search value is a prefix of the value to be compared.
# ""''!>''"": is successful if the search value is not a prefix of the value to be compared.
# ""''<''"": is successful if the search value is a suffix of the value to be compared.
# ""''!<''"": is successful if the search value is not a suffix of the value to be compared.






# ""''?''"": is successful if the 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.





>
|





|
>
|
|
|
|
>
>
>
|
>

|



|
|
|
|
|
|
>
>
>
>
>
>




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
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: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20220913134024

```
QueryExpression  := SearchExpression ActionExpression?















SearchExpression := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm       := SearchOperator? SearchValue
                  | SearchKey SearchOperator SearchValue?
                  | SearchKey ExistOperator
                  | "OR"
                  | "RANDOM"

                  | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey
                  | "OFFSET" SPACE+ PosInt
                  | "LIMIT" SPACE+ PosInt.
SearchValue      := Word.
SearchKey        := MetadataKey.
SearchOperator   := '!'
                  | ('!')? ('~' | ':' | '<' | '>').
ExistOperator    := '?'
                  | '!' '?'.
PosInt           := '0'
                  | ('1' .. '9') DIGIT*.
ActionExpression := '|' (Word (SPACE+ Word)*)?
Word             := NO-SPACE NO-SPACE*
```






|


|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
|
|
|
|
|

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
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: 20220913144959

|= 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:RANDOM LIMIT 40]] | 40 random zettel
| [[query:dead?]] | Zettel with invalid / dead links
| [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel
| [[query:all-tags!?]] | Zettel without any tags
| [[query:tags!?]] | Zettel without tags that are defined within metadata
| [[query:content-tags?]] | Zettel with tags within content







|





|


<
|
|
>
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/00001007900000.zettel.

1
2
3
4
5

6
7
8
9
id: 00001007900000
title: Zettelmarkup: Tutorial
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk

modified: 20220811135314

* [[First steps|00001007903000]]: learn something about paragraphs, emphasized text, and lists.
* [[Second steps|00001007906000]]: know about links, thematic breaks, and headings.



|

>
|



1
2
3
4
5
6
7
8
9
10
id: 00001007900000
title: Zettelmarkup: Tutorial
role: manual
tags: #manual #tutorial #zettelmarkup
syntax: zmk
created: 20220810182917
modified: 20221209191820

* [[First steps|00001007903000]]: learn something about paragraphs, emphasized text, and lists.
* [[Second steps|00001007906000]]: know about links, thematic breaks, and headings.

Changes to docs/manual/00001007903000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk

modified: 20220811122618

[[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations.
Some characters have a special meaning, but you have to enter them is a defined way to see a visible change.
Zettelmarkup is designed to be used for zettel, which are relatively short.
It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example.

=== Paragraphs





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220810182917
modified: 20220926183359

[[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations.
Some characters have a special meaning, but you have to enter them is a defined way to see a visible change.
Zettelmarkup is designed to be used for zettel, which are relatively short.
It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example.

=== Paragraphs
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
* First item
* Second item
* Third item
```

This is rendered as:

:::zs-example
* First item
* Second item
* Third item
:::

Similar, an numbered list element begins a line with the number sign (sic!) followed by a space character:

```zmk
# First item
# Second item
# Third item
```

This is rendered as:

:::zs-example
# First item
# Second item
# Third item
:::

---
After trying out these markup elements, you might want to continue with the [[second steps|00001007906000]].







|















|







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
* First item
* Second item
* Third item
```

This is rendered as:

:::example
* First item
* Second item
* Third item
:::

Similar, an numbered list element begins a line with the number sign (sic!) followed by a space character:

```zmk
# First item
# Second item
# Third item
```

This is rendered as:

:::example
# First item
# Second item
# Third item
:::

---
After trying out these markup elements, you might want to continue with the [[second steps|00001007906000]].

Changes to docs/manual/00001007906000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007906000
title: Zettelmarkup: Second Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk

modified: 20220811135024

After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings.

=== Links
A Zettelstore is much more useful, if you connect related zettel.
If you read a zettel later, this allows you to know about the context of a zettel.
[[Zettelmarkup|00001007000000]] allows you to specify such a connection.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007906000
title: Zettelmarkup: Second Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220811115501
modified: 20220926183427

After you have [[learned|00001007903000]] the basic concepts and markup of Zettelmarkup (paragraphs, emphasized text, and lists), this zettel introduces you into the concepts of links, thematic breaks, and headings.

=== Links
A Zettelstore is much more useful, if you connect related zettel.
If you read a zettel later, this allows you to know about the context of a zettel.
[[Zettelmarkup|00001007000000]] allows you to specify such a connection.
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62

---

Second paragraph.
```

Both are rendered as:
:::zs-example
First paragraph.
---
Second paragraph.
:::

Try it!








|







49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

---

Second paragraph.
```

Both are rendered as:
:::example
First paragraph.
---
Second paragraph.
:::

Try it!

Added docs/manual/00001007990000.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
45
46
47
48
49
50
51
52
53
54
55
56
57
id: 00001007990000
title: Zettelmarkup: Cheat Sheet
role: manual
tags: #manual #reference #zettelmarkup
syntax: zmk
created: 20221209191905
modified: 20221209193310

=== Overview
This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements.
It can not cover any special case.
If you need more information about any of these elements, please refer to the detailed description.

=== Basic Syntax
|[[Text formatting|00001007040100]]|''__italic text__'' &rarr; __italic text__, ''**bold text**'' &rarr; **bold text**, ''""quoted text""'' &rarr; ""quoted text""
|[[Text editing|00001007040100]]|''>>inserted text>>'' &rarr; >>inserted text>>, ''~~deleted text~~'' &rarr; ~~deleted text~~
|[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' &rarr; ''entered text'', ''``source code``'' &rarr; ``source code``, ''==text output=='' &rarr; ==text output==
|[[Superscript, subscript|00001007040100]]|''m^^2^^'' &rarr; m^^2^^, ''H,,2,,O'' &rarr; H,,2,,O
|[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' &rarr; [[Link text|00001007990000]]
|[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' &rarr; [[Zettelstore|https://zettelstore.de]]
|[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' &rarr; {{Image text|00000000040001}}
|[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' &rarr; {{00001007990000}}
|[[Footnote|00001007040330]]|''text[^footnote]'' &rarr; text[^footnote]
|[[Special characters / entities|00001007040000]]|''&rarr;'' &rarr; &rarr;, ''&#x2115;'' &rarr; &#x2115;, ''&#8987;'' &rarr; &#8987;

=== Structuring
* [[Heading|00001007030300]]: ''=== Heading'', ''==== Sub-Heading''
* [[Horizontal rule / thematic break|00001007030400]]: ''---''
* [[Paragraphs|00001007030000]] are separated by an empty line
---

=== Lists
[[Unnumbered list|00001007030200]]:
```
* First list item
* Second list item
* Third list item
```
[[Numbered list|00001007030200]]:
```
# Item number 1
# Item number 2
# Item number 3
```
[[Description List|00001007030100]]:
```
; Term
: Definition
; Other Term
: Definition for other term
```
=== [[Tables|00001007031000]]
```
|=Header|Because|Equal Sign
|Cell 1.1|Cell 1.2| Cell 1.3
|Cell 2.1|Cell 2.2
```

Changes to docs/manual/00001008000000.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
45
46
47
48
49
50



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175300
modified: 20220824114649

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS
* HTML template data
* Image formats: GIF, PNG, JPEG, SVG
* Markdown
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which language should be used.
If it is not given, it defaults to ''plain''.
The following syntax values are supported:

; [!css|''css'']
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.


; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png'']
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the meta-file, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta'']
; [!html|''html'']
: Hypertext Markup Language, will not be parsed further.
  Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]).



  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface.
  The ``< script ...>`` tag is an example.
  See [[security aspects of Markdown|00001008010000#security-aspects]] for some details.
; [!markdown|''markdown''], [!md|''md'']
: For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]].
  Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used.
  See [[Use Markdown within Zettelstore|00001008010000]].
; [!mustache|''mustache'']
: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]].
; [!none|''none'']
: Only the metadata of a zettel is ""parsed"".
  Useful for displaying the full metadata.
  The [[runtime configuration zettel|00000000000100]] uses this syntax.
  The zettel content is ignored.
; [!pikchr]''pikchr''
: A [[PIC|https://en.wikipedia.org/wiki/Pic_language]]-like [[markup language for diagrams|https://pikchr.org/]].
; [!svg|''svg'']
: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].



; [!text|''text''], [!plain|''plain''], [!txt|''txt'']
: Plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]].

If you specify something else, your content will be interpreted as plain text.

=== Language for other elements of a zettel
[[Zettelmarkup|00001007000000]] allows to specify [[evaluation blocks|00001007031300]], which also receive a syntax value.
An evaluation blocks is typically interpreted by external software, for example [[Zettel Presenter|00001006055000#external-applications]].
However, some values are interpreted by Zettelstore during evaluation of a zettel:
; [!draw|''draw'']
: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters.






|












|





>
>
|





>
>








<
<





<
<


>
>
>








<
<
<
<
<
<
<
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







id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175300
modified: 20230529223634

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS
* HTML template data
* Image formats: GIF, PNG, JPEG, SVG
* Markdown
* Plain text, not further interpreted

The [[metadata key|00001006020000#syntax]] ""''syntax''"" specifies which markup language / data format should be used.
If it is not given, it defaults to ''plain''.
The following syntax values are supported:

; [!css|''css'']
: A [[Cascading Style Sheet|https://www.w3.org/Style/CSS/]], to be used when rendering a zettel as HTML.
; [!draw|''draw'']
: A [[language|00001008050000]] to ""draw"" a graphic by using some simple Unicode characters.
; [!gif|''gif'']; [!jpeg|''jpeg'']; [!jpg|''jpg'']; [!png|''png'']; [!webp|''webp'']
: The formats for pixel graphics.
  Typically the data is stored in a separate file and the syntax is given in the meta-file, which has the same name as the zettel identifier and has no file extension.[^Before version 0.2, the meta-file had the file extension ''.meta'']
; [!html|''html'']
: Hypertext Markup Language, will not be parsed further.
  Instead, it is treated as [[text|#text]], but will be encoded differently for [[HTML format|00001012920510]] (same for the [[web user interface|00001014000000]]).

  Since HTML from unknown sources may contain security-related problems, zettel with this syntax are treated as an empty zettel, unless the startup configuration value for [[''insecure-html''|00001004010000#insecure-html]] is set to at least the value ""html"".

  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface.
  The ``< script ...>`` tag is an example.
  See [[security aspects of Markdown|00001008010000#security-aspects]] for some details.
; [!markdown|''markdown''], [!md|''md'']
: For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]].
  Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used.
  See [[Use Markdown within Zettelstore|00001008010000]].


; [!none|''none'']
: Only the metadata of a zettel is ""parsed"".
  Useful for displaying the full metadata.
  The [[runtime configuration zettel|00000000000100]] uses this syntax.
  The zettel content is ignored.


; [!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]].

If you specify something else, your content will be interpreted as plain text.







Changes to docs/manual/00001008010000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001008010000
title: Use Markdown within Zettelstore
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk

modified: 20220627192014

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.
Zettelstore supports the [[CommonMark|00001008010500]] dialect of Markdown.

=== Use Markdown as the default markup language of Zettelstore

Update the [[New Zettel|00000000090001]] template (and other relevant template zettel) by setting the syntax value to ''md'' or ''markdown''.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001008010000
title: Use Markdown within Zettelstore
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221018115601

If you are customized to use Markdown as your markup language, you can configure Zettelstore to support your decision.
Zettelstore supports the [[CommonMark|00001008010500]] dialect of Markdown.

=== Use Markdown as the default markup language of Zettelstore

Update the [[New Zettel|00000000090001]] template (and other relevant template zettel) by setting the syntax value to ''md'' or ''markdown''.
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41


42

43
44
45
46
47
Not every Markdown tool allows both file extensions.

BTW, metadata is stored in a file without a file extension, if neither ''yaml-header'' nor ''zettel-file-syntax'' is set.

=== Security aspects

You should be aware that Markdown is a super-set of HTML.
Any HTML code is valid Markdown code.
If you write your own zettel, this is probably not a problem.

However, if you receive zettel from others, you should be careful.
An attacker might include malicious HTML code in your zettel.
For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites.
When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results.



Zettelstore mitigates this problem by ignoring suspicious text when it encodes a zettel as HTML.

Any HTML text that might contain the ``<script>`` tag or the ``<iframe>`` tag is ignored.
This may lead to unexpected results if you depend on these.
Other [[encodings|00001012920500]] may still contain the full HTML text.

Any external client of Zettelstore, which does not use Zettelstore's [[HTML encoding|00001012920510]], must be programmed to take care of malicious code.







|







>
>
|
>





28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Not every Markdown tool allows both file extensions.

BTW, metadata is stored in a file without a file extension, if neither ''yaml-header'' nor ''zettel-file-syntax'' is set.

=== Security aspects

You should be aware that Markdown is a super-set of HTML.
The body of any HTML document is also a valid Markdown document.
If you write your own zettel, this is probably not a problem.

However, if you receive zettel from others, you should be careful.
An attacker might include malicious HTML code in your zettel.
For example, HTML allows to embed JavaScript, a full-sized programming language that drives many web sites.
When a zettel is displayed, JavaScript code might be executed, sometimes with harmful results.

By default, Zettelstore prohibits any HTML content.
If you want to relax this rule, you should take a look at the startup configuration key [[''insecure-html''|00001004010000#insecure-html]].

Even if you have allowed HTML content, Zettelstore mitigates some of the security problems by ignoring suspicious text when it encodes a zettel as HTML.
Any HTML text that might contain the ``<script>`` tag or the ``<iframe>`` tag is ignored.
This may lead to unexpected results if you depend on these.
Other [[encodings|00001012920500]] may still contain the full HTML text.

Any external client of Zettelstore, which does not use Zettelstore's [[HTML encoding|00001012920510]], must be programmed to take care of malicious code.

Changes to docs/manual/00001008010500.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: 00001008010500
title: CommonMark
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk

modified: 20220113193000
url: https://commonmark.org/

[[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation.

Time will show, if this attempt is successful.

However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects.
Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM).
But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed.
Plus, they sometimes build on an older specification of CommonMark.

Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]].
If possible, Zettelstore will adapt to newer versions when they are available.

To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark.
Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.]
This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element will fail to be encoded as HTML.
In most cases, the differences are not visible for an user, but only by comparing the generated HTML code.







>
|




















>
>
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: 00001008010500
title: CommonMark
role: manual
tags: #manual #markdown #zettelstore
syntax: zmk
created: 20220113183435
modified: 20221018123145
url: https://commonmark.org/

[[CommonMark|https://commonmark.org/]] is a Markdown dialect, an [[attempt|https://xkcd.com/927/]] to unify all the different, divergent dialects of Markdown by providing an unambiguous syntax specification for Markdown, together with a suite of comprehensive tests to validate implementation.

Time will show, if this attempt is successful.

However, CommonMark is a well specified Markdown dialect, in contrast to most (if not all) other dialects.
Other software adopts CommonMark somehow, notably [[GitHub Flavored Markdown|https://github.github.com/gfm/]] (GFM).
But they provide proprietary extensions, which makes it harder to change to another CommonMark implementation if needed.
Plus, they sometimes build on an older specification of CommonMark.

Zettelstore supports the latest CommonMark [[specification version 0.30 (2021-06-19)|https://spec.commonmark.org/0.30/]].
If possible, Zettelstore will adapt to newer versions when they are available.

To provide CommonMark support, Zettelstore uses currently the [[Goldmark|https://github.com/yuin/goldmark]] implementation, which passes all validation tests of CommonMark.
Internally, CommonMark is translated into some kind of super-set of [[Zettelmarkup|00001007000000]], which additionally allows to use HTML code.[^Effectively, Markdown and CommonMark are itself super-sets of HTML.]
This Zettelmarkup super-set is later [[encoded|00001012920500]], often into [[HTML|00001012920510]].
Because Zettelstore HTML encoding philosophy differs a little bit to that of CommonMark, Zettelstore itself will not pass the CommonMark test suite fully.
However, no CommonMark language element will fail to be encoded as HTML.
In most cases, the differences are not visible for an user, but only by comparing the generated HTML code.

Be aware, depending on the value of the startup configuration key [[''insecure-html''|00001004010000#insecure-html]], HTML code found within a CommonMark document or within the mentioned kind of super-set of Zettelmarkup will typically be ignored for security-related reasons.

Changes to docs/manual/00001008050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001008050000
title: The ""draw"" language
role: manual
tags: #graphic #manual #zettelstore
syntax: zmk

modified: 20220311120439

Sometimes, ""a picture is worth a thousand words"".
To create some graphical representations, Zettelmarkup provides a simple mechanism.
Characters like ""''|''"" or ""''-''"" already provide some visual feedback.
For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible:
```
~~~draw

|



>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001008050000
title: The "draw" language
role: manual
tags: #graphic #manual #zettelstore
syntax: zmk
created: 20220131142036
modified: 20230403123738

Sometimes, ""a picture is worth a thousand words"".
To create some graphical representations, Zettelmarkup provides a simple mechanism.
Characters like ""''|''"" or ""''-''"" already provide some visual feedback.
For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible:
```
~~~draw

Changes to docs/manual/00001010000000.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
id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211124140614

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
If you do not configure it in other ways, no person from another computer can connect to your Zettelstore.
You must explicitly configure it to allow access from other computers.

In the case that your own multiple computers, you do not have to access the Zettelstore remotely.
You could install Zettelstore on each computer and set-up some software to synchronize your zettel.
Since zettel are stored as ordinary files, this task could be done in various ways.

=== Read-only
You can start the Zettelstore in an read-only mode.
Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.].






>
|










|







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
id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221018123622

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
If you do not configure it in other ways, no person from another computer can connect to your Zettelstore.
You must explicitly configure it to allow access from other computers.

In the case that you own multiple computers, you do not have to access the Zettelstore remotely.
You could install Zettelstore on each computer and set-up some software to synchronize your zettel.
Since zettel are stored as ordinary files, this task could be done in various ways.

=== Read-only
You can start the Zettelstore in an read-only mode.
Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.].

56
57
58
59
60
61
62
63
64
65
66
67
When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted.
Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public.

The Zettelstore itself does not encrypt messages.
But you can put a server in front of it, which is able to handle encryption.
Most generic web server software do allow this.

To enforce encryption, [[authentication sessions|00001010040700]] are marked as secure by default.
If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration.
Otherwise, authentication will not work.

* [[Use a server for encryption|00001010090100]]







|




57
58
59
60
61
62
63
64
65
66
67
68
When Zettelstore is accessed remotely, the messages that are sent between Zettelstore and the client must be encrypted.
Otherwise, an eavesdropper could fetch sensible data, such as passwords or precious content that is not for the public.

The Zettelstore itself does not encrypt messages.
But you can put a server in front of it, which is able to handle encryption.
Most generic web server software do allow this.

To enforce encryption, [[authenticated sessions|00001010040700]] are marked as secure by default.
If you still want to access the Zettelstore remotely without encryption, you must change the startup configuration.
Otherwise, authentication will not work.

* [[Use a server for encryption|00001010090100]]

Changes to docs/manual/00001010040200.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
id: 00001010040200
title: Creating an user zettel
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk

modified: 20211127174207

All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following two metadata fields:

; ''user-id'' (""user identification"")
: The unique identification to be specified for authentication.
; ''credential''
: A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command.

The title of the zettel typically specifies the real name of the user.

The following metadata elements are optional:

; ''user-role''
: Associate the user with some basic privileges, e.g. a [[user role|00001010070300]]



A user zettel can only be created by the owner of the Zettelstore.

The owner should execute the following steps to create a new user zettel:

# Create a new zettel.
# Save the zettel to get a [[identifier|00001006050000]] for this zettel.
# Choose a unique identification for the user.
#* If the identifier is not unique, authentication will not work for this user.
# Execute the [[``zettelstore password``|00001004051400]] command.
#* You have to specify the user identification and the zettel identifier
#* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself.
# Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''.





>
|















>
>













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: 00001010040200
title: Creating an user zettel
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221205160251

All data to be used for authenticating a user is store in a special zettel called ""user zettel"". 
A user zettel must have set the following two metadata fields:

; ''user-id'' (""user identification"")
: The unique identification to be specified for authentication.
; ''credential''
: A hashed password as generated by the [[``zettelstore password``{=sh}|00001004051400]] command.

The title of the zettel typically specifies the real name of the user.

The following metadata elements are optional:

; ''user-role''
: Associate the user with some basic privileges, e.g. a [[user role|00001010070300]]

A user zettel may additionally contain metadata that [[overwrites corresponding values|00001004020200]] of the [[runtime configuration|00001004020000]].

A user zettel can only be created by the owner of the Zettelstore.

The owner should execute the following steps to create a new user zettel:

# Create a new zettel.
# Save the zettel to get a [[identifier|00001006050000]] for this zettel.
# Choose a unique identification for the user.
#* If the identifier is not unique, authentication will not work for this user.
# Execute the [[``zettelstore password``|00001004051400]] command.
#* You have to specify the user identification and the zettel identifier
#* If you should not know the password of the new user, send her/him the user identification and the user zettel identifier, so that the person can create the hashed password herself.
# Edit the user zettel and add the hashed password under the meta key ''credential'' and the user identification under the key ''user-id''.

Changes to docs/manual/00001010070200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001010070200
title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220913144845

For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public|""public""]
: The zettel is visible to everybody, even if the user is not authenticated.






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001010070200
title: Visibility rules for zettel
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220923104643

For every zettel you can specify under which condition the zettel is visible to others.
This is controlled with the metadata key [[''visibility''|00001006020000#visibility]].
The following values are supported:

; [!public|""public""]
: The zettel is visible to everybody, even if the user is not authenticated.
37
38
39
40
41
42
43
44
45
46
47
48
49
50
Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.

=== Examples
Similar to the [[API|00001012051840]], you can easily create a zettel list based on the ''visibility'' metadata key:

| public  | [[query:visibility:public]]
| login   | [[query:visibility:login]]
| creator | [[query:visibility:creator]]
| owner   | [[query:visibility:owner]]
| expert  | [[query:visibility:expert]][^Only if [[''expert-mode''|00001004020000#expert-mode]] is enabled, this list will show some zettel.]







|






37
38
39
40
41
42
43
44
45
46
47
48
49
50
Please note: if [[authentication is not enabled|00001010040100]], every user has the same rights as the owner of a Zettelstore.
This is also true, if the Zettelstore runs additionally in [[read-only mode|00001004010000#read-only-mode]].
In this case, the [[runtime configuration zettel|00001004020000]] is shown (its visibility is ""owner"").
The [[startup configuration|00001004010000]] is not shown, because the associated computed zettel with identifier ''00000000000096'' is stored with the visibility ""expert"".
If you want to show such a zettel, you must set ''expert-mode'' to true.

=== Examples
By using a [[query expression|00001007700000]], you can easily create a zettel list based on the ''visibility'' metadata key:

| public  | [[query:visibility:public]]
| login   | [[query:visibility:login]]
| creator | [[query:visibility:creator]]
| owner   | [[query:visibility:owner]]
| expert  | [[query:visibility:expert]][^Only if [[''expert-mode''|00001004020000#expert-mode]] is enabled, this list will show some zettel.]

Changes to docs/manual/00001012000000.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
45
46
47
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220913141632

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.

There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use.

=== Authentication
If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller.
* [[Authenticate an user|00001012050200]] to obtain an access token
* [[Renew an access token|00001012050400]] without costly re-authentication
* [[Provide an access token|00001012050600]] when doing an API call

=== Zettel lists
* [[List metadata of all zettel|00001012051200]]
** [[Query expressions|00001012051840]] (includes content search)
* [[Map metadata values to lists of zettel identifier|00001012052400]]
* [[Query the list of all zettel|00001012051400]]

=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]
* [[Retrieve context of an existing zettel|00001012053800]]
* [[Retrieve unlinked references to an existing zettel|00001012053900]]
* [[Retrieve zettel order within an existing zettel|00001012054000]]
* [[Update metadata and content of a zettel|00001012054200]]
* [[Rename a zettel|00001012054400]]
* [[Delete a zettel|00001012054600]]

=== Various helper methods
* [[Retrieve administrative data|00001012070500]]
* [[Execute some commands|00001012080100]]
** [[Check for authentication|00001012080200]]
** [[Refresh internal data|00001012080500]]






|






|










|
<
<








<
<
<









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
id: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807171136

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software.

There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use.

=== Authentication
If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller.
* [[Authenticate an user|00001012050200]] to obtain an access token
* [[Renew an access token|00001012050400]] without costly re-authentication
* [[Provide an access token|00001012050600]] when doing an API call

=== Zettel lists
* [[List all zettel|00001012051200]]


* [[Query the list of all zettel|00001012051400]]

=== 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/00001012050200.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
45
46
47
48
49
50
51
52
53
54
55
56
57
id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk

modified: 20220107215844

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request:
```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL:
```sh
# curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data:
```sh
# curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
```

In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls.

**Important:** obtaining a token is a time-intensive process.
Zettelstore will delay every request to obtain a token for a certain amount of time.
Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more.

However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"token":"freeaccess","token_type":"Bearer","expires_in":316224000}
```

In this case, it is even possible to omit the user identification/password.

=== HTTP Status codes
In all cases of successful authentication, a JSON object is returned, which contains the token under the key ''"token"''.
A successful authentication is signaled with the HTTP status code 200, as usual.

Other status codes possibly send by the Zettelstore:
; ''400''
: Unable to process the request.
  In most cases the form data was invalid.
; ''401''
: Authentication failed.
  Either the user identification is invalid or you provided the wrong password.
; ''403''
: Authentication is not active.





>
|








|





|





|


|









|





|











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
id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230412150544

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request:
```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA2MiwiaWF0IjoxNjgxMzA0MDAyLCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kdF8PdiL50gIPkRD3ovgR6nUXR0-80EKAXcY2zVYgYvryF09iXnNR3zrvYnGzdrArMcnvAYqVvuXtqhQj2jG9g" 600)
```

Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL:
```sh
# curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 600)
```

If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data:
```sh
# curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600)
```

In all cases, you will receive a list with three elements that will contain all [[relevant data|00001012921000]] to be used for further API calls.

**Important:** obtaining a token is a time-intensive process.
Zettelstore will delay every request to obtain a token for a certain amount of time.
Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more.

However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
("Bearer" "freeaccess" 316224000)
```

In this case, it is even possible to omit the user identification/password.

=== HTTP Status codes
In all cases of successful authentication, a list is returned, which contains the token as the second element.
A successful authentication is signaled with the HTTP status code 200, as usual.

Other status codes possibly send by the Zettelstore:
; ''400''
: Unable to process the request.
  In most cases the form data was invalid.
; ''401''
: Authentication failed.
  Either the user identification is invalid or you provided the wrong password.
; ''403''
: Authentication is not active.

Changes to docs/manual/00001012050400.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: 00001012050400
title: API: Renew an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk

modified: 20220107215751

An access token is only valid for a certain duration.
Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data.

Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/a'' and include the current access token in the ''Authorization'' header:

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456}
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.

If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a
{"token":"freeaccess","token_type":"Bearer","expires_in":316224000}
```

In this case, it is even possible to omit the access token.

=== HTTP Status codes
; ''200''
: Renew process was successful, the body contains an [[appropriate JSON object|00001012921000]].
; ''400''
: The renew process was not successful.
  There are several reasons for this.
  Maybe access bearer token was not valid.

  Probably you should [[authenticate|00001012050200]] again with user identification and password.





>
|








|








|






|






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
id: 00001012050400
title: API: Renew an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230412160219

An access token is only valid for a certain duration.
Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data.

Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/a'' and include the current access token in the ''Authorization'' header:

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 456)
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.

If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a
("Bearer" "freeaccess" 316224000)
```

In this case, it is even possible to omit the access token.

=== HTTP Status codes
; ''200''
: Renew process was successful, the body contains a [[list|00001012921000]] with the relevant data.
; ''400''
: The renew process was not successful.
  There are several reasons for this.
  Maybe access bearer token was not valid.

  Probably you should [[authenticate|00001012050200]] again with user identification and password.

Changes to docs/manual/00001012051200.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
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
id: 00001012051200
title: API: List metadata of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220913151852

To list the metadata of all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/j''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/j
{"query":"","list":[{"id":"00001012051200","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050600","meta":{"title":"API: Provide an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050400","meta":{"title":"API: Renew an access token","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012050200","meta":{"title":"API: Authenticate a client","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62},{"id":"00001012000000","meta":{"title":"API","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual"},"rights":62}]}
```

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001012051840]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.

If you reformat the JSON output from the ''GET /j'' call, you'll see its structure better:

```json
{
  "query": "",
  "human": "",
  "list": [
    {
      "id": "00001012051200",
      "meta": {
        "title": "API: List for all zettel some data",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      },
      "rights":62
    },
    {
      "id": "00001012050600",
      "meta": {
        "title": "API: Provide an access token",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      },
      "rights":62
    },
    {
      "id": "00001012050400",
      "meta": {
        "title": "API: Renew an access token",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      },
      "rights":62
    },
    {
      "id": "00001012050200",
      "meta": {
        "title": "API: Authenticate a client",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      },
      "rights":62
    },
    {
      "id": "00001012000000",
      "meta": {
        "title": "API",
        "tags": "#api #manual #zettelstore",
        "syntax": "zmk",
        "role": "manual"
      },
      "rights":62
    }
  ]
}
```
In this special case, the metadata of each zettel just contains the four default keys ''title'', ''tags'', ''syntax'', and ''role''.

[!plain]Alternatively, you can retrieve the list of zettel in a simple, plain format using the [[endpoint|00001012920000]] ''/z''.
In this case, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:

```sh
# curl http://127.0.0.1:23123/z

00001012051200 API: Renew an access token
00001012050600 API: Provide an access token
00001012050400 API: Renew an access token
00001012050200 API: Authenticate a client


00001012000000 API











```






























=== Note
This request will always return a list of metadata, provided the request was syntactically correct.
There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token.
In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty.

With this call, you cannot differentiate between an empty result list (e.g because your [[content search|00001012051840]] did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token).

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]].
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid.

|




|

|
<
|
<
<
<
<

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|





>
|



>
>
|
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|



|



|




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
id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170810

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:

```sh
# curl http://127.0.0.1:23123/z
...
00001012051200 API: List all zettel
00001012050600 API: Provide an access token
00001012050400 API: Renew an access token
00001012050200 API: Authenticate a client
...
```

The list is **not** sorted, even in the these examples where it appears to be sorted.
If you want to have it ordered, you must specify it with the help of a [[query expression|00001007700000]] / [[search term|00001007702000]].
See [[Query the list of all zettel|00001012051400]] how to do it.

=== Data output

Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':

```sh
# curl 'http://127.0.0.1:23123/z?enc=data'
(meta-list (query "") (human "") (list (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ...
```

Pretty-printed, this results in:
```
(meta-list (query "")
           (human "")
           (list (zettel (id "00001012921200")
                         (meta (title "API: Encoding of Zettel Access Rights")
                               (role "manual")
                               (tags "#api #manual #reference #zettelstore")
                               (syntax "zmk")
                               (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
                               (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
                               (box-number "1")
                               (created "00010101000000")
                               (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300")
                               (modified "20220201171959")
                               (published "20220201171959"))
                         (rights 62))
                 (zettel (id "00001007030100")
```

* The result is a list, starting with the symbol ''meta-list''.
* Then, some key/value pairs are following, also nested.
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
* ''list'' starts a list of zettel.
* ''zettel'' itself start, well, a zettel.
* ''id'' denotes the zettel identifier, encoded as a string.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

=== Note
This request (and similar others) will always return a list of metadata, provided the request was syntactically correct.
There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token.
In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty.

With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token).

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid.

Changes to docs/manual/00001012051400.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
45
46
47
48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64
65
66
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20220913150204


The [[endpoint|00001012920000]] ''/q'' allows to query the list of all zettel.

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 will return nothing.
It is an error, if both are empty.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.


For example, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|role''.

If successful, the output is a JSON object:



```sh
# curl http://127.0.0.1:23123/q?q=|role





{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}}





```


The JSON object only contains the key ''"map"'' with the value of another object.



This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value.








Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/q?q=|tags''.




If successful, the output is a JSON object:



```sh
# curl http://127.0.0.1:23123/q?q=|tags




{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}













```






The JSON object only contains the key ''"map"'' with the value of another object.


This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.





If you want only those tags that occur at least 100 times, use the endpoint ''/q?q=|MIN100+tags''.
You see from this that actions are separated by space characters.



There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

Only the first aggregate action will be executed.


=== HTTP Status codes
; ''200''
: Query was successful.
; ''204''
: Query was successful, but results in no content.
  Most likely, you specified no appropriate aggregator.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid, or you forgot to specify a valid query.






|
>

|



|
<



>
|
>
|
>

>

|
>
>
>
>
>
|
>
>
>
>
>


>
|
>
>
>
|
>
>
>
>
>
>

>
|
>
>
>
>
|

>
>

|
>
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>


>
>
>
>
>
|
>
>
|
>
>
>
>

|

>
>














>











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
id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20230807170638
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.


Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.

The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata.

It is allowed to specify this query parameter more than once.

This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]].

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: Structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```

If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]:

```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data'
(meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (list (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ...
```

The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more).
It contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.

The symbol ''list'' starts the list of zettel data.
Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value.
Leading zeroes are removed.
Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value.
Metadata keys are encoded as a symbol, metadata values as a string.
''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel.

=== Aggregates

An implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list.


As an example for a query action, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|role''.

```sh
# curl 'http://127.0.0.1:23123/z?q=|role'
configuration	00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001
manual	00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000
zettel	00010000000000 00000000090001
```

The result is a text file.
The first word, separated by a horizontal tab (U+0009) contains the role name.
The rest of the line consists of zettel identifier, where the corresponding zettel have this role.
Zettel identifier are separated by a space character (U+0020).

Please note that the list is **not** sorted by the role name, so the same request might result in a different order.
If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore.

Of course, this list can also be returned as a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|role&enc=data'
(aggregate "role" (query "| role") (human "| role") (list ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519)))
```

The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above.
Then a string follows, which specifies the key on which the aggregate was performed.
''query'' and ''human'' have the same meaning as above.
The ''symbol'' list starts the result list of aggregates.
Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''.
If successful, the output is a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
(aggregate "tags" (query "| tags") (human "| tags") (list ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```

If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''.
You see from this that actions are separated by space characters.

=== Actions

There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

Only the first aggregate action will be executed.


=== HTTP Status codes
; ''200''
: Query was successful.
; ''204''
: Query was successful, but results in no content.
  Most likely, you specified no appropriate aggregator.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid, or you forgot to specify a valid query.

Deleted docs/manual/00001012051840.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
id: 00001012051840
title: API: Shape the list of zettel metadata by specifying a query expression
role: manual
tags: #api #manual #search #zettelstore
syntax: zmk
created: 20210709143714
modified: 20220913150355

The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata.

You are allowed to specify this query parameter more than once, as well as the other query parameters.
All results will be intersected, i.e. a zettel will be included into the list if all of the provided values match.

This parameter loosely resembles the search form of the [[web user interface|00001014000000]].

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/j?q=title%3AAPI'
{"query":"title MATCH API","list":[{"id":"00001012921000","meta":{"title":"API: JSON structure of an access token","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920500","meta":{"title":"Formats available by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}},{"id":"00001012920000","meta":{"title":"Endpoints used by the API","tags":"#api #manual #reference #zettelstore","syntax":"zmk","role":"manual"}}, ...
```

However, if you want all zettel that does not match a given value, you must prefix the value with the exclamation mark character (""!"", U+0021).
For example, if you want to retrieve all zettel that do not contain the string ""API"" in their title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/j?q=title!%3AAPI'
{"query":"title NOT MATCH API","list":[{"id":"00010000000000","meta":{"back":"00001003000000 00001005090000","backward":"00001003000000 00001005090000","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00000000000001 00000000000003 00000000000096 00000000000100","lang":"en","license":"EUPL-1.2-or-later","role":"zettel","syntax":"zmk","title":"Home"}},{"id":"00001014000000","meta":{"back":"00001000000000 00001004020000 00001012920510","backward":"00001000000000 00001004020000 00001012000000 00001012920510","copyright":"(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>","forward":"00001012000000","lang":"en","license":"EUPL-1.2-or-later","published":"00001014000000","role":"manual","syntax":"zmk","tags":"#manual #webui #zettelstore","title":"Web user interface"}},
...
```

In both cases, an implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/j?q=url%3A'`` and ``curl 'http://localhost:23123/j?q=url%3A!'`` may result in an empty list.

Alternatively, you also can use the [[endpoint|00001012920000]] ''/z'' for a simpler result format.
The first example translates to:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI'
00001012921000 API: JSON structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






















































































Deleted docs/manual/00001012052400.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
id: 00001012052400
title: API: Map metadata values to list of zettel identifier
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210216172944

modified: 20220912112253

**Note**: this endpoint is deprecated since v0.7 and will be removed in v0.8.
Please use a [[query|00001012051400]] instead.
---
The [[endpoint|00001012920000]] ''/m'' allows to retrieve a map of metadata values (of a specific key) to the list of zettel identifier, which reference zettel containing this value under the given metadata key.

Currently, two keys are supported:
* [[''role''|00001006020100]]
* [[''tags''|00001006020000#tags]]

To list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?key=role''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/m?key=role
{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/m?key=tags''.
If successful, the output is a JSON object:

```sh
# curl http://127.0.0.1:23123/m?key=tags
{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.

Please note that this structure will likely change in the future to be more compliant with other API calls.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































Changes to docs/manual/00001012053200.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
45
46
47
48
49
50
51

52
53
54
55
56











57
58
59
60
61
62
63
64
65
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

modified: 20220628111320

A zettel is created by adding it to the [[list of zettel|00001012000000#zettel-lists]].
Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/j'', but you must send the data of the new zettel via a HTTP POST request.

The body of the POST request must contain a JSON object that specifies metadata and content of the zettel to be created.
The following keys of the JSON object are used:
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"encoding"''
: States how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
  Other values will result in a HTTP response status code ''400''.
; ''"content"''
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.

Other keys will be ignored.
Even these three keys are just optional.
The body of the HTTP POST request must not be empty and it must contain a JSON object.

Therefore, a body containing just ''{}'' is perfectly valid.
The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]:

```
# curl -X POST --data '{}' http://127.0.0.1:23123/j
{"id":"20210713161000"}
```
If creating the zettel was successful, the HTTP response will contain a JSON object with one key:
; ''"id"''
: Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage.

In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel.
A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL.

As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing:
```
# curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' http://127.0.0.1:23123/j
{"id":"20210713163100"}
```

[!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z'' to create a new zettel.
In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z
20210903211500
```












=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text).
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the JSON was not formed according to above rules.
; ''403''
: You are not allowed to create a new 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
id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807170416

A zettel is created by adding it to the [[list of zettel|00001012000000]].
Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request.








































The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z
20210903211500
```

The zettel identifier of the created zettel is returned.
In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel.
A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL.

=== Data input
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.

=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (data value or plain text).
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the symbolic expression was not formed according to above rules.
; ''403''
: You are not allowed to create a new zettel.

Changes to docs/manual/00001012053300.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
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
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20220908162927

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/j/00001012053300
{"id":"00001012053300","meta":{"title":"API: Retrieve data for an existing zettel","tags":"#api #manual #zettelstore","syntax":"zmk","role":"manual","copyright":"(c) 2020 by Detlef Stern <ds@zettelstore.de>","lang":"en","license":"CC BY-SA 4.0"},"content":"The endpoint to work with a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000}}.\n\nFor example, ...
```

Pretty-printed, this results in:
```
{
  "id": "00001012053300",
  "meta": {
    "back": "00001012000000 00001012053200 00001012054400",
    "backward": "00001012000000 00001012053200 00001012054400 00001012920000",
    "box-number": "1",
    "forward": "00001010040100 00001012050200 00001012920000 00001012920800",
    "modified": "20210726190012",
    "published": "20210726190012",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata and content of an existing zettel"
  },
  "encoding": "",
  "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' (...)
  "rights": 62
}
```

The following keys of the JSON object are used:
; ''"id"''
: The zettel identifier of the zettel you requested.
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"encoding"''
: States how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
  Other values will result in a HTTP response status code ''400''.
; ''"content"''
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.


=== Plain zettel
[!plain]Additionally, you can retrieve the plain zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}''
Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.
Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value).

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
...
````

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300?part=zettel'
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint
...
````





































=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object / plain zettel data.
; ''204''
: Request was valid, but there is no data to be returned.
  Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.






|

|

|
|
|
|
<
<
|
<
<
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
>

<
<
<




<
<
<
<
<
<
<
<
<








|





>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|












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
id: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20230807170259

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'


The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].



For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
















```sh
...
















````




Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.
Valid values for [[''PART''|00001012920800]] are ""zettel"", ""[[meta|00001012053400]]"", and ""content"" (the default value).











````sh
# curl 'http://127.0.0.1:23123/z/00001012053300?part=zettel'
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint
...
````

=== Data output

Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':

```sh
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=data&part=zettel'
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400") (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000") (box-number "1") (created "20211004093206") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500") (modified "20230703174152") (published "20230703174152") (role "manual") (syntax "zmk") (tags "#api #manual #zettelstore") (title "API: Retrieve metadata and content of an existing zettel")) (rights 62) (encoding "") (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```

If you print the result a little bit nicer, you will see its structure:
```
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400")
              (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000")
              (box-number "1")
              (created "20211004093206")
              (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500")
              (modified "20230703174152")
              (published "20230703174152")
              (role "manual")
              (syntax "zmk")
              (tags "#api #manual #zettelstore")
              (title "API: Retrieve metadata and content of an existing zettel"))
        (rights 62)
        (encoding "")
        (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```

* The result is a list, starting with the symbol ''zettel''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
* The zettel contents is stored as a value of the key ''content''.
  Typically, text content is not encoded, and binary content is encoded via Base64.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''204''
: Request was valid, but there is no data to be returned.
  Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053400.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
45
46
47
48
49
50
51
52
53
54
55






























56
57
58
59
60
61
62
63
64
65
66
67
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20220908162635

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/m/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/j/00001012053400''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/m/00001012053400
{"meta":{"all-tags":"#api #manual #zettelstore","back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","forward":"00001010040100 00001012050200 00001012920000 00001012920800","modified":"20211004111240","published":"20211004111240","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62}
```

Pretty-printed, this results in:
```
{
  "meta": {
    "all-tags": "#api #manual #zettelstore",
    "back": "00001012000000 00001012053300",
    "backward": "00001012000000 00001012053300 00001012920000",
    "box-number": "1",
    "forward": "00001010040100 00001012050200 00001012920000 00001012920800",
    "modified": "20211004111240",
    "published": "20211004111240",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata of an existing zettel"
  },
  "rights": 62
}
```
The following keys of the JSON object are used:
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.

[!plain]Additionally, you can retrieve the plain metadata of a zettel, without using JSON.
Just change the [[endpoint|00001012920000]] to ''/z/{ID}?part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
````































=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.






|

|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<









>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>


|









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
id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170155

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.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053500.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
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
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20220908162843

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

For example, to retrieve some evaluated data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/v/00001012053500
{"meta":{"title":[{"t":"Text","s":"API:"},{"t":"Space"},{"t":"Text","s":"Retrieve"},{"t":"Space"},{"t":"Text","s":"evaluated"},{"t":"Space"},{"t":"Text","s":"metadata"},{"t":"Space"},{"t":"Text","s":"and"},{"t":"Space"},{"t":"Text","s":"content"},{"t":"Space"},{"t":"Text","s":"of"},{"t":"Space"},{"t":"Text","s":"an"},{"t":"Space"},{"t":"Text","s":"existing"},{"t":"Space"},{"t":"Text","s":"zettel"},{"t":"Space"},{"t":"Text","s":"in"},{"t":"Space"}, ...
```

To select another encoding, you can provide a query parameter ''enc=ENCODING''.
The default value for [[''ENCODING''|00001012920500]] is ""[[zjson|00001012920503]]"".
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some more.

```sh
# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</title>

<meta name="zs-role" content="manual">
<meta name="keywords" content="api, manual, zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-back" content="00001012000000">
<meta name="zs-backward" content="00001012000000">
<meta name="zs-box-number" content="1">
<meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>">

<meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800">



<meta name="zs-published" content="00001012053500">

</head>
<body>

<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/v/{ID}</kbd>, where <kbd>{ID}</kbd> is a placeholder for the <a href="00001006050000">zettel identifier</a>.</p>
...
```

You also can use the query parameter ''part=PART'' to specify which [[parts|00001012920800]] of a zettel must be encoded.
In this case, its default value is ''content''.
```sh
# curl 'http://127.0.0.1:23123/v/00001012053500?enc=html&part=meta'
<meta name="zs-title" content="API: Retrieve evaluated metadata and content of an existing zettel in various encodings">
<meta name="zs-role" content="manual">
<meta name="keywords" content="api, manual, zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-back" content="00001012000000">
<meta name="zs-backward" content="00001012000000">
<meta name="zs-box-number" content="1">
<meta name="copyright" content="(c) 2020-2021 by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-forward" content="00001010040100 00001012050200 00001012920000 00001012920800">
<meta name="zs-lang" content="en">
<meta name="zs-published" content="00001012053500">
```

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.






|

|

|
|

|
|


|
<
|
>

|
|
<



>

|

|
|

|
>
|
>
>
>
|
>


>
|



<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


|









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
id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230807170112

The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a symbolic expression value:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.

Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].
In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel'
<html>

<head>
<meta charset="utf-8">
<title>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</title>
<meta name="zs-title" content="API: Retrieve evaluated metadata and content of an existing zettel in various encodings">
<meta name="zs-role" content="manual">
<meta name="zs-tags" content="#api #manual #zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-back" content="00001006000000 00001012000000 00001012053600">
<meta name="zs-backward" content="00001006000000 00001012000000 00001012053600 00001012920000">
<meta name="zs-box-number" content="1">
<meta name="zs-copyright" content="(c) 2020-present by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-created" content="20210726174524">
<meta name="zs-forward" content="00001006050000 00001010040100 00001012050200 00001012920000 00001012920500 00001012920503 00001012920510 00001012920519 00001012920800">
<meta name="zs-lang" content="en">
<meta name="zs-license" content="EUPL-1.2-or-later">
<meta name="zs-modified" content="20221219161621">
<meta name="zs-published" content="20221219161621">
<meta name="zs-visibility" content="public">
</head>
<body>
<h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1>
<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>,
...
```


















=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053600.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
id: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220908163514

The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/p/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__.
By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated.

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/v/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
If successful, the output is a JSON object:
```sh
# curl http://127.0.0.1:23123/p/00001012053600
[{"t":"Para","i":[{"t":"Text","s":"The"},{"t":"Space"},{"t":"Link","q":"zettel","s":"00001012920000","i":[{"t":"Text","s":"endpoint"}]},{"t":"Space"},{"t":"Text","s":"to"},{"t":"Space"},{"t":"Text","s":"work"},{"t":"Space"},{"t":"Text","s":"with"},{"t":"Space"}, ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.






|

|




|
|

|
|







|









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: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230807170019

The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__.
By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated.

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Deleted docs/manual/00001012053800.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
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
id: 00001012053800
title: API: Retrieve context of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220202112607

The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel.
Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]].

The context is defined by a __direction__, a __depth__, and a __limit__:
* Direction: connections are directed.
  For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''forward'' list all zettel to which the current zettel links to.
  When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"".
  All other values, including a missing value, is interpreted as ""both"".
* Depth: a direct connection has depth 1, an indirect connection is the length of the shortest path between two zettel.
  You should limit the depth by using the parameter ''depth''.
  Its default value is ""5"".
  A value of ""0"" does disable any depth check.
* Limit: to set an upper bound for the returned context, you should use the parameter ''limit''.
  Its default value is ""200"".
  A value of ""0"" disables does not limit the number of elements returned.

The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]].
This zettel is connected to all other zettel.
If it is included, the context would become too big and therefore unusable.

To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t].

````
# curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&depth=2'
{"id": "00001012053800","meta": {...},"rights":62,"list":[{"id": "00001012921000","meta": {...},"rights":62},{"id": "00001012920800","meta": {...},"rights":62},{"id": "00010000000000","meta": {...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001012053800",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001012921000",
      "meta": {...},
      "rights":62
    },
    {
      "id": "00001012920800",
      "meta": {...},
      "rights":62
    },
    {
      "id": "00010000000000",
      "meta": {...},
      "rights":62
    }
  ]
}
````
=== Keys
The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the context was requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that contains the zettel of the context.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<






























































































































































Deleted docs/manual/00001012053900.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
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
id: 00001012053900
title: API: Retrieve unlinked references to an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20220913152019

The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.

__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel.

To retrieve unlinked references to an existing zettel, use the [[endpoint|00001012920000]] ''/u/{ID}''.

````
# curl 'http://127.0.0.1:23123/u/00001007000000'
{"id": "00001007000000","meta": {...},"rights":62,"list": [{"id": "00001012070500","meta": {...},"rights":62},...{"id": "00001006020000","meta": {...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001007000000",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001012070500",
      "meta": {...},
      "rights": 62
    },
    ...
    {
      "id": "00001006020000",
      "meta": {...},
      "rights": 62
    }
  ]
}
````

This call searches within all zettel whether the title of the specified zettel occurs there.
The other zettel must not link to the specified zettel.
The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting.
The match must be exact, but is case-insensitive.

If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references,
you can specify the title phase to be searched for as a query parameter ''phrase'':

````
# curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown'
{"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]}
````

%%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content.

=== Keys
The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the unlinked references were requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe zettel with unlinked references.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
































































































































































Deleted docs/manual/00001012054000.zettel.

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
id: 00001012054000
title: API: Retrieve zettel order within an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220202112451

Some zettel act as a ""table of contents"" for other zettel.
The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another.
Every zettel with a certain internal structure can act as the ""table of contents"" for others.

What is a ""table of contents""?
Basically, it is just a list of references to other zettel.

To retrieve the ""table of contents"", the software looks at first level [[list items|00001007030200]].
If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the table of contents.

This applies only to first level list items (ordered or unordered list), but not to deeper levels.
Only the first reference to a valid zettel is collected for the table of contents.
Following references to zettel within such an list item are ignored.

To retrieve the zettel order of an existing zettel, use the [[endpoint|00001012920000]] ''/o/{ID}''.

````
# curl http://127.0.0.1:23123/o/00001000000000
{"id":"00001000000000","meta":{...},"rights":62,"list":[{"id":"00001001000000","meta":{...},"rights":62},{"id":"00001002000000","meta":{...},"rights":62},{"id":"00001003000000","meta":{...},"rights":62},{"id":"00001004000000","meta":{...},"rights":62},...,{"id":"00001014000000","meta":{...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001000000000",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001001000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001002000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001003000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001004000000",
      "meta": {...},
      "rights": 62
    },
    ...
    {
      "id": "00001014000000",
      "meta": {...},
      "rights": 62
    }
  ]
}
````

The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the references were requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe other zettel in the defined order.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































Changes to docs/manual/00001012054200.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
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk

modified: 20211124180943

Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]].
In both cases you must provide the data for the new or updated zettel in the body of the HTTP request.

One difference is the endpoint.
The [[endpoint|00001012920000]] to update a zettel is ''/j/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP PUT request to that endpoint:

```
# curl -X PUT --data '{}' http://127.0.0.1:23123/j/00001012054200
```
This will put some empty content and metadata to the zettel you are currently reading.
As usual, some metadata will be calculated if it is empty.

The body of the HTTP response is empty, if the request was successful.

[!plain]Alternatively, you can use the [[endpoint|00001012920000]] ''/z/{ID}'' to update a zettel.
In this case, the zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```








=== HTTP Status codes
; ''204''
: Update was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the request body was not valid.
; ''403''
: You are not allowed to delete the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.





>
|





|
|

<
<
<
<
<
<
<
<
<
|

>

|

>
>
>
>
>
>
>












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
id: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20230807165948

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#