Zettelstore

Check-in Differences
Login

Check-in Differences

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

Difference From v0.10.1 To trunk

2024-11-28
13:29
Manual: remove info about previous new-style zids; fix some typos ... (Leaf check-in: d5f6fa92b2 user: stern tags: trunk)
2024-11-27
17:18
Remove support of mapping zids to previous new-style zids. ... (check-in: ca58093de8 user: stern tags: trunk)
2023-01-30
12:49
Merge in 0.10.1 ... (check-in: ed2d3ce05d user: stern tags: trunk)
12:40
Version 0.10.1 ... (Leaf check-in: 70b0cdc454 user: stern tags: release, release-0.10, v0.10.1)
2023-01-24
21:47
Version 0.10.0 ... (check-in: b6301cf091 user: stern tags: trunk, release, v0.10.0)

Changes to .fossil-settings/ignore-glob.

1
2
3
bin/*
releases/*
parser/pikchr/*.out


<
1
2

bin/*
releases/*

Changes to LICENSE.txt.

1
2
3
4
5
6
7
8
Copyright (c) 2020-2023 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
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

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

.PHONY:  check relcheck api build release clean

check:
	go run tools/build.go check

relcheck:
	go run tools/build.go relcheck

api:
	go run tools/build.go testapi

version:
	@echo $(shell go run tools/build.go version)

build:
	go run tools/build.go build

release:
	go run tools/build.go release

clean:
	go run tools/build.go clean

|







|


|


|


|


|


|


|


|
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

## 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 version build release clean

check:
	go run tools/check/check.go

relcheck:
	go run tools/check/check.go -r

api:
	go run tools/testapi/testapi.go

version:
	@echo $(shell go run tools/build/build.go version)

build:
	go run tools/build/build.go build

release:
	go run tools/build/build.go release

clean:
	go run tools/clean/clean.go

Changes to README.md.

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
gradually, one major focus is a long-term store of these notes, hence the name
&ldquo;Zettelstore&rdquo;.

To get an initial impression, take a look at the
[manual](https://zettelstore.de/manual/). It is a live example of the
zettelstore software, running in read-only mode.

[Zettelstore Client](https://zettelstore.de/client) provides client
software to access Zettelstore via its API more easily, [Zettelstore
Contrib](https://zettelstore.de/contrib) contains contributed software, which
often connects to Zettelstore via its API. Some of the software packages may be
experimental.

The software, including the manual, is licensed
under the [European Union Public License 1.2 (or
later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk).

[Stay](https://twitter.com/zettelstore)
<a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a>&nbsp;&hellip;







|
|








<
|
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

26
gradually, one major focus is a long-term store of these notes, hence the name
&ldquo;Zettelstore&rdquo;.

To get an initial impression, take a look at the
[manual](https://zettelstore.de/manual/). It is a live example of the
zettelstore software, running in read-only mode.

[Zettelstore Client](https://t73f.de/r/zsc) provides client software to access
Zettelstore via its API more easily, [Zettelstore
Contrib](https://zettelstore.de/contrib) contains contributed software, which
often connects to Zettelstore via its API. Some of the software packages may be
experimental.

The software, including the manual, is licensed
under the [European Union Public License 1.2 (or
later)](https://zettelstore.de/home/file?name=LICENSE.txt&ci=trunk).


[Stay tuned](https://mastodon.social/tags/Zettelstore)&nbsp;&hellip;

Changes to VERSION.

1
0.10.1
|
1
0.19.0-dev

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
34
35
36
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"net/url"

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

// 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-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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
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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import "t73f.de/r/zsc/attrs"

// Definition of Block nodes.

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

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

Changes to ast/inline.go.

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

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

// CreateInlineSliceFromWords makes a new inline list from words,
// that will be space-separated.
func CreateInlineSliceFromWords(words ...string) InlineSlice {
	inl := make(InlineSlice, 0, 2*len(words)-1)
	for i, word := range words {
		if i > 0 {
			inl = append(inl, &SpaceNode{Lexeme: " "})
		}
		inl = append(inl, &TextNode{Text: word})
	}
	return inl
}

// WalkChildren walks down to the list.
func (is *InlineSlice) WalkChildren(v Visitor) {
	for _, in := range *is {
		Walk(v, in)
	}
}

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

// TextNode just contains some text.
type TextNode struct {
	Text string // The text itself.
}

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
}

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

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

// Count returns the number of space runes.
func (sn *SpaceNode) Count() int {
	return utf8.RuneCountInString(sn.Lexeme)
}

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

// BreakNode signals a new line that must / should be interpreted as a new line break.
type BreakNode struct {
	Hard bool // Hard line break?
}


|






>
>
>





<
<
|









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



















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







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

package ast

import (


	"t73f.de/r/zsc/attrs"
)

// Definitions of inline nodes.

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

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














// WalkChildren walks down to the list.
func (is *InlineSlice) WalkChildren(v Visitor) {
	for _, in := range *is {
		Walk(v, in)
	}
}

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

// TextNode just contains some text.
type TextNode struct {
	Text string // The text itself.
}

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

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


















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

// BreakNode signals a new line that must / should be interpreted as a new line break.
type BreakNode struct {
	Hard bool // Hard line break?
}

191
192
193
194
195
196
197
198
199
200
201
202
203
204

205
206
207
208
209
210
211
212

// FormatKind specifies the format that is applied to the inline nodes.
type FormatKind int

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text.
	FormatStrong            // Strongly emphasized text.
	FormatInsert            // Inserted text.
	FormatDelete            // Deleted text.
	FormatSuper             // Superscripted text.
	FormatSub               // SubscriptedText.
	FormatQuote             // Quoted text.

	FormatSpan              // Generic inline container.
)

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

// WalkChildren walks to the formatted text.
func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }








|
|
|
|
|
|
|
>
|







162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184

// FormatKind specifies the format that is applied to the inline nodes.
type FormatKind int

// Constants for FormatCode
const (
	_            FormatKind = iota
	FormatEmph              // Emphasized text
	FormatStrong            // Strongly emphasized text
	FormatInsert            // Inserted text
	FormatDelete            // Deleted text
	FormatSuper             // Superscripted text
	FormatSub               // SubscriptedText
	FormatQuote             // Quoted text
	FormatMark              // Marked text
	FormatSpan              // Generic inline container
)

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

// WalkChildren walks to the formatted text.
func (fn *FormatNode) WalkChildren(v Visitor) { Walk(v, &fn.Inlines) }

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
//-----------------------------------------------------------------------------
// 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 invalidReference(s) {
		return &Reference{URL: nil, Value: s, State: RefStateInvalid}
	}
	if strings.HasPrefix(s, QueryPrefix) {

|






>
>
>








>
|



|







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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package ast

import (
	"net/url"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/id"
)

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

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

Changes to ast/ref_test.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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_test

import (
	"testing"


|






>
>
>







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

package ast_test

import (
	"testing"

Changes to ast/walk.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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

// Visitor is a visitor for walking the AST.
type Visitor interface {
	Visit(node Node) Visitor

|






>
>
>







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

package ast

// Visitor is a visitor for walking the AST.
type Visitor interface {
	Visit(node Node) Visitor

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
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
//-----------------------------------------------------------------------------
// 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"),
		},
		&ast.ParaNode{
			Inlines: ast.CreateInlineSliceFromWords("This", "is", "the", "introduction."),
		},
		&ast.NestedListNode{
			Kind: ast.NestedListUnordered,
			Items: []ast.ItemSlice{
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.CreateInlineSliceFromWords("Item", "1"),
					},
				},
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.CreateInlineSliceFromWords("Item", "2"),
					},
				},
			},
		},
		&ast.ParaNode{
			Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."),
		},
		ast.CreateParaNode(
			&ast.FormatNode{
				Kind: ast.FormatEmph,
				Attrs: attrs.Attributes(map[string]string{
					"":      "class",
					"color": "green",
				}),
				Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."),
			},
			&ast.SpaceNode{Lexeme: " "},
			&ast.LinkNode{
				Ref:     &ast.Reference{Value: "http://zettelstore.de"},
				Inlines: ast.CreateInlineSliceFromWords("URL", "text."),
			},
		),
	}
	v := benchVisitor{}
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		ast.Walk(&v, &root)
	}
}

type benchVisitor struct{}

func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv }

|






>
>
>







|






|


|






|




|





|








|

|


|





|







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

package ast_test

import (
	"testing"

	"t73f.de/r/zsc/attrs"
	"zettelstore.de/z/ast"
)

func BenchmarkWalk(b *testing.B) {
	root := ast.BlockSlice{
		&ast.HeadingNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "A Simple Heading"}},
		},
		&ast.ParaNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is the introduction."}},
		},
		&ast.NestedListNode{
			Kind: ast.NestedListUnordered,
			Items: []ast.ItemSlice{
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 1"}},
					},
				},
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 2"}},
					},
				},
			},
		},
		&ast.ParaNode{
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some intermediate text."}},
		},
		ast.CreateParaNode(
			&ast.FormatNode{
				Kind: ast.FormatEmph,
				Attrs: attrs.Attributes(map[string]string{
					"":      "class",
					"color": "green",
				}),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some emphasized text."}},
			},
			&ast.TextNode{Text: " "},
			&ast.LinkNode{
				Ref:     &ast.Reference{Value: "http://zettelstore.de"},
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}},
			},
		),
	}
	v := benchVisitor{}
	b.ResetTimer()
	for range b.N {
		ast.Walk(&v, &root)
	}
}

type benchVisitor struct{}

func (bv *benchVisitor) Visit(ast.Node) ast.Visitor { return bv }

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

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







|
|







41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

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

	// User is allowed to read zettel
	CanRead(user, m *meta.Meta) bool

	// User is allowed to write zettel.
	CanWrite(user, oldMeta, newMeta *meta.Meta) bool

	// User is allowed to rename zettel
	CanRename(user, m *meta.Meta) bool

	// User is allowed to delete zettel.
	CanDelete(user, m *meta.Meta) bool

	// User is allowed to refresh box data.
	CanRefresh(user *meta.Meta) bool
}







<
<
<






91
92
93
94
95
96
97



98
99
100
101
102
103

	// User is allowed to read zettel
	CanRead(user, m *meta.Meta) bool

	// User is allowed to write zettel.
	CanWrite(user, oldMeta, newMeta *meta.Meta) bool




	// User is allowed to delete zettel.
	CanDelete(user, m *meta.Meta) bool

	// User is allowed to refresh box data.
	CanRefresh(user *meta.Meta) bool
}

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-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 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
26
27
28
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"bytes"

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

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







|






43
44
45
46
47
48
49
50
51
52
53
54
55
56
		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
84
85
86
87
88
89
//-----------------------------------------------------------------------------
// Copyright (c) 2023-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2023-present Detlef Stern
//-----------------------------------------------------------------------------

package impl

import (
	"bytes"
	"crypto"
	"crypto/hmac"
	"encoding/base64"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxreader"
)

var encoding = base64.RawURLEncoding

const digestAlg = crypto.SHA384

func sign(claim sx.Object, secret []byte) ([]byte, error) {
	var buf bytes.Buffer
	_, err := sx.Print(&buf, claim)
	if err != nil {
		return nil, err
	}
	token := make([]byte, encoding.EncodedLen(buf.Len()))
	encoding.Encode(token, buf.Bytes())

	digest := hmac.New(digestAlg.New, secret)
	_, err = digest.Write(buf.Bytes())
	if err != nil {
		return nil, err
	}
	dig := digest.Sum(nil)
	encDig := make([]byte, encoding.EncodedLen(len(dig)))
	encoding.Encode(encDig, dig)

	token = append(token, '.')
	token = append(token, encDig...)
	return token, nil
}

func check(token []byte, secret []byte) (sx.Object, error) {
	i := bytes.IndexByte(token, '.')
	if i <= 0 || 1024 < i {
		return nil, ErrMalformedToken
	}
	buf := make([]byte, len(token))
	n, err := encoding.Decode(buf, token[:i])
	if err != nil {
		return nil, err
	}
	rdr := sxreader.MakeReader(bytes.NewReader(buf[:n]))
	obj, err := rdr.Read()
	if err != nil {
		return nil, err
	}

	var objBuf bytes.Buffer
	_, err = sx.Print(&objBuf, obj)
	if err != nil {
		return nil, err
	}

	digest := hmac.New(digestAlg.New, secret)
	_, err = digest.Write(objBuf.Bytes())
	if err != nil {
		return nil, err
	}

	n, err = encoding.Decode(buf, token[i+1:])
	if err != nil {
		return nil, err
	}
	if !hmac.Equal(buf[:n], digest.Sum(nil)) {
		return nil, ErrMalformedToken
	}
	return obj, nil
}

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

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

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


	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"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
}







|
>


















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






|
>
|
>

|

|
>
>
>
>
>
>
>

|

<
<
<
|
>

|

|

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







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
	}
	return h.Sum(nil)
}

// IsReadonly returns true, if the systems is configured to run in read-only-mode.
func (a *myAuth) IsReadonly() bool { return a.readonly }

// ErrMalformedToken signals a broken token.
var ErrMalformedToken = errors.New("auth: malformed token")

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

// ErrOtherKind signals that the token was defined for another token kind.
var ErrOtherKind = errors.New("auth: wrong token kind")

// ErrNoZid signals that the 'zid' key is missing.
var ErrNoZid = errors.New("auth: missing zettel id")

// GetToken returns a token to be used for authentification.
func (a *myAuth) GetToken(ident *meta.Meta, d time.Duration, kind auth.TokenKind) ([]byte, error) {
	subject, ok := ident.Get(api.KeyUserID)
	if !ok || subject == "" {
		return nil, ErrNoIdent
	}

	now := time.Now().Round(time.Second)
	sClaim := sx.MakeList(
		sx.Int64(kind),
		sx.MakeString(subject),
		sx.Int64(now.Unix()),
		sx.Int64(now.Add(d).Unix()),



		sx.Int64(ident.Zid),





	)
	return sign(sClaim, a.secret)


}

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

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

	obj, err := check(tok, a.secret)
	if err != nil {
		return tokenData, err
	}

	tokenData.Token = tok
	err = setupTokenData(obj, k, &tokenData)
	return tokenData, err
}

func setupTokenData(obj sx.Object, k auth.TokenKind, tokenData *auth.TokenData) error {
	vals, err := sexp.ParseList(obj, "isiii")
	if err != nil {
		return ErrMalformedToken
	}



	if auth.TokenKind(vals[0].(sx.Int64)) != k {
		return ErrOtherKind
	}
	ident := vals[1].(sx.String).GetValue()
	if ident == "" {
		return ErrNoIdent
	}
	issued := time.Unix(int64(vals[2].(sx.Int64)), 0)
	expires := time.Unix(int64(vals[3].(sx.Int64)), 0)




	now := time.Now().Round(time.Second)

	if expires.Before(now) {
		return ErrTokenExpired


	}

	zid := id.Zid(vals[4].(sx.Int64))
	if !zid.IsValid() {
		return ErrNoZid
	}

	tokenData.Ident = string(ident)
	tokenData.Issued = issued
	tokenData.Now = now
	tokenData.Expires = expires
	tokenData.Zid = zid
	return nil
}

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
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) 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 (
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
)

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

func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	return ap.pre.CanCreate(user, newMeta)
}

func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool {
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta)
}

func (ap *anonPolicy) CanRename(user, m *meta.Meta) bool {
	return ap.pre.CanRename(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool {
	return ap.pre.CanDelete(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool {
	if ap.authConfig.GetExpertMode() || ap.authConfig.GetSimpleMode() {
		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
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-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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
}

func (ap *anonPolicy) CanCreate(user, newMeta *meta.Meta) bool {
	return ap.pre.CanCreate(user, newMeta)
}

func (ap *anonPolicy) CanRead(user, m *meta.Meta) bool {
	return ap.pre.CanRead(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return ap.pre.CanWrite(user, oldMeta, newMeta) && ap.checkVisibility(oldMeta)
}





func (ap *anonPolicy) CanDelete(user, m *meta.Meta) bool {
	return ap.pre.CanDelete(user, m) && ap.checkVisibility(m)
}

func (ap *anonPolicy) CanRefresh(user *meta.Meta) bool {
	if ap.authConfig.GetExpertMode() || ap.authConfig.GetSimpleMode() {
		return true

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
35
36
37
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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















|







|
|

|


|
|

|


|


>
>
>
>













<
<
<
<
<
<
<
<
|



|


|



|



|


|



|



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







|




|












>
>
>
>
>
>
>
>
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
	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) 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)
}
func (pp *polBox) ReIndex(ctx context.Context, zid id.Zid) error {
	user := server.GetUser(ctx)
	if pp.policy.CanRefresh(user) {
		// If a user is allowed to refresh all data, it it also allowed to re-index a zettel.
		return pp.box.ReIndex(ctx, zid)
	}
	return box.NewErrNotAllowed("ReIndex", user, zid)
}

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
24
25
26
27
28
29
30
31
32
33
34
35
//-----------------------------------------------------------------------------
// 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 (
	"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 }
func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool   { return true }
func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool {
	return d.canChange(user, oldMeta)
}
func (d *defaultPolicy) CanRename(user, m *meta.Meta) bool { return d.canChange(user, m) }
func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) }

func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil }

func (d *defaultPolicy) canChange(user, m *meta.Meta) bool {
	metaRo, ok := m.Get(api.KeyReadOnly)
	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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

import (
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/zettel/meta"
)

type defaultPolicy struct {
	manager auth.AuthzManager
}

func (*defaultPolicy) CanCreate(_, _ *meta.Meta) bool { return true }
func (*defaultPolicy) CanRead(_, _ *meta.Meta) bool   { return true }
func (d *defaultPolicy) CanWrite(user, oldMeta, _ *meta.Meta) bool {
	return d.canChange(user, oldMeta)
}

func (d *defaultPolicy) CanDelete(user, m *meta.Meta) bool { return d.canChange(user, m) }

func (*defaultPolicy) CanRefresh(user *meta.Meta) bool { return user != nil }

func (d *defaultPolicy) canChange(user, m *meta.Meta) bool {
	metaRo, ok := m.Get(api.KeyReadOnly)
	if !ok {

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-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 (
	"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
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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

import (
	"t73f.de/r/zsc/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
}
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
	switch userRole := o.manager.GetUserRole(user); userRole {
	case meta.UserRoleReader, meta.UserRoleCreator:
		return false
	}
	return o.userCanCreate(user, newMeta)
}

func (o *ownerPolicy) CanRename(user, m *meta.Meta) bool {
	if user == nil || !o.pre.CanRename(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok {
		return res
	}
	return o.userIsOwner(user)
}

func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool {
	if user == nil || !o.pre.CanDelete(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok {
		return res
	}







<
<
<
<
<
<
<
<
<
<







111
112
113
114
115
116
117










118
119
120
121
122
123
124
	switch userRole := o.manager.GetUserRole(user); userRole {
	case meta.UserRoleReader, meta.UserRoleCreator:
		return false
	}
	return o.userCanCreate(user, newMeta)
}











func (o *ownerPolicy) CanDelete(user, m *meta.Meta) bool {
	if user == nil || !o.pre.CanDelete(user, m) {
		return false
	}
	if res, ok := o.checkVisibility(user, o.authConfig.GetVisibility(m)); ok {
		return res
	}

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-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 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
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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

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

// 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{}
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
}

func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid &&
		p.post.CanWrite(user, oldMeta, newMeta)
}

func (p *prePolicy) CanRename(user, m *meta.Meta) bool {
	return m != nil && p.post.CanRename(user, m)
}

func (p *prePolicy) CanDelete(user, m *meta.Meta) bool {
	return m != nil && p.post.CanDelete(user, m)
}

func (p *prePolicy) CanRefresh(user *meta.Meta) bool {
	return p.post.CanRefresh(user)
}







<
<
<
<







56
57
58
59
60
61
62




63
64
65
66
67
68
69
}

func (p *prePolicy) CanWrite(user, oldMeta, newMeta *meta.Meta) bool {
	return oldMeta != nil && newMeta != nil && oldMeta.Zid == newMeta.Zid &&
		p.post.CanWrite(user, oldMeta, newMeta)
}





func (p *prePolicy) CanDelete(user, m *meta.Meta) bool {
	return m != nil && p.post.CanDelete(user, m)
}

func (p *prePolicy) CanRefresh(user *meta.Meta) bool {
	return p.post.CanRefresh(user)
}

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-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 (
	"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
28
29
30
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package policy

import (
	"fmt"
	"testing"

	"t73f.de/r/zsc/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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
		)
		name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v",
			ts.readonly, ts.withAuth, ts.expert, ts.simple)
		t.Run(name, func(tt *testing.T) {
			testCreate(tt, pol, ts.withAuth, ts.readonly)
			testRead(tt, pol, ts.withAuth, ts.expert)
			testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testRename(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple)
		})
	}
}

type testAuthzManager struct {







<







55
56
57
58
59
60
61

62
63
64
65
66
67
68
		)
		name := fmt.Sprintf("readonly=%v/withauth=%v/expert=%v/simple=%v",
			ts.readonly, ts.withAuth, ts.expert, ts.simple)
		t.Run(name, func(tt *testing.T) {
			testCreate(tt, pol, ts.withAuth, ts.readonly)
			testRead(tt, pol, ts.withAuth, ts.expert)
			testWrite(tt, pol, ts.withAuth, ts.readonly, ts.expert)

			testDelete(tt, pol, ts.withAuth, ts.readonly, ts.expert)
			testRefresh(tt, pol, ts.withAuth, ts.expert, ts.simple)
		})
	}
}

type testAuthzManager struct {
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
487
488
489
		{owner, roTrue, roTrue, false},
		{owner2, roTrue, roTrue, false},
	}
	for _, tc := range testCases {
		t.Run("Write", func(tt *testing.T) {
			got := pol.CanWrite(tc.user, tc.old, tc.new)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testRename(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {
	t.Helper()
	anonUser := newAnon()
	creator := newCreator()
	reader := newReader()
	writer := newWriter()
	owner := newOwner()
	owner2 := newOwner2()
	zettel := newZettel()
	expertZettel := newExpertZettel()
	roFalse := newRoFalseZettel()
	roTrue := newRoTrueZettel()
	roReader := newRoReaderZettel()
	roWriter := newRoWriterZettel()
	roOwner := newRoOwnerZettel()
	notAuthNotReadonly := !withAuth && !readonly
	testCases := []struct {
		user *meta.Meta
		meta *meta.Meta
		exp  bool
	}{
		// No meta
		{anonUser, nil, false},
		{creator, nil, false},
		{reader, nil, false},
		{writer, nil, false},
		{owner, nil, false},
		{owner2, nil, false},
		// Any zettel
		{anonUser, zettel, notAuthNotReadonly},
		{creator, zettel, notAuthNotReadonly},
		{reader, zettel, notAuthNotReadonly},
		{writer, zettel, notAuthNotReadonly},
		{owner, zettel, !readonly},
		{owner2, zettel, !readonly},
		// Expert zettel
		{anonUser, expertZettel, notAuthNotReadonly && expert},
		{creator, expertZettel, notAuthNotReadonly && expert},
		{reader, expertZettel, notAuthNotReadonly && expert},
		{writer, expertZettel, notAuthNotReadonly && expert},
		{owner, expertZettel, !readonly && expert},
		{owner2, expertZettel, !readonly && expert},
		// No r/o zettel
		{anonUser, roFalse, notAuthNotReadonly},
		{creator, roFalse, notAuthNotReadonly},
		{reader, roFalse, notAuthNotReadonly},
		{writer, roFalse, notAuthNotReadonly},
		{owner, roFalse, !readonly},
		{owner2, roFalse, !readonly},
		// Reader r/o zettel
		{anonUser, roReader, false},
		{creator, roReader, false},
		{reader, roReader, false},
		{writer, roReader, notAuthNotReadonly},
		{owner, roReader, !readonly},
		{owner2, roReader, !readonly},
		// Writer r/o zettel
		{anonUser, roWriter, false},
		{creator, roWriter, false},
		{reader, roWriter, false},
		{writer, roWriter, false},
		{owner, roWriter, !readonly},
		{owner2, roWriter, !readonly},
		// Owner r/o zettel
		{anonUser, roOwner, false},
		{creator, roOwner, false},
		{reader, roOwner, false},
		{writer, roOwner, false},
		{owner, roOwner, false},
		{owner2, roOwner, false},
		// r/o = true zettel
		{anonUser, roTrue, false},
		{creator, roTrue, false},
		{reader, roTrue, false},
		{writer, roTrue, false},
		{owner, roTrue, false},
		{owner2, roTrue, false},
	}
	for _, tc := range testCases {
		t.Run("Rename", func(tt *testing.T) {
			got := pol.CanRename(tc.user, tc.meta)
			if tc.exp != got {
				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert bool) {







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







390
391
392
393
394
395
396
























































































397
398
399
400
401
402
403
		{owner, roTrue, roTrue, false},
		{owner2, roTrue, roTrue, false},
	}
	for _, tc := range testCases {
		t.Run("Write", func(tt *testing.T) {
			got := pol.CanWrite(tc.user, tc.old, tc.new)
			if tc.exp != got {
























































































				tt.Errorf("exp=%v, but got=%v", tc.exp, got)
			}
		})
	}
}

func testDelete(t *testing.T, pol auth.Policy, withAuth, readonly, expert 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-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 "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

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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

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

	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

















// ZidFunc is a function that processes identifier of a zettel.
type ZidFunc func(id.Zid)

// MetaFunc is a function that processes metadata of a zettel.
type MetaFunc func(*meta.Meta)

// 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 {
	// ReadOnly indicates that boxes cannot be modified.
	ReadOnly 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
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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

import (
	"context"
	"errors"
	"fmt"
	"io"


	"time"

	"t73f.de/r/zsc/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








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
















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

	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

// WriteBox is a box that can create / update zettel content.
type WriteBox interface {
	// CanCreateZettel returns true, if box could possibly create a new zettel.
	CanCreateZettel(ctx context.Context) bool

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

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

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

// ZidFunc is a function that processes identifier of a zettel.
type ZidFunc func(id.Zid)

// MetaFunc is a function that processes metadata of a zettel.
type MetaFunc func(*meta.Meta)

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

	// FetchZids returns the set of all zettel identifer managed by the box.
	FetchZids(ctx context.Context) (*id.Set, error)

	// GetMeta returns the metadata of the zettel with the given identifier.
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)

	// SelectMeta returns a list of metadata that comply to the given selection criteria.
	// If `metaSeq` is `nil`, the box assumes metadata of all available zettel.
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)

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

	// ReIndex one zettel to update its index data.
	ReIndex(context.Context, id.Zid) error
}

// Stats record stattistics about a box.
type Stats struct {
	// ReadOnly indicates that boxes cannot be modified.
	ReadOnly bool

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

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




// Subject is a box that notifies observers about changes.
type Subject interface {
	// RegisterObserver registers an observer that will be notified
	// if one or all zettel are found to be changed.
	RegisterObserver(UpdateFunc)
}







>

|
>




|






>
>
>







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

// 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 an existing zettel happened
	OnDelete              // A zettel was deleted
)

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

// UpdateNotifier is an UpdateFunc, but with separate values.
type UpdateNotifier func(BaseBox, id.Zid, UpdateReason)

// Subject is a box that notifies observers about changes.
type Subject interface {
	// RegisterObserver registers an observer that will be notified
	// if one or all zettel are found to be changed.
	RegisterObserver(UpdateFunc)
}
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

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
	}







|
|

|







244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
	return context.WithValue(ctx, ctxNoEnrichKey, &ctxNoEnrichKey)
}

type ctxNoEnrichType struct{}

var ctxNoEnrichKey ctxNoEnrichType

// DoEnrich determines if the context is not marked to not enrich metadata.
func DoEnrich(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
	}
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

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







|
>
|
>








|
|

|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327


























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

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

type compBox struct {
	log      *logger.Logger
	number   int
	enricher box.Enricher
}

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
	content func(*meta.Meta) []byte
}{
	id.MustParse(api.ZidVersion):              {genVersionBuildM, genVersionBuildC},
	id.MustParse(api.ZidHost):                 {genVersionHostM, genVersionHostC},
	id.MustParse(api.ZidOperatingSystem):      {genVersionOSM, genVersionOSC},
	id.MustParse(api.ZidLog):                  {genLogM, genLogC},






	id.MustParse(api.ZidBoxManager):           {genManagerM, genManagerC},


	id.MustParse(api.ZidMetadataKey):          {genKeysM, genKeysC},
	id.MustParse(api.ZidParser):               {genParserM, genParserC},
	id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC},
}

// Get returns the one program box.
func getCompBox(boxNumber int, mf box.Enricher) box.ManagedBox {
	return &compBox{
		log: kernel.Main.GetLogger(kernel.BoxService).Clone().
			Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(),
		number:   boxNumber,
		enricher: mf,
	}
}

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

|






>
>
>









|


<
<
<



>
>
>





|













|

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






|













<
<
<
<
<
<
<
|




|
|

|


|
|


>
|
|


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

|







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

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

import (
	"context"
	"net/url"

	"t73f.de/r/zsc/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(_ *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
			return getCompBox(cdata.Number, cdata.Enricher), nil
		})
}

type compBox struct {
	log      *logger.Logger
	number   int
	enricher box.Enricher
}

var myConfig *meta.Meta
var myZettel = map[id.Zid]struct {
	meta    func(id.Zid) *meta.Meta
	content func(context.Context, *compBox) []byte
}{
	id.MustParse(api.ZidVersion):         {genVersionBuildM, genVersionBuildC},
	id.MustParse(api.ZidHost):            {genVersionHostM, genVersionHostC},
	id.MustParse(api.ZidOperatingSystem): {genVersionOSM, genVersionOSC},
	id.MustParse(api.ZidLog):             {genLogM, genLogC},
	id.MustParse(api.ZidMemory):          {genMemoryM, genMemoryC},
	id.MustParse(api.ZidSx):              {genSxM, genSxC},
	// id.MustParse(api.ZidHTTP):                 {genHttpM, genHttpC},
	// id.MustParse(api.ZidAPI):                  {genApiM, genApiC},
	// id.MustParse(api.ZidWebUI):                {genWebUiM, genWebUiC},
	// id.MustParse(api.ZidConsole):              {genConsoleM, genConsoleC},
	id.MustParse(api.ZidBoxManager): {genManagerM, genManagerC},
	// id.MustParse(api.ZidIndex):                {genIndexM, genIndexC},
	// id.MustParse(api.ZidQuery):                {genQueryM, genQueryC},
	id.MustParse(api.ZidMetadataKey):          {genKeysM, genKeysC},
	id.MustParse(api.ZidParser):               {genParserM, genParserC},
	id.MustParse(api.ZidStartupConfiguration): {genConfigZettelM, genConfigZettelC},
}

// Get returns the one program box.
func getCompBox(boxNumber int, mf box.Enricher) *compBox {
	return &compBox{
		log: kernel.Main.GetLogger(kernel.BoxService).Clone().
			Str("box", "comp").Int("boxnum", int64(boxNumber)).Child(),
		number:   boxNumber,
		enricher: mf,
	}
}

// Setup remembers important values.
func Setup(cfg *meta.Meta) { myConfig = cfg.Clone() }

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








func (cb *compBox) GetZettel(ctx 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(ctx, cb)),
				}, 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, 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)
	}
}







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


|
<


>
>










>
>
>
>
>
>






>
>
>






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






















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 getTitledMeta(zid id.Zid, title string) *meta.Meta {
	m := meta.New(zid)
	m.Set(api.KeyTitle, title)
	return m
}

func updateMeta(m *meta.Meta) {
	if _, ok := m.Get(api.KeySyntax); !ok {
		m.Set(api.KeySyntax, meta.SyntaxZmk)
	}
	m.Set(api.KeyRole, api.ValueRoleConfiguration)
	if _, ok := m.Get(api.KeyCreated); !ok {
		m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	}
	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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//-----------------------------------------------------------------------------
// 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)
	m.Set(api.KeyTitle, "Zettelstore Startup Configuration")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityExpert)
	return m
}

func genConfigZettelC(*meta.Meta) []byte {
	var buf bytes.Buffer
	for i, p := range myConfig.Pairs() {
		if i > 0 {
			buf.WriteByte('\n')
		}
		buf.WriteString("; ''")
		buf.WriteString(p.Key)

|






>
>
>






>

|
|
<
<






<
|
<
<
<


|







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

package compbox

import (
	"bytes"
	"context"

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


)

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

	return getTitledMeta(zid, "Zettelstore Startup Configuration")



}

func genConfigZettelC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	for i, p := range myConfig.Pairs() {
		if i > 0 {
			buf.WriteByte('\n')
		}
		buf.WriteString("; ''")
		buf.WriteString(p.Key)

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

	"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
41
42
43
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"
	"fmt"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func genKeysM(zid id.Zid) *meta.Meta {

	m := getTitledMeta(zid, "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(context.Context, *compBox) []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
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 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, 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"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer
	for _, entry := range entries {
		ts := entry.TS.Format(tsFormat)
		buf.WriteString(ts)
		for j := len(ts); j < len(tsFormat); j++ {

|






>
>
>






>

|
|
|
|



<
|

<
|



|







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

package compbox

import (
	"bytes"
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func genLogM(zid id.Zid) *meta.Meta {

	m := getTitledMeta(zid, "Zettelstore Log")
	m.Set(api.KeySyntax, meta.SyntaxText)

	m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))
	return m
}

func genLogC(context.Context, *compBox) []byte {
	const tsFormat = "2006-01-02 15:04:05.999999"
	entries := kernel.Main.RetrieveLogEntries()
	var buf bytes.Buffer
	for _, entry := range entries {
		ts := entry.TS.Format(tsFormat)
		buf.WriteString(ts)
		for j := len(ts); j < len(tsFormat); j++ {

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
28
29
30
31
32
33
34
35
36
37
//-----------------------------------------------------------------------------
// 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"

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

func genManagerC(*meta.Meta) []byte {
	kvl := kernel.Main.GetServiceStatistics(kernel.BoxService)
	if len(kvl) == 0 {
		return nil
	}
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	for _, kv := range kvl {

|






>
>
>






>


<
|
|
|



<
|
<
<


|







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

package compbox

import (
	"bytes"
	"context"
	"fmt"


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

func genManagerM(zid id.Zid) *meta.Meta {

	return getTitledMeta(zid, "Zettelstore Box Manager")


}

func genManagerC(context.Context, *compBox) []byte {
	kvl := kernel.Main.GetServiceStatistics(kernel.BoxService)
	if len(kvl) == 0 {
		return nil
	}
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	for _, kv := range kvl {

Added box/compbox/memory.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
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"runtime"

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

func genMemoryM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Memory")
}

func genMemoryC(context.Context, *compBox) []byte {
	pageSize := os.Getpagesize()
	var m runtime.MemStats
	runtime.GC()
	runtime.ReadMemStats(&m)

	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	fmt.Fprintf(&buf, "|Page Size|%d\n", pageSize)
	fmt.Fprintf(&buf, "|Pages|%d\n", m.HeapSys/uint64(pageSize))
	fmt.Fprintf(&buf, "|Heap Objects|%d\n", m.HeapObjects)
	fmt.Fprintf(&buf, "|Heap Sys (KiB)|%d\n", m.HeapSys/1024)
	fmt.Fprintf(&buf, "|Heap Inuse (KiB)|%d\n", m.HeapInuse/1024)
	fmt.Fprintf(&buf, "|CPUs|%d\n", runtime.NumCPU())
	fmt.Fprintf(&buf, "|Threads|%d\n", runtime.NumGoroutine())
	debug := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool)
	if debug {
		for i, bysize := range m.BySize {
			fmt.Fprintf(&buf, "|Size %2d: %d|%d - %d &rarr; %d\n",
				i, bysize.Size, bysize.Mallocs, bysize.Frees, bysize.Mallocs-bysize.Frees)
		}
	}
	return buf.Bytes()
}

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

	"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?:|=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()
}

|






>
>
>






>

|


|
|
|
|
|



<
|





|



|






|






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

package compbox

import (
	"bytes"
	"context"
	"fmt"
	"slices"
	"strings"

	"t73f.de/r/zsc/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 := getTitledMeta(zid, "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(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Syntax<|=Alt. Value(s):|=Text Parser?:|=Text Format?:|=Image Format?:\n")
	syntaxes := parser.GetSyntaxes()
	slices.Sort(syntaxes)
	for _, syntax := range syntaxes {
		info := parser.Get(syntax)
		if info.Name != syntax {
			continue
		}
		altNames := info.AltNames
		slices.Sort(altNames)
		fmt.Fprintf(
			&buf, "|%v|%v|%v|%v|%v\n",
			syntax, strings.Join(altNames, ", "), info.IsASTParser, info.IsTextFormat, info.IsImageFormat)
	}
	return buf.Bytes()
}

Added box/compbox/sx.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
//-----------------------------------------------------------------------------
// Copyright (c) 2024-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2024-present Detlef Stern
//-----------------------------------------------------------------------------

package compbox

import (
	"bytes"
	"context"
	"fmt"

	"t73f.de/r/sx"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func genSxM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Sx Engine")
}

func genSxC(context.Context, *compBox) []byte {
	var buf bytes.Buffer
	buf.WriteString("|=Name|=Value>\n")
	fmt.Fprintf(&buf, "|Symbols|%d\n", sx.MakeSymbol("NIL").Factory().Size())
	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
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
//-----------------------------------------------------------------------------
// 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 (

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

func genVersionBuildM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Version")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}
func genVersionBuildC(*meta.Meta) []byte {
	return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
}

func genVersionHostM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Host")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	return m
}
func genVersionHostC(*meta.Meta) []byte {
	return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string))
}

func genVersionOSM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Operating System")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	return m
}
func genVersionOSC(*meta.Meta) []byte {
	goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string)
	goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string)
	result := make([]byte, 0, len(goOS)+len(goArch)+1)
	result = append(result, goOS...)
	result = append(result, '/')
	return append(result, goArch...)
}

|






>
>
>





>
|
>
|
|
|


<
<
<
<
<
<
<

|




|




|
<
<

|




|
<
<

|







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

package compbox

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)








func genVersionBuildM(zid id.Zid) *meta.Meta {
	m := getTitledMeta(zid, "Zettelstore Version")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVTime).(string))
	m.Set(api.KeyVisibility, api.ValueVisibilityLogin)
	return m
}
func genVersionBuildC(context.Context, *compBox) []byte {
	return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
}

func genVersionHostM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Host")


}
func genVersionHostC(context.Context, *compBox) []byte {
	return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreHostname).(string))
}

func genVersionOSM(zid id.Zid) *meta.Meta {
	return getTitledMeta(zid, "Zettelstore Operating System")


}
func genVersionOSC(context.Context, *compBox) []byte {
	goOS := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoOS).(string)
	goArch := kernel.Main.GetConfig(kernel.CoreService, kernel.CoreGoArch).(string)
	result := make([]byte, 0, len(goOS)+len(goArch)+1)
	result = append(result, goOS...)
	result = append(result, '/')
	return append(result, goArch...)
}

Changes to box/constbox/base.css.















1
2
3
4
5
6
7
8
9
10
11
12














*,*::before,*::after {
    box-sizing: border-box;
  }
  html {
    font-size: 1rem;
    font-family: serif;
    scroll-behavior: smooth;
    height: 100%;
  }
  body {
    margin: 0;
    min-height: 100vh;
>
>
>
>
>
>
>
>
>
>
>
>
>
>




<







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.
 *
 * SPDX-License-Identifier: EUPL-1.2
 * SPDX-FileCopyrightText: 2020-present Detlef Stern
 *-----------------------------------------------------------------------------
 */

*,*::before,*::after {
    box-sizing: border-box;
  }
  html {

    font-family: serif;
    scroll-behavior: smooth;
    height: 100%;
  }
  body {
    margin: 0;
    min-height: 100vh;
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
  .zs-dropdown:hover > .zs-dropdown-content { display: block }
  main { padding: 0 1rem }
  article > * + * { margin-top: .5rem }
  article header {
    padding: 0;
    margin: 0;
  }
  h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal }
  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;
    padding-left: 1rem;
    margin-left: 1rem;
    margin-right: 2rem;
    font-style: italic;
  }
  blockquote p { margin-bottom: .5rem }
  blockquote cite { font-style: normal }
  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }

  thead>tr>td { border-bottom: 2px solid hsl(0, 0%, 70%); font-weight: bold }
  tfoot>tr>td { border-top: 2px solid hsl(0, 0%, 70%); font-weight: bold }
  td {
    text-align: left;
    padding: .25rem .5rem;
    border-bottom: 1px solid hsl(0, 0%, 85%)
  }
  main form {
    padding: 0 .5em;
    margin: .5em 0 0 0;
  }
  main form:after {
    content: ".";
    display: block;







|
|
|
|
|
|
|
|
>

|

|


|
|
|
|
<

|
<





>
|
|
<
<
<
|
<







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
  .zs-dropdown:hover > .zs-dropdown-content { display: block }
  main { padding: 0 1rem }
  article > * + * { margin-top: .5rem }
  article header {
    padding: 0;
    margin: 0;
  }
  h1,h2,h3,h4,h5,h6 { font-family:sans-serif; font-weight:normal; margin:.4em 0 }
  h1 { font-size:1.5em }
  h2 { font-size:1.25em }
  h3 { font-size:1.15em }
  h4 { font-size:1.05em; font-weight: bold }
  h5 { font-size:1.05em }
  h6 { font-size:1.05em; font-weight: lighter }
  p { margin: .5em 0 0 0 }
  p.zs-meta-zettel { margin-top: .5em; margin-left: .5em }
  li,figure,figcaption,dl { margin: 0 }
  dt { margin: .5em 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5em 0 0 2em }
  dd > p:first-child { margin: 0 0 0 0 }
  blockquote {
    border-left: .5em solid lightgray;
    padding-left: 1em;
    margin-left: 1em;
    margin-right: 2em;

  }
  blockquote p { margin-bottom: .5em }

  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }
  td, th {text-align: left; padding: .25em .5em;}
  th { font-weight: bold }
  thead th { border-bottom: 2px solid hsl(0, 0%, 70%) }



  td { border-bottom: 1px solid hsl(0, 0%, 85%) }

  main form {
    padding: 0 .5em;
    margin: .5em 0 0 0;
  }
  main form:after {
    content: ".";
    display: block;
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
  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 }

  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5rem;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1rem .2rem;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25rem;
  }
  pre {
    padding: .5rem .7rem;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5rem;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5rem .7rem;
    max-width: 100%;
    border-radius: .5rem;
    border: 1px solid black;
  }
  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;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  td.left { text-align:left }
  td.center { text-align:center }
  td.right { text-align:right }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2rem }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1rem;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1rem }
  nav > details { margin-top:1rem }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2rem;
    background-color: #eee;
  }
  footer { padding: 0 1rem }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }







>



|








|


|


|



|









|

|





|
|




|



|






|
|
|






|



|


|
|







|


|








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
  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[rel~="external"]::after { content: "âžš"; display: inline-block }
  img { max-width: 100% }
  img.right { float: right }
  ol.zs-endnotes {
    padding-top: .5em;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1em .2em;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25em;
  }
  pre {
    padding: .5em .7em;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5em;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5em .7em;
    max-width: 100%;
    border-radius: .5em;
    border: 1px solid black;
  }
  div.zs-indication p:first-child { margin-top: 0 }
  span.zs-indication {
    border: 1px solid black;
    border-radius: .25em;
    padding: .1rem .2em;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5em 1em;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5em 1em;
  }
  .zs-error {
    background-color: lightpink;
    border-style: none !important;
    font-weight: bold;
  }
  td.left, th.left { text-align:left }
  td.center, th.center { text-align:center }
  td.right, th.right { text-align:right }
  .zs-font-size-0 { font-size:75% }
  .zs-font-size-1 { font-size:83% }
  .zs-font-size-2 { font-size:100% }
  .zs-font-size-3 { font-size:117% }
  .zs-font-size-4 { font-size:150% }
  .zs-font-size-5 { font-size:200% }
  .zs-deprecated { border-style: dashed; padding: .2em }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1em;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1em }
  nav > details { margin-top:1em }
  details > summary {
    width: 100%;
    background-color: #eee;
    font-family:sans-serif;
  }
  details > ul {
    margin-top:0;
    padding-left:2em;
    background-color: #eee;
  }
  footer { padding: 0 1em }
  @media (prefers-reduced-motion: reduce) {
    * {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

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>
{{#NewZettelLinks.Has}}
<div class="zs-dropdown">
<button>New</button>
<nav class="zs-dropdown-content">
{{#NewZettelLinks.Links}}
<a href="{{{URL}}}">{{Text}}</a>
{{/NewZettelLinks.Links}}
</nav>
</div>
{{/NewZettelLinks.Has}}
<form action="{{{SearchURL}}}">
<input type="text" placeholder="Search.." name="{{QueryKeyQuery}}">
</form>
</nav>
<main class="content">
{{{Content}}}
</main>
{{#FooterHTML}}<footer><hr>{{{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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

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

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

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

	"t73f.de/r/zsc/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(_ *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 (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
			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")
}

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,
		},
		domain.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,
		},
		domain.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,
		},
		domain.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:   "20221013105100",
		},
		domain.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			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:     meta.SyntaxMustache,
			api.KeyCreated:    "20210305133215",

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


































	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.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:     meta.SyntaxCSS,
			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:     meta.SyntaxNone,
			api.KeyCreated:    "20220321183214",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(nil)},
	id.EmojiZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Generic Emoji",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxGif,
			api.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:     meta.SyntaxZmk,
			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:     meta.SyntaxZmk,
			api.KeyCreated:    "20201028185209",


			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		domain.NewContent(nil)},






















	id.MustParse(api.ZidTemplateNewUser): {
		constHeader{
			api.KeyTitle:                       "New User",
			api.KeyRole:                        api.ValueRoleConfiguration,
			api.KeySyntax:                      meta.SyntaxNone,
			api.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:  meta.SyntaxZmk,
			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







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


|
<


>
>




















|











|










|







|

|

|




|
|
>


|




|

>


|




|
|
>


|




|

<
<
<
<
<
<
<
<
|


|




|

<
<
<
<
<
<
<
<
|


|




|

>


|




|
|
>


|




|

>


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






>


|








|
<
<
<
<
<
<
<
<
<









|







>


|


|
|
|
|
>
>
|

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











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








|











|
|

|
|

|
|

|
|

|
|

|
|

>
>
>
|
|

|
|

|
|

|
|









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



93
94
95
96
97
98
99





















100
101
102

103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198








199
200
201
202
203
204
205
206
207
208








209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295









296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
487
			cb.enricher.Enrich(ctx, m, cb.number)
			handle(m)
		}
	}
	return nil
}






















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.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20240418095500",
		},
		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:   "20240219145300",
			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:   "20240219145200",
			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:   "20241127170400",
			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:   "20241127170500",
			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:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentFormSxn)},
	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170530",
			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:   "20240219145200",
			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:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentErrorSxn)},
	id.StartSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Start Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230824160700",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnBase),
		},
		zettel.NewContent(contentStartCodeSxn)},
	id.BaseSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Base Code",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230619132800",
			api.KeyModified:   "20241118173500",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
			api.KeyPrecursor:  string(api.ZidSxnPrelude),
		},
		zettel.NewContent(contentBaseCodeSxn)},
	id.PreludeSxnZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Sxn Prelude",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20231006181700",
			api.KeyModified:   "20240222121200",
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		zettel.NewContent(contentPreludeSxn)},
	id.MustParse(api.ZidBaseCSS): {
		constHeader{
			api.KeyTitle:      "Zettelstore Base CSS",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxCSS,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240827143500",
			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.KeyModified:   "20231129111800",
			api.KeyVisibility: api.ValueVisibilityCreator,
		},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:                 "New Zettel",
			api.KeyRole:                  api.ValueRoleConfiguration,
			api.KeySyntax:                meta.SyntaxZmk,
			api.KeyCreated:               "20201028185209",
			api.KeyModified:              "20230929132900",
			meta.NewPrefix + api.KeyRole: api.ValueRoleZettel,
			api.KeyVisibility:            api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewRole): {
		constHeader{
			api.KeyTitle:                  "New Role",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20231129110800",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleRole,
			meta.NewPrefix + api.KeyTitle: "",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	id.MustParse(api.ZidTemplateNewTag): {
		constHeader{
			api.KeyTitle:                  "New Tag",
			api.KeyRole:                   api.ValueRoleConfiguration,
			api.KeySyntax:                 meta.SyntaxZmk,
			api.KeyCreated:                "20230929132400",
			meta.NewPrefix + api.KeyRole:  api.ValueRoleTag,
			meta.NewPrefix + api.KeyTitle: "#",
			api.KeyVisibility:             api.ValueVisibilityCreator,
		},
		zettel.NewContent(nil)},
	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.MustParse(api.ZidRoleZettelZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleZettel,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129161400",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleZettel)},
	id.MustParse(api.ZidRoleConfigurationZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleConfiguration,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162800",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleConfiguration)},
	id.MustParse(api.ZidRoleRoleZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleRole,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162900",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleRole)},
	id.MustParse(api.ZidRoleTagZettel): {
		constHeader{
			api.KeyTitle:      api.ValueRoleTag,
			api.KeyRole:       api.ValueRoleRole,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20231129162000",
			api.KeyLang:       api.ValueLangEN,
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		zettel.NewContent(contentRoleTag)},
	id.MustParse(api.ZidAppDirectory): {
		constHeader{
			api.KeyTitle:      "Zettelstore Application Directory",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxNone,
			api.KeyLang:       api.ValueLangEN,
			api.KeyCreated:    "20240703235900",
			api.KeyVisibility: api.ValueVisibilityLogin,
		},
		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 delete.sxn
var contentDeleteSxn []byte

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

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

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

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

//go:embed prelude.sxn
var contentPreludeSxn []byte

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

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

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

//go:embed rolezettel.zettel
var contentRoleZettel []byte

//go:embed roleconfiguration.zettel
var contentRoleConfiguration []byte

//go:embed rolerole.zettel
var contentRoleRole []byte

//go:embed roletag.zettel
var contentRoleTag []byte

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

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; Cost:{{#Costs}}&#x2000;<a href="{{{URL}}}">{{{Text}}}</a>{{/Costs}}
</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}}
{{#Incoming.Has}}
<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.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming.Links}}
</ul>
</div>
{{/Incoming.Has}}
{{#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
27
28
29
30
31
32
33
34
35
36
37
38
39
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Delete Zettel " ,zid))
  (p "Do you really want to delete this zettel?")
  ,@(if shadowed-box
    `((div (@ (class "zs-info"))
      (h2 "Information")
      (p "If you delete this zettel, the previously shadowed zettel from overlayed box " ,shadowed-box " becomes available.")
    ))
  )
  ,@(if incoming
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "If you delete this zettel, incoming references from the following zettel will become invalid.")
      (ul ,@(map wui-item-link incoming))
    ))
  )
  ,@(if (and (bound? 'useless) useless)
    `((div (@ (class "zs-warning"))
      (h2 "Warning!")
      (p "Deleting this zettel will also delete the following files, so that they will not be interpreted as content for this zettel.")
      (ul ,@(map wui-item useless))
    ))
  )
  ,(wui-meta-desc metapairs)
  (form (@ (method "POST")) (input (@ (class "zs-primary") (type "submit") (value "Delete"))))
)

Changes to box/constbox/dependencies.zettel.

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
(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.
```

=== 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
```
MIT License







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







97
98
99
100
101
102
103













































104
105
106
107
108
109
110
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```














































=== yuin/goldmark
; URL & Source
: [[https://github.com/yuin/goldmark]]
; License
: MIT License
```
MIT License
170
171
172
173
174
175
176















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
143
144
145
146
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, SxWebs, Webs, Zettelstore-Client
These are companion projects, written by the main developer of Zettelstore.
They are published under the same license, [[EUPL v1.2, or later|00000000000004]].

; URL & Source Sx
: [[https://t73f.de/r/sx]]
; URL & Source SxWebs
: [[https://t73f.de/r/sxwebs]]
; URL & Source Webs
: [[https://t73f.de/r/webs]]
; URL & Source Zettelstore-Client
: [[https://t73f.de/r/zsc]]
; 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
5
6
7
8
9
10
11
12
13
14
15
16
17
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 ,heading))
  ,message
)

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
55
<article>
<header>
<h1>{{Heading}}</h1>
</header>
<form action="{{FormActionURL}}" method="POST" enctype="multipart/form-data">
<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">
<input class="zs-upload" type="file" id="zs-file" name="file">
</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
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) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 ,heading))
  (form (@ (action ,form-action-url) (method "POST") (enctype "multipart/form-data"))
  (div
    (label (@ (for "zs-title")) "Title " (a (@ (title "Main heading of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-title") (name "title")
              (title "Title of this zettel")
              (placeholder "Title..") (value ,meta-title) (dir "auto") (autofocus))))
  (div
    (label (@ (for "zs-role")) "Role " (a (@ (title "One word, without spaces, to set the main role of this zettel.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-role") (name "role")
              (title "One word, letters and digits, but no spaces, to set the main role of the zettel.")
              (placeholder "role..") (value ,meta-role) (dir "auto")
      ,@(if role-data '((list "zs-role-data")))
    ))
    ,@(wui-datalist "zs-role-data" role-data)
  )
  (div
    (label (@ (for "zs-tags")) "Tags " (a (@ (title "Tags must begin with an '#' sign. They are separated by spaces.")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (id "zs-tags") (name "tags")
              (title "Tags/keywords to categorize the zettel. Each tags is a word that begins with a '#' character; they are separated by spaces")
              (placeholder "#tag") (value ,meta-tags) (dir "auto"))))
  (div
    (label (@ (for "zs-meta")) "Metadata " (a (@ (title "Other metadata for this zettel. Each line contains a key/value pair, separated by a colon ':'.")) (@H "&#9432;")))
    (textarea (@ (class "zs-input") (id "zs-meta") (name "meta") (rows "4")
                 (title "Additional metadata about the zettel")
                 (placeholder "metakey: metavalue") (dir "auto")) ,meta))
  (div
    (label (@ (for "zs-syntax")) "Syntax " (a (@ (title "Syntax of zettel content below, one word. Typically 'zmk' (for zettelmarkup).")) (@H "&#9432;")))
    (input (@ (class "zs-input") (type "text") (pattern "\\w*") (id "zs-syntax") (name "syntax")
              (title "Syntax/format of zettel content below, one word, letters and digits, no spaces.")
              (placeholder "syntax..") (value ,meta-syntax) (dir "auto")
      ,@(if syntax-data '((list "zs-syntax-data")))
    ))
    ,@(wui-datalist "zs-syntax-data" syntax-data)
  )
  ,@(if (bound? 'content)
    `((div
      (label (@ (for "zs-content")) "Content " (a (@ (title "Content for this zettel, according to above syntax.")) (@H "&#9432;")))
      (textarea (@ (class "zs-input zs-content") (id "zs-content") (name "content") (rows "20")
                   (title "Zettel content, according to the given syntax")
                   (placeholder "Zettel content..") (dir "auto")) ,content)
    ))
  )
  (div
    (input (@ (class "zs-primary") (type "submit") (value "Submit")))
    (input (@ (class "zs-secondary") (type "submit") (value "Save") (formaction "?save")))
    (input (@ (class "zs-upload") (type "file") (id "zs-file") (name "file")))
  ))
)

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
72
73
74
<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}}
{{#CanCopy}} &#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanVersion}} &#183; <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}}
{{#CanFolge}} &#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#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}}
{{#QueryLinks.Has}}
<h3>Queries</h3>
<ul>
{{#QueryLinks.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/QueryLinks.Links}}
</ul>
{{/QueryLinks.Has}}
{{#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}}</a></td>
{{/Elements}}
</tr>
{{/EvalMatrix}}
</table>
<h3>Parsed (not evaluated)</h3>
<table>
{{#ParseMatrix}}
<tr>
<th>{{Header}}</th>
{{#Elements}}<td><a href="{{{URL}}}">{{Text}}</a></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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Information for Zettel " ,zid)
    (p
      (a (@ (href ,web-url)) "Web")
      (@H " &#183; ") (a (@ (href ,context-url)) "Context")
      (@H " / ") (a (@ (href ,context-full-url)) "Full")
      ,@(if (bound? 'edit-url) `((@H " &#183; ") (a (@ (href ,edit-url)) "Edit")))
      ,@(ROLE-DEFAULT-actions (current-binding))
      ,@(if (bound? 'reindex-url) `((@H " &#183; ") (a (@ (href ,reindex-url)) "Reindex")))
      ,@(if (bound? 'delete-url) `((@H " &#183; ") (a (@ (href ,delete-url)) "Delete")))
    )
  )
  (h2 "Interpreted Metadata")
  (table ,@(map wui-info-meta-table-row metadata))
  (h2 "References")
  ,@(if local-links `((h3 "Local")    (ul ,@(map wui-local-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-2023 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
8
9
10
11
12
<header>
<h1>{{Title}}</h1>
</header>
<form action="{{{SearchURL}}}">
<input class="zs-input" type="text" placeholder="Search.." name="{{QueryKeyQuery}}" value="{{QueryValue}}">
</form>
{{{Content}}}
{{#CanCreate}}<form action="{{{CreateURL}}}">
<input type="hidden" name="{{QueryKeyQuery}}" value="{{QueryValue}}">
<input type="hidden" name="{{QueryKeySeed}}" value="{{Seed}}">
<input class="zs-primary" type="submit" value="Save As Zettel">
</form>{{/CanCreate}}
<
<
<
<
<
<
<
<
<
<
<
<
























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
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) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 ,heading))
  (search (form (@ (action ,search-url))
    (input (@ (class "zs-input") (type "search") (inputmode "search") (name ,query-key-query)
              (title "Contains the search that leads to the list below. You're allowed to modify it")
              (placeholder "Search..") (value ,query-value) (dir "auto")))))
  ,@(if (bound? 'tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Tag zettel: " ,@tag-zettel))
    )
  ,@(if (bound? 'create-tag-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create tag zettel: " ,@create-tag-zettel))
    )
  ,@(if (bound? 'role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Role zettel: " ,@role-zettel))
    )
  ,@(if (bound? 'create-role-zettel)
     `((p (@ (class "zs-meta-zettel")) "Create role zettel: " ,@create-role-zettel))
    )
  ,@content
  ,@endnotes
  (form (@ (action ,(if (bound? 'create-url) create-url)))
      ,(if (bound? 'data-url)
          `(@L "Other encodings"
               ,(if (> num-entries 3) `(@L " of these " ,num-entries " entries: ") ": ")
               (a (@ (href ,data-url)) "data")
               ", "
               (a (@ (href ,plain-url)) "plain")
           )
      )
      ,@(if (bound? 'create-url)
        `((input (@ (type "hidden") (name ,query-key-query) (value ,query-value)))
          (input (@ (type "hidden") (name ,query-key-seed) (value ,seed)))
          (input (@ (class "zs-primary") (type "submit") (value "Save As Zettel")))
        )
      )
  )
)

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
15
16
17
18
19
20
21
22
23
24
25
26
27
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header (h1 "Login"))
  ,@(if retry '((div (@ (class "zs-indication zs-error")) "Wrong user name / password. Try again.")))
  (form (@ (method "POST") (action ""))
    (div
      (label (@ (for "username")) "User name:")
      (input (@ (class "zs-input") (type "text") (id "username") (name "username") (placeholder "Your user name..") (autofocus))))
    (div
      (label (@ (for "password")) "Password:")
      (input (@ (class "zs-input") (type "password") (id "password") (name "password") (placeholder "Your password.."))))
    (div
      (input (@ (class "zs-primary") (type "submit") (value "Login"))))
  )
)

Changes to box/constbox/newtoc.zettel.

1
2
3


4
This zettel lists all zettel that should act as a template for new zettel.
These zettel will be included in the ""New"" menu of the WebUI.
* [[New Zettel|00000000090001]]


* [[New User|00000000090002]]



>
>

1
2
3
4
5
6
This zettel lists all zettel that should act as a template for new zettel.
These zettel will be included in the ""New"" menu of the WebUI.
* [[New Zettel|00000000090001]]
* [[New Role|00000000090004]]
* [[New Tag|00000000090003]]
* [[New User|00000000090002]]

Added box/constbox/prelude.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
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

;;; This zettel contains sxn definitions that are independent of specific
;;; subsystems, such as WebUI, API, or other. It just contains generic code to
;;; be used in all places. It asumes that the symbols NIL and T are defined.

;; not macro
(defmacro not (x) `(if ,x NIL T))

;; not= macro, to negate an equivalence
(defmacro not= args `(not (= ,@args)))

;; let* macro
;;
;; (let* (BINDING ...) EXPR ...), where SYMBOL may occur in later bindings.
(defmacro let* (bindings . body)
    (if (null? bindings)
        `(begin ,@body)
        `(let ((,(caar bindings) ,(cadar bindings)))
               (let* ,(cdr bindings) ,@body))))

;; cond macro
;;
;; (cond ((COND EXPR) ...))
(defmacro cond clauses
    (if (null? clauses)
        ()
        (let* ((clause (car clauses))
               (the-cond (car clause)))
              (if (= the-cond T)
                  `(begin ,@(cdr clause))
                  `(if ,the-cond
                       (begin ,@(cdr clause))
                       (cond ,@(cdr clauses)))))))

;; and macro
;;
;; (and EXPR ...)
(defmacro and args
    (cond ((null? args)       T)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) (and ,@(cdr args))))))


;; or macro
;;
;; (or EXPR ...)
(defmacro or args
    (cond ((null? args)       NIL)
          ((null? (cdr args)) (car args))
          (T                  `(if ,(car args) T (or ,@(cdr args))))))

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>
{{#Incoming.Has}}
<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.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/Incoming.Links}}
</ul>
</div>
{{/Incoming.Has}}
{{#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/roleconfiguration.zettel.









































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Zettel with role ""configuration"" are used within Zettelstore to manage and to show the current configuration of the software.

Typically, there are some public zettel that show the license of this software, its dependencies, some CSS code to make the default web user interface a litte bit nicer, and the defult image to singal a broken image.

Other zettel are only visible if an user has authenticated itself, or if there is no authentication enabled.
In this case, one additional configuration zettel is the zettel containing the version number of this software.
Other zettel are showing the supported metadata keys and supported syntax values.
Zettel that allow to configure the menu of template to create new zettel are also using the role ""configuration"".

Most important is the zettel that contains the runtime configuration.
You may change its metadata value to change the behaviour of the software.

One configuration is the ""expert mode"".
If enabled, and if you are authorized so see them, you will discover some more zettel.
For example, HTML templates to customize the default web user interface, to show the application log, to see statistics about zettel boxes, to show the host name and it operating system, and many more.

You are allowed to add your own configuration zettel, for example if you want to customize the look and feel of zettel by placing relevant data into your own zettel.

By default, user zettel (for authentification) use also the role ""configuration"".
However, you are allowed to change this.

Added box/constbox/rolerole.zettel.





















>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
A zettel with the role ""role"" describes a specific role.
The described role must be the title of such a zettel.

This zettel is such a zettel, as it describes the meaning of the role ""role"".
Therefore it has the title ""role"" too.
If you like, this zettel is a meta-role.

You are free to create your own role-describing zettel.
For example, you want to document the intended meaning of the role.
You might also be interested to describe needed metadata so that some software is enabled to analyse or to process your zettel.

Added box/constbox/roletag.zettel.













>
>
>
>
>
>
1
2
3
4
5
6
A zettel with role ""tag"" is a zettel that describes specific tag.
The tag name must be the title of such a zettel.

Such zettel are similar to this specific zettel: this zettel describes zettel with a role ""tag"".
These zettel with the role ""tag"" describe specific tags.
These might form a hierarchy of meta-tags (and meta-roles).

Added box/constbox/rolezettel.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
A zettel with the role ""zettel"" is typically used to document your own thoughts.
Such zettel are the main reason to use the software Zettelstore.

The only predefined zettel with the role ""zettel"" is the [[default home zettel|00010000000000]], which contains some welcome information.

You are free to change this.
In this case you should modify this zettel too, so that it reflects your own use of zettel with the role ""zettel"".

Added box/constbox/start.sxn.



































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

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

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

;; Contains WebUI specific code, but not related to a specific template.

;; wui-list-item returns the argument as a HTML list item.
(defun wui-item (s) `(li ,s))

;; wui-info-meta-table-row takes a pair and translates it into a HTML table row
;; with two columns.
(defun wui-info-meta-table-row (p)
    `(tr (td (@ (class zs-info-meta-key)) ,(car p)) (td (@ (class zs-info-meta-value)) ,(cdr p))))

;; wui-local-link translates a local link into HTML.
(defun wui-local-link (l) `(li (a (@ (href ,l )) ,l)))

;; wui-link takes a link (title . url) and returns a HTML reference.
(defun wui-link (q) `(a (@ (href ,(cdr q))) ,(car q)))

;; wui-item-link taks a pair (text . url) and returns a HTML link inside
;; a list item.
(defun wui-item-link (q) `(li ,(wui-link q)))

;; wui-tdata-link taks a pair (text . url) and returns a HTML link inside
;; a table data item.
(defun wui-tdata-link (q) `(td ,(wui-link q)))

;; wui-item-popup-link is like 'wui-item-link, but the HTML link will open
;; a new tab / window.
(defun wui-item-popup-link (e)
    `(li (a (@ (href ,e) (target "_blank") (rel "external noreferrer")) ,e)))

;; wui-option-value returns a value for an HTML option element.
(defun wui-option-value (v) `(option (@ (value ,v))))

;; wui-datalist returns a HTML datalist with the given HTML identifier and a
;; list of values.
(defun wui-datalist (id lst)
    (if lst
        `((datalist (@ (id ,id)) ,@(map wui-option-value lst)))))

;; wui-pair-desc-item takes a pair '(term . text) and returns a list with
;; a HTML description term and a HTML description data. 
(defun wui-pair-desc-item (p) `((dt ,(car p)) (dd ,(cdr p))))

;; wui-meta-desc returns a HTML description list made from the list of pairs
;; given.
(defun wui-meta-desc (l)
    `(dl ,@(apply append (map wui-pair-desc-item l))))

;; wui-enc-matrix returns the HTML table of all encodings and parts.
(defun wui-enc-matrix (matrix)
    `(table
      ,@(map
         (lambda (row) `(tr (th ,(car row)) ,@(map wui-tdata-link (cdr row))))
         matrix)))

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

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

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

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

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

;; ROLE-role-actions returns an additional action "Zettel" for zettel with role "role".
(defun ROLE-role-actions (binding)
    `(,@(ROLE-DEFAULT-actions binding)
      ,@(let ((title (binding-lookup 'title binding)))
             (if (and (defined? title) title)
                 `(,ACTION-SEPARATOR (a (@ (href ,(query->url (concat "role:" title)))) "Zettel"))
             )
        )
    )
)

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

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
43
44
45
46
47
48
49
50
51
52
<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>)
{{#Tags.Has}}&#183; {{#Tags.Links}} <a href="{{{URL}}}">{{Text}}</a>{{/Tags.Links}}{{/Tags.Has}}
{{#CanCopy}}&#183; <a href="{{{CopyURL}}}">Copy</a>{{/CanCopy}}
{{#CanVersion}}&#183; <a href="{{{VersionURL}}}">Version</a>{{/CanVersion}}
{{#CanFolge}}&#183; <a href="{{{FolgeURL}}}">Folge</a>{{/CanFolge}}
{{#PredecessorRefs}}<br>Predecessor: {{{PredecessorRefs}}}{{/PredecessorRefs}}
{{#PrecursorRefs}}<br>Precursor: {{{PrecursorRefs}}}{{/PrecursorRefs}}
{{#HasExtURL}}<br>URL: <a href="{{{ExtURL}}}"{{{ExtNewWindow}}}>{{ExtURL}}</a>{{/HasExtURL}}
{{#Author}}<br>By {{Author}}{{/Author}}
</div>
</header>
{{{Content}}}
</article>
{{#NeedBottomNav}}<nav>{{/NeedBottomNav}}
{{#FolgeLinks.Has}}
<details open>
<summary>Folgezettel</summary>
<ul>
{{#FolgeLinks.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/FolgeLinks.Links}}
</ul>
</details>
{{/FolgeLinks.Has}}
{{#BackLinks.Has}}
<details open>
<summary>Incoming</summary>
<ul>
{{#BackLinks.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/BackLinks.Links}}
</ul>
</details>
{{/BackLinks.Has}}
{{#SuccessorLinks.Has}}
<details open>
<summary>Successors</summary>
<ul>
{{#SuccessorLinks.Links}}
<li><a href="{{{URL}}}">{{Text}}</a></li>
{{/SuccessorLinks.Links}}
</ul>
</details>
{{/SuccessorLinks.Has}}
{{#NeedBottomNav}}</nav>{{/NeedBottomNav}}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































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
31
32
33
34
35
36
37
38
39
40
41
42
43
;;;----------------------------------------------------------------------------
;;; Copyright (c) 2023-present Detlef Stern
;;;
;;; This file is part of Zettelstore.
;;;
;;; Zettelstore is licensed under the latest version of the EUPL (European
;;; Union Public License). Please see file LICENSE.txt for your rights and
;;; obligations under this license.
;;;
;;; SPDX-License-Identifier: EUPL-1.2
;;; SPDX-FileCopyrightText: 2023-present Detlef Stern
;;;----------------------------------------------------------------------------

`(article
  (header
    (h1 ,heading)
    (div (@ (class "zs-meta"))
      ,@(if (bound? 'edit-url) `((a (@ (href ,edit-url)) "Edit") (@H " &#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-binding))
      ,@(if predecessor-refs `((br) "Predecessor: " ,predecessor-refs))
      ,@(if precursor-refs `((br) "Precursor: " ,precursor-refs))
      ,@(if prequel-refs `((br) "Prequel: " ,prequel-refs))
      ,@(ROLE-DEFAULT-heading (current-binding))
    )
  )
  ,@content
  ,endnotes
  ,@(if (or folge-links sequel-links back-links successor-links)
    `((nav
      ,@(if folge-links `((details (@ (,folge-open)) (summary "Folgezettel") (ul ,@(map wui-item-link folge-links)))))
      ,@(if sequel-links `((details (@ (,sequel-open)) (summary "Sequel") (ul ,@(map wui-item-link sequel-links)))))
      ,@(if back-links `((details (@ (,back-open)) (summary "Incoming") (ul ,@(map wui-item-link back-links)))))
      ,@(if successor-links `((details (@ (,successor-open)) (summary "Successors") (ul ,@(map wui-item-link successor-links)))))
     ))
  )
)

Changes to box/dirbox/dirbox.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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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 provides a directory-based zettel box.
package dirbox

import (
	"context"
	"errors"
	"net/url"
	"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()

|






>
>
>
















<
<
<



>
>
>







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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package dirbox provides a directory-based zettel box.
package dirbox

import (
	"context"
	"errors"
	"net/url"
	"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()
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
	_ notifyTypeSpec = iota
	dirNotifyAny
	dirNotifySimple
	dirNotifyFS
)

func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec {
	for count := 0; count < 2; count++ {
		switch notifyType {
		case kernel.BoxDirTypeNotify:
			return dirNotifyFS
		case kernel.BoxDirTypeSimple:
			return dirNotifySimple
		default:
			notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)







|







88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
	_ notifyTypeSpec = iota
	dirNotifyAny
	dirNotifySimple
	dirNotifyFS
)

func getDirSrvInfo(log *logger.Logger, notifyType string) notifyTypeSpec {
	for range 2 {
		switch notifyType {
		case kernel.BoxDirTypeNotify:
			return dirNotifyFS
		case kernel.BoxDirTypeSimple:
			return dirNotifySimple
		default:
			notifyType = kernel.Main.GetConfig(kernel.BoxService, kernel.BoxDefaultDirType).(string)
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
	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)
		go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}

	var notifier notify.Notifier
	var err error
	switch dp.notifySpec {
	case dirNotifySimple:
		notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir)
	default:
		notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir)
	}
	if err != nil {
		dp.log.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
}







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





|














|




>







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
	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 := range dp.fSrvs {
		cc := make(chan fileCmd)
		go fileService(i, dp.log.Clone().Str("sub", "file").Uint("fn", uint64(i)).Child(), dp.dir, cc)
		dp.fCmds = append(dp.fCmds, cc)
	}

	var notifier notify.Notifier
	var err error
	switch dp.notifySpec {
	case dirNotifySimple:
		notifier, err = notify.NewSimpleDirNotifier(dp.log.Clone().Str("notify", "simple").Child(), dp.dir)
	default:
		notifier, err = notify.NewFSDirNotifier(dp.log.Clone().Str("notify", "fs").Child(), dp.dir)
	}
	if err != nil {
		dp.log.Error().Err(err).Msg("Unable to create directory supervisor")
		dp.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(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 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(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)







|
|
|
|



















|

















|




|


|



|

|




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







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

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

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

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, box.OnZettel)
	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(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(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.ErrNotFound
	}
	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")
}







|



|







|










|





|



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















|







|










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
		}
		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, box.OnZettel)
	}
	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) 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, box.OnDelete)
	}
	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
12
13
14
15
//-----------------------------------------------------------------------------
// 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 dirbox

import "testing"

func TestIsPrime(t *testing.T) {

|






>
>
>







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

package dirbox

import "testing"

func TestIsPrime(t *testing.T) {
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
		if got != tc.exp {
			t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got)
		}
	}
}

func TestMakePrime(t *testing.T) {
	for i := uint32(0); i < 1500; i++ {
		np := makePrime(i)
		if np < i {
			t.Errorf("makePrime(%d) < %d", i, np)
			continue
		}
		if !isPrime(np) {
			t.Errorf("makePrime(%d) == %d is not prime", i, np)







|







31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
		if got != tc.exp {
			t.Errorf("isPrime(%d)=%v, but got %v", tc.n, tc.exp, got)
		}
	}
}

func TestMakePrime(t *testing.T) {
	for i := range uint32(1500) {
		np := makePrime(i)
		if np < i {
			t.Errorf("makePrime(%d) < %d", i, np)
			continue
		}
		if !isPrime(np) {
			t.Errorf("makePrime(%d) == %d is not prime", i, np)

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
42
43
44
45
46
47
48
49
50
51
52
53
54
//-----------------------------------------------------------------------------
// 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)
	}
	log.Trace().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped")
}

type fileCmd interface {
	run(*logger.Logger, string)
}

const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing.

// COMMAND: srvGetMeta ----------------------------------------
//
// Retrieves the meta data from a 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
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package dirbox

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

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/box/filebox"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/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.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service started")
	for cmd := range cmds {
		cmd.run(dirPath)
	}
	log.Debug().Uint("i", uint64(i)).Str("dirpath", dirPath).Msg("File service stopped")
}

type fileCmd interface {
	run(string)
}

const serviceTimeout = 5 * time.Second // must be shorter than the web servers timeout values for reading+writing.

// COMMAND: srvGetMeta ----------------------------------------
//
// Retrieves the meta data from a zettel.
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
	rc    chan<- resGetMeta
}
type resGetMeta struct {
	meta *meta.Meta
	err  error
}

func (cmd *fileGetMeta) run(log *logger.Logger, dirPath string) {
	var m *meta.Meta
	var err error

	entry := cmd.entry
	zid := entry.Zid
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
		if contentName == "" || contentExt == "" {
			log.Panic().Zid(zid).Msg("No meta, no content in getMeta")
		}
		if entry.HasMetaInContent() {
			m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName))
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))
	}







|









|
<
|







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
	rc    chan<- resGetMeta
}
type resGetMeta struct {
	meta *meta.Meta
	err  error
}

func (cmd *fileGetMeta) run(dirPath string) {
	var m *meta.Meta
	var err error

	entry := cmd.entry
	zid := entry.Zid
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
		if contentName == "" || contentExt == "" {
			err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid)

		} else if entry.HasMetaInContent() {
			m, _, err = parseMetaContentFile(zid, filepath.Join(dirPath, contentName))
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))
	}
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
}
type resGetMetaContent struct {
	meta    *meta.Meta
	content []byte
	err     error
}

func (cmd *fileGetMetaContent) run(log *logger.Logger, dirPath string) {
	var m *meta.Meta
	var content []byte
	var err error

	entry := cmd.entry
	zid := entry.Zid
	contentName := entry.ContentName
	contentExt := entry.ContentExt
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" || contentExt == "" {
			log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent")
		}
		if entry.HasMetaInContent() {
			m, content, err = parseMetaContentFile(zid, contentPath)
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
			content, err = os.ReadFile(contentPath)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))







|











|
<
|







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
}
type resGetMetaContent struct {
	meta    *meta.Meta
	content []byte
	err     error
}

func (cmd *fileGetMetaContent) run(dirPath string) {
	var m *meta.Meta
	var content []byte
	var err error

	entry := cmd.entry
	zid := entry.Zid
	contentName := entry.ContentName
	contentExt := entry.ContentExt
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" || contentExt == "" {
			err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid)

		} else if entry.HasMetaInContent() {
			m, content, err = parseMetaContentFile(zid, contentPath)
		} else {
			m = filebox.CalcDefaultMeta(zid, contentExt)
			content, err = os.ReadFile(contentPath)
		}
	} else {
		m, err = parseMetaFile(zid, filepath.Join(dirPath, metaName))
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
	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
	contentName := entry.ContentName
	m := cmd.zettel.Meta
	content := cmd.zettel.Content.AsBytes()
	metaName := entry.MetaName
	if metaName == "" {
		if contentName == "" {
			log.Panic().Zid(zid).Msg("No meta, no content in setZettel")
		}
		contentPath := filepath.Join(dirPath, contentName)
		if entry.HasMetaInContent() {
			err := writeZettelFile(contentPath, m, content)
			cmd.rc <- err
			return
		}
		err := writeFileContent(contentPath, content)

		cmd.rc <- err
		return
	}

	err := writeMetaFile(filepath.Join(dirPath, metaName), m)
	if err == nil && contentName != "" {
		err = writeFileContent(filepath.Join(dirPath, contentName), content)
	}
	cmd.rc <- err
}

func writeMetaFile(metaPath string, m *meta.Meta) error {







|














|




|
>








|
|
|
|
|
|
|
|
|
>




|







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
	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(dirPath string) {
	var err error
	entry := cmd.entry
	zid := entry.Zid
	contentName := entry.ContentName
	m := cmd.zettel.Meta
	content := cmd.zettel.Content.AsBytes()
	metaName := entry.MetaName
	if metaName == "" {
		if contentName == "" {
			err = fmt.Errorf("no meta, no content in setZettel, zid=%v", zid)
		} else {
			contentPath := filepath.Join(dirPath, contentName)
			if entry.HasMetaInContent() {
				err = writeZettelFile(contentPath, m, content)
				cmd.rc <- err
				return
			}
			err = writeFileContent(contentPath, content)
		}
		cmd.rc <- err
		return
	}

	err = writeMetaFile(filepath.Join(dirPath, metaName), m)
	if err == nil && contentName != "" {
		err = writeFileContent(filepath.Join(dirPath, contentName), content)
	}
	cmd.rc <- err
}

func writeMetaFile(metaPath string, m *meta.Meta) error {
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
}

func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error {
	zettelFile, err := openFileWrite(contentPath)
	if err != nil {
		return err
	}
	if err == nil {
		err = writeMetaHeader(zettelFile, m)
	}
	if err == nil {
		_, err = zettelFile.Write(content)
	}
	if err1 := zettelFile.Close(); err == nil {
		err = err1
	}
	return err







<
|
<







237
238
239
240
241
242
243

244

245
246
247
248
249
250
251
}

func writeZettelFile(contentPath string, m *meta.Meta, content []byte) error {
	zettelFile, err := openFileWrite(contentPath)
	if err != nil {
		return err
	}

	err = writeMetaHeader(zettelFile, m)

	if err == nil {
		_, err = zettelFile.Write(content)
	}
	if err1 := zettelFile.Close(); err == nil {
		err = err1
	}
	return err
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

type fileDeleteZettel struct {
	entry *notify.DirEntry
	rc    chan<- resDeleteZettel
}
type resDeleteZettel = error

func (cmd *fileDeleteZettel) run(log *logger.Logger, dirPath string) {
	var err error

	entry := cmd.entry
	contentName := entry.ContentName
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent")
		}
		err = os.Remove(contentPath)

	} else {
		if contentName != "" {
			err = os.Remove(contentPath)
		}
		err1 := os.Remove(filepath.Join(dirPath, metaName))
		if err == nil {
			err = err1







|







|
|
|
>







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

type fileDeleteZettel struct {
	entry *notify.DirEntry
	rc    chan<- resDeleteZettel
}
type resDeleteZettel = error

func (cmd *fileDeleteZettel) run(dirPath string) {
	var err error

	entry := cmd.entry
	contentName := entry.ContentName
	contentPath := filepath.Join(dirPath, contentName)
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			err = fmt.Errorf("no meta, no content in deleteZettel, zid=%v", entry.Zid)
		} else {
			err = os.Remove(contentPath)
		}
	} else {
		if contentName != "" {
			err = os.Remove(contentPath)
		}
		err1 := os.Remove(filepath.Join(dirPath, metaName))
		if err == nil {
			err = err1
355
356
357
358
359
360
361
362






363
364
365
366
367
368
369
370
371
		m,
		entry.Zid,
		entry.ContentExt,
		entry.MetaName != "",
		entry.UselessFiles,
	)
}







func openFileWrite(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
}

func writeFileZid(w io.Writer, zid id.Zid) error {
	_, err := io.WriteString(w, "id: ")
	if err == nil {
		_, err = w.Write(zid.Bytes())
		if err == nil {








>
>
>
>
>
>

|







358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
		m,
		entry.Zid,
		entry.ContentExt,
		entry.MetaName != "",
		entry.UselessFiles,
	)
}

// fileMode to create a new file: user, group, and all are allowed to read and write.
//
// If you want to forbid others or the group to read or to write, you must set
// umask(1) accordingly.
const fileMode os.FileMode = 0666 //

func openFileWrite(path string) (*os.File, error) {
	return os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)
}

func writeFileZid(w io.Writer, zid id.Zid) error {
	_, err := io.WriteString(w, "id: ")
	if err == nil {
		_, err = w.Write(zid.Bytes())
		if err == nil {

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

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

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

	"t73f.de/r/zsc/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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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 := notify.NewSimpleZipNotifier(zb.log, zb.name)
	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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package filebox

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

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/notify"
	"zettelstore.de/z/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   box.UpdateNotifier
	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 (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 == "" {
			err = fmt.Errorf("no meta, no content in getZettel, zid=%v", zid)
			return zettel.Zettel{}, err
		}
		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)
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
		}
		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
	st.Zettel = zb.dirSrv.NumDirEntries()
	zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) {
	var inMeta bool
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
		if contentName == "" || contentExt == "" {
			zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta")
		}
		if entry.HasMetaInContent() {
			m, err = readZipMetaFile(reader, zid, contentName)
		} else {
			m = CalcDefaultMeta(zid, contentExt)
		}
	} else {
		m, err = readZipMetaFile(reader, zid, metaName)
	}







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






|

















|
<
|







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



























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
	st.Zettel = zb.dirSrv.NumDirEntries()
	zb.log.Trace().Int("zettel", int64(st.Zettel)).Msg("ReadStats")
}

func (zb *zipBox) readZipMeta(reader *zip.ReadCloser, zid id.Zid, entry *notify.DirEntry) (m *meta.Meta, err error) {
	var inMeta bool
	if metaName := entry.MetaName; metaName == "" {
		contentName := entry.ContentName
		contentExt := entry.ContentExt
		if contentName == "" || contentExt == "" {
			err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid)

		} else if entry.HasMetaInContent() {
			m, err = readZipMetaFile(reader, zid, contentName)
		} else {
			m = CalcDefaultMeta(zid, contentExt)
		}
	} else {
		m, err = readZipMetaFile(reader, zid, metaName)
	}

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

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

package 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 range 90 { // 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, minVal, defVal, maxVal int) int {
	sVal := u.Query().Get(key)
	if sVal == "" {
		return defVal
	}
	iVal, err := strconv.Atoi(sVal)
	if err != nil {
		return defVal
	}
	if iVal < minVal {
		return minVal
	}
	if iVal > maxVal {
		return maxVal
	}
	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
//-----------------------------------------------------------------------------
// 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 id.Set
	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)
		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.Zid(zid)
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid)
	ar.last.next = room
	ar.last = room
}

func (ar *anterooms) 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 *anterooms) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = ar.makeAnteroom(id.Invalid)
	ar.last = ar.first
}

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

	if ns := len(newZids); ns > 0 {
		ar.nextNum++
		ar.first = &anteroom{num: ar.nextNum, next: ar.first, waiting: newZids, 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 *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
	}
	roomNo := ar.first.num
	if ar.first.waiting == nil {
		ar.removeFirst()
		return arReload, id.Invalid, roomNo
	}
	for zid := range ar.first.waiting {
		delete(ar.first.waiting, zid)
		if len(ar.first.waiting) == 0 {
			ar.removeFirst()
		}
		return arZettel, zid, roomNo
	}
	ar.removeFirst()

	return arNothing, id.Invalid, 0
}

func (ar *anterooms) removeFirst() {
	ar.first = ar.first.next
	if ar.first == nil {
		ar.last = 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
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
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"sync"

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

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

type anteroom struct {

	next    *anteroom
	waiting *id.Set
	curLoad int
	reload  bool
}

type anteroomQueue struct {
	mx      sync.Mutex

	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 room.waiting.Contains(zid) {
			// 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 {

	if zid == id.Invalid {




		panic(zid)
	}
	waiting := id.NewSetCap(max(ar.maxLoad, 100), zid)
	return &anteroom{next: nil, waiting: waiting, curLoad: 1, reload: false}
}

func (ar *anteroomQueue) Reset() {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	ar.first = &anteroom{next: nil, waiting: nil, curLoad: 0, reload: true}
	ar.last = ar.first
}

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

	if !allZids.IsEmpty() {

		ar.first = &anteroom{next: ar.first, waiting: allZids, curLoad: allZids.Length(), reload: true}
		if ar.first.next == nil {
			ar.last = ar.first
		}


	} else {
		ar.first = nil
		ar.last = nil

	}
}

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, bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	first := ar.first


	if first != nil {
		if first.waiting == nil && first.reload {
			ar.removeFirst()
			return arReload, id.Invalid, false
		}
		if zid, found := first.waiting.Pop(); found {
			if first.waiting.IsEmpty() {

				ar.removeFirst()
			}
			return arZettel, zid, first.reload
		}
		ar.removeFirst()
	}
	return arNothing, id.Invalid, false
}

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

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
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 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 {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.EnqueueZettel(id.Zid(1))
	ar.EnqueueZettel(id.Zid(2))

|






>
>
>







|




|

|
|
|







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

package 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, lastReload := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || lastReload {
		t.Errorf("Expected arZettel/1/false, but got %v/%v/%v", action, zid, lastReload)
	}
	_, zid, _ = ar.Dequeue()
	if zid != id.Invalid {
		t.Errorf("Expected invalid Zid, but got %v", zid)
	}
	ar.EnqueueZettel(id.Zid(1))
	ar.EnqueueZettel(id.Zid(2))
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))







|







51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
	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)
	}
}







|










|







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



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package 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 := range len(mgr.boxes) - 2 {
		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 {
	if err := mgr.checkContinue(ctx); err != nil {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		return box.CanCreateZettel(ctx)
	}
	return false
}

// CreateZettel creates a new zettel.
func (mgr *Manager) CreateZettel(ctx context.Context, ztl zettel.Zettel) (id.Zid, error) {
	mgr.mgrLog.Debug().Msg("CreateZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return id.Invalid, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		ztl.Meta = mgr.cleanMetaProperties(ztl.Meta)
		zidO, err := box.CreateZettel(ctx, ztl)
		if err == nil {
			mgr.idxUpdateZettel(ctx, ztl)
		}
		return zidO, err
	}
	return id.Invalid, box.ErrReadOnly
}

// 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 err := mgr.checkContinue(ctx); err != nil {
		return zettel.Zettel{}, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()

	return mgr.getZettel(ctx, zid)
}
func (mgr *Manager) getZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	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 err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}
	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 err := mgr.checkContinue(ctx); err != nil {









		return nil, err
	}




	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()

	return mgr.fetchZids(ctx)
}
func (mgr *Manager) fetchZids(ctx context.Context) (*id.Set, error) {
	numZettel := 0
	for _, p := range mgr.boxes {







		var mbstats box.ManagedBoxStats



		p.ReadStats(&mbstats)

		numZettel += mbstats.Zettel

	}
	result := id.NewSetCap(numZettel)
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded)
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

// FetchZidsO returns the set of all old-style zettel identifer managed by the box.
func (mgr *Manager) FetchZidsO(ctx context.Context) (*id.Set, error) {
	mgr.mgrLog.Debug().Msg("FetchZidsO")
	return mgr.fetchZids(ctx)
}

func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, bx := range mgr.boxes {
		if bx.HasZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// GetMeta returns just the metadata of the zettel with the given identifier.
func (mgr *Manager) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	mgr.mgrLog.Debug().Zid(zid).Msg("GetMeta")
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}

	m, err := mgr.idxStore.GetMeta(ctx, zid)
	if err != nil {
		// TODO: Call GetZettel and return just metadata, in case the index is not complete.
		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 err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}
	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.NewSet()
		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.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 {
	if err := mgr.checkContinue(ctx); err != nil {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		return box.CanUpdateZettel(ctx, zettel)
	}
	return false

}

// 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 err := mgr.checkContinue(ctx); err != nil {


		return err
	}







	return mgr.updateZettel(ctx, zettel)
}


func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error {

	if box, isWriteBox := mgr.boxes[0].(box.WriteBox); isWriteBox {
		zettel.Meta = mgr.cleanMetaProperties(zettel.Meta)
		if err := box.UpdateZettel(ctx, zettel); err != nil {
			return err
		}

		mgr.idxUpdateZettel(ctx, zettel)
		return nil
	}










	return box.ErrReadOnly











}

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


		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, zidO id.Zid) error {
	mgr.mgrLog.Debug().Zid(zidO).Msg("DeleteZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()



	for _, p := range mgr.boxes {
		err := p.DeleteZettel(ctx, zidO)
		if err == nil {
			mgr.idxDeleteZettel(ctx, zidO)
			return err
		}
		var errZNF box.ErrZettelNotFound
		if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrZettelNotFound{Zid: zidO}
}

// Remove all (computed) properties from metadata before storing the zettel.
func (mgr *Manager) cleanMetaProperties(m *meta.Meta) *meta.Meta {
	result := m.Clone()
	for _, p := range result.ComputedPairsRest() {
		if mgr.propertyKeys.Has(p.Key) {
			result.Delete(p.Key)
		}
	}
	return result
}

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

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

|






>
>
>









|
|



|







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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/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()
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.Zid(zid)
	}
}







|


75
76
77
78
79
80
81
82
83
84
	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
45
46
47
48
49
50
//-----------------------------------------------------------------------------
// 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.
		// In the month 000101, the day must be > 0.
		return "00010101000000"
	}
	seconds := zid % 100
	if seconds > 59 {
		seconds = 59

|






>
>
>








|

|
|




<

|
>



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




|







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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package manager

import (
	"context"
	"strconv"

	"t73f.de/r/zsc/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.
	_, hasCreated := m.Get(api.KeyCreated)
	if !hasCreated {
		m.Set(api.KeyCreated, computeCreated(m.Zid))
	}

	if box.DoEnrich(ctx) {




		computePublished(m)
		if boxNumber > 0 {
			m.Set(api.KeyBoxNumber, strconv.Itoa(boxNumber))
		}
		mgr.idxStore.Enrich(ctx, m)
	}

	if !hasCreated {
		m.Set(meta.KeyCreatedMissing, api.ValueTrue)
	}
}

func computeCreated(zid id.Zid) string {
	if zid <= 10101000000 {
		// A year 0000 is not allowed and therefore an artificial Zid.
		// In the year 0001, the month must be > 0.
		// In the month 000101, the day must be > 0.
		return "00010101000000"
	}
	seconds := zid % 100
	if seconds > 59 {
		seconds = 59

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
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
//-----------------------------------------------------------------------------
// 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")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchPrefix(prefix string) id.Set {
	found := mgr.idxStore.SearchPrefix(prefix)
	mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(len(found))).Msg("SearchPrefix")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchSuffix returns all zettel that have a word with the given suffix.
// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchSuffix(suffix string) id.Set {
	found := mgr.idxStore.SearchSuffix(suffix)
	mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(len(found))).Msg("SearchSuffix")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchContains returns all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchContains(s string) id.Set {
	found := mgr.idxStore.SearchContains(s)
	mgr.idxLog.Debug().Str("s", s).Int("found", int64(len(found))).Msg("SearchContains")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// 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())
	for {
		// Sleep first, so the indexer will wait for boxes to initialize.
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}
		mgr.idxWorkService(ctx)
	}
}

func (mgr *Manager) idxWorkService(ctx context.Context) {
	var roomNum uint64
	var start time.Time
	for {
		switch action, zid, arRoomNum := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			mgr.idxLog.Debug().Msg("reload")
			roomNum = 0
			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				if rno := mgr.idxAr.Reload(zids); rno > 0 {
					roomNum = rno
				}
				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now().Local()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arZettel:
			mgr.idxLog.Debug().Zid(zid).Msg("zettel")
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// Zettel was deleted or is not accessible b/c of other reasons
				mgr.idxLog.Trace().Zid(zid).Msg("delete")
				mgr.idxMx.Lock()
				mgr.idxSinceReload++
				mgr.idxMx.Unlock()
				mgr.idxDeleteZettel(zid)
				continue
			}
			mgr.idxLog.Trace().Zid(zid).Msg("update")

			mgr.idxMx.Lock()
			if arRoomNum == roomNum {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()
			mgr.idxUpdateZettel(ctx, zettel)
		}
	}
}

func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-mgr.idxReady:
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}

		timer.Reset(timerDuration)
	case <-mgr.done:
		if !timer.Stop() {
			<-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) {
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}

|






>
>
>












<
<
<



>
>
>




|

|








|

|








|

|








|

|











|
|








|



<




<


|




<



|
<
<











<
<
<
|



>

|




<














>










|


>
|
|
>

|





>
>
>
>
>
>
>
>
>
>
>







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

package 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(found.Length())).Msg("SearchEqual")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchPrefix(prefix string) *id.Set {
	found := mgr.idxStore.SearchPrefix(prefix)
	mgr.idxLog.Debug().Str("prefix", prefix).Int("found", int64(found.Length())).Msg("SearchPrefix")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchSuffix returns all zettel that have a word with the given suffix.
// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchSuffix(suffix string) *id.Set {
	found := mgr.idxStore.SearchSuffix(suffix)
	mgr.idxLog.Debug().Str("suffix", suffix).Int("found", int64(found.Length())).Msg("SearchSuffix")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// SearchContains returns all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchContains(s string) *id.Set {
	found := mgr.idxStore.SearchContains(s)
	mgr.idxLog.Debug().Str("s", s).Int("found", int64(found.Length())).Msg("SearchContains")
	if msg := mgr.idxLog.Trace(); msg.Enabled() {
		msg.Str("ids", fmt.Sprint(found)).Msg("IDs")
	}
	return found
}

// 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())
	for {
		mgr.idxWorkService(ctx)
		if !mgr.idxSleepService(timer, timerDuration) {
			return
		}

	}
}

func (mgr *Manager) idxWorkService(ctx context.Context) {

	var start time.Time
	for {
		switch action, zid, lastReload := mgr.idxAr.Dequeue(); action {
		case arNothing:
			return
		case arReload:
			mgr.idxLog.Debug().Msg("reload")

			zids, err := mgr.FetchZids(ctx)
			if err == nil {
				start = time.Now()
				mgr.idxAr.Reload(zids)


				mgr.idxMx.Lock()
				mgr.idxLastReload = time.Now().Local()
				mgr.idxSinceReload = 0
				mgr.idxMx.Unlock()
			}
		case arZettel:
			mgr.idxLog.Debug().Zid(zid).Msg("zettel")
			zettel, err := mgr.GetZettel(ctx, zid)
			if err != nil {
				// Zettel was deleted or is not accessible b/c of other reasons
				mgr.idxLog.Trace().Zid(zid).Msg("delete")



				mgr.idxDeleteZettel(ctx, zid)
				continue
			}
			mgr.idxLog.Trace().Zid(zid).Msg("update")
			mgr.idxUpdateZettel(ctx, zettel)
			mgr.idxMx.Lock()
			if lastReload {
				mgr.idxDurReload = time.Since(start)
			}
			mgr.idxSinceReload++
			mgr.idxMx.Unlock()

		}
	}
}

func (mgr *Manager) idxSleepService(timer *time.Timer, timerDuration time.Duration) bool {
	select {
	case _, ok := <-mgr.idxReady:
		if !ok {
			return false
		}
	case _, ok := <-timer.C:
		if !ok {
			return false
		}
		// mgr.idxStore.Optimize() // TODO: make it less often, for example once per 10 minutes
		timer.Reset(timerDuration)
	case <-mgr.done:
		if !timer.Stop() {
			<-timer.C
		}
		return false
	}
	return true
}

func (mgr *Manager) idxUpdateZettel(ctx context.Context, zettel zettel.Zettel) {
	var cData collectData
	cData.initialize()
	if mustIndexZettel(zettel.Meta) {
		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 mustIndexZettel(m *meta.Meta) bool {
	zid := m.Zid
	if zid >= id.DefaultHomeZid {
		return true
	}
	if zid >= id.Zid(100000000) && zid < id.Zid(9999999997) {
		return true
	}
	return false
}

func (mgr *Manager) idxCollectFromMeta(ctx context.Context, m *meta.Meta, zi *store.ZettelIndex, cData *collectData) {
	for _, pair := range m.ComputedPairs() {
		descr := meta.GetDescription(pair.Key)
		if descr.IsProperty() {
			continue
		}
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
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:

			for _, word := range strfun.NormalizeWords(pair.Value) {
				cData.words.Add(word)



			}
		}
	}
}











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

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







>
|
|
>
>
>





>
>
>
>
>
>
>
>
>
>

|
|




|









|







|


|
|



|
|

|

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
			is := parser.ParseMetadata(pair.Value)
			collectInlineIndexData(&is, cData)
		case meta.TypeURL:
			if _, err := url.Parse(pair.Value); err == nil {
				cData.urls.Add(pair.Value)
			}
		default:
			if descr.Type.IsSet {
				for _, val := range meta.ListFromValue(pair.Value) {
					idxCollectMetaValue(cData.words, val)
				}
			} else {
				idxCollectMetaValue(cData.words, pair.Value)
			}
		}
	}
}

func idxCollectMetaValue(stWords store.WordSet, value string) {
	if words := strfun.NormalizeWords(value); len(words) > 0 {
		for _, word := range words {
			stWords.Add(word)
		}
	} else {
		stWords.Add(value)
	}
}

func (mgr *Manager) idxProcessData(ctx context.Context, zi *store.ZettelIndex, cData *collectData) {
	cData.refs.ForEach(func(ref id.Zid) {
		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(ctx context.Context, zid id.Zid) {
	toCheck := mgr.idxStore.DeleteZettel(ctx, zid)
	mgr.idxCheckZettel(toCheck)
}

func (mgr *Manager) idxCheckZettel(s *id.Set) {
	s.ForEach(func(zid id.Zid) {
		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
39
40
41
42
43
44
45
46
//-----------------------------------------------------------------------------
// 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
	Notify   chan<- box.UpdateInfo
}

// Connect returns a handle to the specified box.
func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) {
	if authManager.IsReadonly() {
		rawURL := u.String()
		// TODO: the following is wrong under some circumstances:

|






>
>
>












<


|


<
<



>
>







|







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

// Package manager coordinates the various boxes and indexes of a Zettelstore.
package manager

import (
	"context"
	"io"
	"net/url"
	"sync"
	"time"


	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/mapstore"
	"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
	Notify   box.UpdateNotifier
}

// Connect returns a handle to the specified box.
func Connect(u *url.URL, authManager auth.BaseManager, cdata *ConnectData) (box.ManagedBox, error) {
	if authManager.IsReadonly() {
		rawURL := u.String()
		// TODO: the following is wrong under some circumstances:
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
func Register(scheme string, create createFunc) {
	if _, ok := registry[scheme]; ok {
		panic(scheme)
	}
	registry[scheme] = create
}

// GetSchemes returns all registered scheme, ordered by scheme string.
func GetSchemes() []string { return maps.Keys(registry) }

// Manager is a coordinating box.
type Manager struct {
	mgrLog       *logger.Logger
	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(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 {
			return nil, err
		}
		if p != nil {







<
<
<



|
|
>











|








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


















|
|


>
|







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
func Register(scheme string, create createFunc) {
	if _, ok := registry[scheme]; ok {
		panic(scheme)
	}
	registry[scheme] = create
}




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

// State returns the box.StartState of the manager.
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: createIdxStore(rtConfig),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.notifyChanged}
	boxes := make([]box.ManagedBox, 0, len(boxURIs)+2)
	for _, uri := range boxURIs {
		p, err := Connect(uri, authManager, &cdata)
		if err != nil {
			return nil, err
		}
		if p != nil {
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
		return nil, err
	}
	cdata.Number++
	boxes = append(boxes, constbox, compbox)
	mgr.boxes = boxes
	return mgr, nil
}





// RegisterObserver registers an observer that will be notified
// if a zettel was found to be changed.
func (mgr *Manager) RegisterObserver(f box.UpdateFunc) {
	if f != nil {
		mgr.mxObserver.Lock()
		mgr.observers = append(mgr.observers, f)
		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 {







>
>
>
>














|
|







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
		return nil, err
	}
	cdata.Number++
	boxes = append(boxes, constbox, compbox)
	mgr.boxes = boxes
	return mgr, nil
}

func createIdxStore(_ config.Config) store.Store {
	return mapstore.New()
}

// RegisterObserver registers an observer that will be notified
// if a zettel was found to be changed.
func (mgr *Manager) RegisterObserver(f box.UpdateFunc) {
	if f != nil {
		mgr.mxObserver.Lock()
		mgr.observers = append(mgr.observers, f)
		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
		}
	}
}








>
>




>
|
>







210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

				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
				}

				isStarted := mgr.State() == box.StartStateStarted
				mgr.idxEnqueue(reason, zid)
				if ci.Box == nil {
					ci.Box = mgr
				}
				if isStarted {
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
}

222
223
224
225
226
227
228
229
230


231
232
233
234


235

236
237
238
239
240
241
242
	cache[zid] = destutterData{
		deadAt: now.Add(500 * time.Millisecond),
		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:
	}
}







|

>
>



|
>
>

>







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
	cache[zid] = destutterData{
		deadAt: now.Add(500 * time.Millisecond),
		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zidO id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zidO)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zidO)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zidO).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
319










320
321
322
323
324
325
326
	}
}

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











// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))







<
|
>


>









>





|





>
>
>
>
>
>

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





|


>






|





|
<
<
|


>
>







>
>
>
>
>
>
>
>
>
>







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

// 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.Info().Msg("Waiting for more than one minute to start")
			} else {
				mgr.mgrLog.Trace().Msg("Wait for boxes to start")
			}
		}
		time.Sleep(waitTime)
	}
}

func (mgr *Manager) allBoxesStarted() bool {
	for _, bx := range mgr.boxes {
		if b, ok := bx.(box.StartStopper); ok && b.State() != box.StartStateStarted {
			return false
		}
	}
	return true
}

// 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 err := mgr.checkContinue(ctx); err != nil {
		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 err := mgr.checkContinue(ctx); err != nil {


		return err
	}
	mgr.infos <- box.UpdateInfo{Reason: box.OnReload, Zid: id.Invalid}
	mgr.mgrMx.Lock()
	defer mgr.mgrMx.Unlock()
	for _, bx := range mgr.boxes {
		if rb, ok := bx.(box.Refresher); ok {
			rb.Refresh(ctx)
		}
	}
	return nil
}

// ReIndex data of the given zettel.
func (mgr *Manager) ReIndex(ctx context.Context, zid id.Zid) error {
	mgr.mgrLog.Debug().Msg("ReIndex")
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	mgr.infos <- box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: zid}
	return nil
}

// ReadStats populates st with box statistics.
func (mgr *Manager) ReadStats(st *box.Stats) {
	mgr.mgrLog.Debug().Msg("ReadStats")
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	subStats := make([]box.ManagedBoxStats, len(mgr.boxes))
353
354
355
356
357
358
359













	st.IndexedUrls = storeSt.Urls
}

// Dump internal data structures to a Writer.
func (mgr *Manager) Dump(w io.Writer) {
	mgr.idxStore.Dump(w)
}




















>
>
>
>
>
>
>
>
>
>
>
>
>
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
	st.IndexedUrls = storeSt.Urls
}

// Dump internal data structures to a Writer.
func (mgr *Manager) Dump(w io.Writer) {
	mgr.idxStore.Dump(w)
}

func (mgr *Manager) checkContinue(ctx context.Context) error {
	if mgr.State() != box.StartStateStarted {
		return box.ErrStopped
	}
	return ctx.Err()
}

func (mgr *Manager) notifyChanged(bbox box.BaseBox, zid id.Zid, reason box.UpdateReason) {
	if infos := mgr.infos; infos != nil {
		mgr.infos <- box.UpdateInfo{Box: bbox, Reason: reason, Zid: zid}
	}
}

Added box/manager/mapstore/mapstore.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
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
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
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

// Package mapstore stored the index in main memory via a Go map.
package mapstore

import (
	"context"
	"fmt"
	"io"
	"slices"
	"strings"
	"sync"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type zettelData struct {
	meta      *meta.Meta // a local copy of the metadata, without computed keys
	dead      *id.Set    // set of dead references in this zettel
	forward   *id.Set    // set of forward references in this zettel
	backward  *id.Set    // set of zettel that reference with zettel
	otherRefs map[string]bidiRefs
	words     []string // list of words of this zettel
	urls      []string // list of urls of this zettel
}

type bidiRefs struct {
	forward  *id.Set
	backward *id.Set
}

func (zd *zettelData) optimize() {
	zd.dead.Optimize()
	zd.forward.Optimize()
	zd.backward.Optimize()
	for _, bidi := range zd.otherRefs {
		bidi.forward.Optimize()
		bidi.backward.Optimize()
	}
}

type mapStore struct {
	mx     sync.RWMutex
	intern map[string]string // map to intern strings
	idx    map[id.Zid]*zettelData
	dead   map[id.Zid]*id.Set // map dead refs where they occur
	words  stringRefs
	urls   stringRefs

	// Stats
	mxStats sync.Mutex
	updates uint64
}
type stringRefs map[string]*id.Set

// New returns a new memory-based index store.
func New() store.Store {
	return &mapStore{
		intern: make(map[string]string, 1024),
		idx:    make(map[id.Zid]*zettelData),
		dead:   make(map[id.Zid]*id.Set),
		words:  make(stringRefs),
		urls:   make(stringRefs),
	}
}

func (ms *mapStore) 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 *mapStore) Enrich(_ context.Context, m *meta.Meta) {
	if ms.doEnrich(m) {
		ms.mxStats.Lock()
		ms.updates++
		ms.mxStats.Unlock()
	}
}

func (ms *mapStore) 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 !zi.dead.IsEmpty() {
		m.Set(api.KeyDead, zi.dead.MetaString())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Clone())
	if !zi.backward.IsEmpty() {
		m.Set(api.KeyBackward, zi.backward.MetaString())
		updated = true
	}
	if !zi.forward.IsEmpty() {
		m.Set(api.KeyForward, zi.forward.MetaString())
		back.ISubstract(zi.forward)
		updated = true
	}
	for k, refs := range zi.otherRefs {
		if !refs.backward.IsEmpty() {
			m.Set(k, refs.backward.MetaString())
			back.ISubstract(refs.backward)
			updated = true
		}
	}
	if !back.IsEmpty() {
		m.Set(api.KeyBack, back.MetaString())
		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 *mapStore) SearchEqual(word string) *id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result = result.IUnion(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result = result.IUnion(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {
		return result
	}

	return addBackwardZids(result, zid, zi)
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *mapStore) SearchPrefix(prefix string) *id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
	if err != nil {
		return result
	}
	var minZid id.Zid
	if l < 14 && prefix == "0000000000000"[:l] {
		minZid = id.Zid(1)
	} else {
		minZid, err = id.Parse(prefix + "00000000000000"[:14-l])
		if err != nil {
			return result
		}
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			result = addBackwardZids(result, zid, zi)
		}
	}
	return result
}

// SearchSuffix returns all zettel that have a word with the given suffix.
// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *mapStore) SearchSuffix(suffix string) *id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(suffix, strings.HasSuffix)
	l := len(suffix)
	if l > 14 {
		return result
	}
	val, err := id.ParseUint(suffix)
	if err != nil {
		return result
	}
	modulo := uint64(1)
	for range l {
		modulo *= 10
	}
	for zid, zi := range ms.idx {
		if uint64(zid)%modulo == val {
			result = addBackwardZids(result, zid, zi)
		}
	}
	return result
}

// SearchContains returns all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *mapStore) SearchContains(s string) *id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(s, strings.Contains)
	if len(s) > 14 {
		return result
	}
	if _, err := id.ParseUint(s); err != nil {
		return result
	}
	for zid, zi := range ms.idx {
		if strings.Contains(zid.String(), s) {
			result = addBackwardZids(result, zid, zi)
		}
	}
	return result
}

func (ms *mapStore) 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.IUnion(refs)
	}
	for u, refs := range ms.urls {
		if !pred(u, s) {
			continue
		}
		result.IUnion(refs)
	}
	return result
}

func addBackwardZids(result *id.Set, zid id.Zid, zi *zettelData) *id.Set {
	// Must only be called if ms.mx is read-locked!
	result = result.Add(zid)
	result = result.IUnion(zi.backward)
	for _, mref := range zi.otherRefs {
		result = result.IUnion(mref.backward)
	}
	return result
}

func removeOtherMetaRefs(m *meta.Meta, back *id.Set) *id.Set {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:
			if zid, err := id.Parse(p.Value); err == nil {
				back = back.Remove(zid)
			}
		case meta.TypeIDSet:
			for _, val := range meta.ListFromValue(p.Value) {
				if zid, err := id.Parse(val); err == nil {
					back = back.Remove(zid)
				}
			}
		}
	}
	return back
}

func (ms *mapStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) *id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()
	m := ms.makeMeta(zidx)
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelData{}
		ziExist = false
	}

	// Is this zettel an old dead reference mentioned in other zettel?
	var toCheck *id.Set
	if refs, ok := ms.dead[zidx.Zid]; ok {
		// These must be checked later again
		toCheck = refs
		delete(ms.dead, zidx.Zid)
	}

	zi.meta = m
	ms.updateDeadReferences(zidx, zi)
	ids := ms.updateForwardBackwardReferences(zidx, zi)
	toCheck = toCheck.IUnion(ids)
	ids = ms.updateMetadataReferences(zidx, zi)
	toCheck = toCheck.IUnion(ids)
	zi.words = updateStrings(zidx.Zid, ms.words, zi.words, zidx.GetWords())
	zi.urls = updateStrings(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls())

	// Check if zi must be inserted into ms.idx
	if !ziExist {
		ms.idx[zidx.Zid] = zi
	}
	zi.optimize()
	return toCheck
}

var internableKeys = map[string]bool{
	api.KeyRole:      true,
	api.KeySyntax:    true,
	api.KeyFolgeRole: true,
	api.KeyLang:      true,
	api.KeyReadOnly:  true,
}

func isInternableValue(key string) bool {
	if internableKeys[key] {
		return true
	}
	return strings.HasSuffix(key, meta.SuffixKeyRole)
}

func (ms *mapStore) internString(s string) string {
	if is, found := ms.intern[s]; found {
		return is
	}
	ms.intern[s] = s
	return s
}

func (ms *mapStore) 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 *mapStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelData) {
	// Must only be called if ms.mx is write-locked!
	drefs := zidx.GetDeadRefs()
	newRefs, remRefs := zi.dead.Diff(drefs)
	zi.dead = drefs
	remRefs.ForEach(func(ref id.Zid) {
		ms.dead[ref] = ms.dead[ref].Remove(zidx.Zid)
	})
	newRefs.ForEach(func(ref id.Zid) {
		ms.dead[ref] = ms.dead[ref].Add(zidx.Zid)
	})
}

func (ms *mapStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelData) *id.Set {
	// Must only be called if ms.mx is write-locked!
	brefs := zidx.GetBackRefs()
	newRefs, remRefs := zi.forward.Diff(brefs)
	zi.forward = brefs

	var toCheck *id.Set
	remRefs.ForEach(func(ref id.Zid) {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = bzi.backward.Remove(zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	})
	newRefs.ForEach(func(ref id.Zid) {
		bzi := ms.getOrCreateEntry(ref)
		bzi.backward = bzi.backward.Add(zidx.Zid)
		if bzi.meta == nil {
			toCheck = toCheck.Add(ref)
		}
	})
	return toCheck
}

func (ms *mapStore) 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 := mr.forward.Diff(mrefs)
		mr.forward = mrefs
		zi.otherRefs[key] = mr

		newRefs.ForEach(func(ref id.Zid) {
			bzi := ms.getOrCreateEntry(ref)
			if bzi.otherRefs == nil {
				bzi.otherRefs = make(map[string]bidiRefs)
			}
			bmr := bzi.otherRefs[key]
			bmr.backward = bmr.backward.Add(zidx.Zid)
			bzi.otherRefs[key] = bmr
			if bzi.meta == nil {
				toCheck = toCheck.Add(ref)
			}
		})

		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}
	return toCheck
}

func updateStrings(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		srefs[word] = srefs[word].Add(zid)
	}
	for _, word := range removeWords {
		refs, ok := srefs[word]
		if !ok {
			continue
		}
		refs = refs.Remove(zid)
		if refs.IsEmpty() {
			delete(srefs, word)
			continue
		}
		srefs[word] = refs
	}
	return next.Words()
}

func (ms *mapStore) 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 *mapStore) DeleteZettel(_ context.Context, zid id.Zid) *id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()
	return ms.doDeleteZettel(zid)
}

func (ms *mapStore) doDeleteZettel(zid id.Zid) *id.Set {
	// Must only be called if ms.mx is write-locked!
	zi, ok := ms.idx[zid]
	if !ok {
		return nil
	}

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)
	for key, mrefs := range zi.otherRefs {
		ms.removeInverseMeta(zid, key, mrefs.forward)
	}
	deleteStrings(ms.words, zi.words, zid)
	deleteStrings(ms.urls, zi.urls, zid)
	delete(ms.idx, zid)
	return toCheck
}

func (ms *mapStore) deleteDeadSources(zid id.Zid, zi *zettelData) {
	// Must only be called if ms.mx is write-locked!
	zi.dead.ForEach(func(ref id.Zid) {
		if drefs, ok := ms.dead[ref]; ok {
			if drefs = drefs.Remove(zid); drefs.IsEmpty() {
				delete(ms.dead, ref)
			} else {
				ms.dead[ref] = drefs
			}
		}
	})
}

func (ms *mapStore) deleteForwardBackward(zid id.Zid, zi *zettelData) *id.Set {
	// Must only be called if ms.mx is write-locked!
	zi.forward.ForEach(func(ref id.Zid) {
		if fzi, ok := ms.idx[ref]; ok {
			fzi.backward = fzi.backward.Remove(zid)
		}
	})

	var toCheck *id.Set
	zi.backward.ForEach(func(ref id.Zid) {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = bzi.forward.Remove(zid)
			toCheck = toCheck.Add(ref)
		}
	})
	return toCheck
}

func (ms *mapStore) removeInverseMeta(zid id.Zid, key string, forward *id.Set) {
	// Must only be called if ms.mx is write-locked!
	forward.ForEach(func(ref id.Zid) {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.otherRefs == nil {
			return
		}
		bmr, ok := bzi.otherRefs[key]
		if !ok {
			return
		}
		bmr.backward = bmr.backward.Remove(zid)
		if !bmr.backward.IsEmpty() || !bmr.forward.IsEmpty() {
			bzi.otherRefs[key] = bmr
		} else {
			delete(bzi.otherRefs, key)
			if len(bzi.otherRefs) == 0 {
				bzi.otherRefs = nil
			}
		}
	})
}

func deleteStrings(msStringMap stringRefs, curStrings []string, zid id.Zid) {
	// Must only be called if ms.mx is write-locked!
	for _, word := range curStrings {
		refs, ok := msStringMap[word]
		if !ok {
			continue
		}
		refs = refs.Remove(zid)
		if refs.IsEmpty() {
			delete(msStringMap, word)
			continue
		}
		msStringMap[word] = refs
	}
}

func (ms *mapStore) Optimize() {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	// No need to optimize ms.idx: is already done via ms.UpdateReferences
	for _, dead := range ms.dead {
		dead.Optimize()
	}
	for _, s := range ms.words {
		s.Optimize()
	}
	for _, s := range ms.urls {
		s.Optimize()
	}
}

func (ms *mapStore) 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 *mapStore) Dump(w io.Writer) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()

	io.WriteString(w, "=== Dump\n")
	ms.dumpIndex(w)
	ms.dumpDead(w)
	dumpStringRefs(w, "Words", "", "", ms.words)
	dumpStringRefs(w, "URLs", "[[", "]]", ms.urls)
}

func (ms *mapStore) dumpIndex(w io.Writer) {
	if len(ms.idx) == 0 {
		return
	}
	io.WriteString(w, "==== Zettel Index\n")
	zids := make(id.Slice, 0, len(ms.idx))
	for id := range ms.idx {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, "=====", id)
		zi := ms.idx[id]
		if !zi.dead.IsEmpty() {
			fmt.Fprintln(w, "* Dead:", zi.dead)
		}
		dumpSet(w, "* Forward:", zi.forward)
		dumpSet(w, "* Backward:", zi.backward)

		otherRefs := make([]string, 0, len(zi.otherRefs))
		for k := range zi.otherRefs {
			otherRefs = append(otherRefs, k)
		}
		slices.Sort(otherRefs)
		for _, k := range otherRefs {
			fmt.Fprintln(w, "* Meta", k)
			dumpSet(w, "** Forward:", zi.otherRefs[k].forward)
			dumpSet(w, "** Backward:", zi.otherRefs[k].backward)
		}
		dumpStrings(w, "* Words", "", "", zi.words)
		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
	}
}

func (ms *mapStore) dumpDead(w io.Writer) {
	if len(ms.dead) == 0 {
		return
	}
	fmt.Fprintf(w, "==== Dead References\n")
	zids := make(id.Slice, 0, len(ms.dead))
	for id := range ms.dead {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, ";", id)
		fmt.Fprintln(w, ":", ms.dead[id])
	}
}

func dumpSet(w io.Writer, prefix string, s *id.Set) {
	if !s.IsEmpty() {
		io.WriteString(w, prefix)
		s.ForEach(func(zid id.Zid) {
			io.WriteString(w, " ")
			w.Write(zid.Bytes())
		})
		fmt.Fprintln(w)
	}
}
func dumpStrings(w io.Writer, title, preString, postString string, slice []string) {
	if len(slice) > 0 {
		sl := make([]string, len(slice))
		copy(sl, slice)
		slices.Sort(sl)
		fmt.Fprintln(w, title)
		for _, s := range sl {
			fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString)
		}
	}
}

func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) {
	if len(srefs) == 0 {
		return
	}
	fmt.Fprintln(w, "====", title)
	for _, s := range maps.Keys(srefs) {
		fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		fmt.Fprintln(w, ":", srefs[s])
	}
}

Deleted box/manager/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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
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
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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//-----------------------------------------------------------------------------

// Package memstore stored the index in main memory.
package memstore

import (
	"context"
	"fmt"
	"io"
	"sort"
	"strings"
	"sync"

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

type metaRefs struct {
	forward  id.Slice
	backward id.Slice
}

type zettelIndex struct {
	dead     id.Slice
	forward  id.Slice
	backward id.Slice
	meta     map[string]metaRefs
	words    []string
	urls     []string
}

func (zi *zettelIndex) isEmpty() bool {
	if len(zi.forward) > 0 || len(zi.backward) > 0 || len(zi.dead) > 0 || len(zi.words) > 0 {
		return false
	}
	return len(zi.meta) == 0
}

type stringRefs map[string]id.Slice

type memStore struct {
	mx    sync.RWMutex
	idx   map[id.Zid]*zettelIndex
	dead  map[id.Zid]id.Slice // map dead refs where they occur
	words stringRefs
	urls  stringRefs

	// Stats
	updates uint64
}

// New returns a new memory-based index store.
func New() store.Store {
	return &memStore{
		idx:   make(map[id.Zid]*zettelIndex),
		dead:  make(map[id.Zid]id.Slice),
		words: make(stringRefs),
		urls:  make(stringRefs),
	}
}

func (ms *memStore) Enrich(_ context.Context, m *meta.Meta) {
	if ms.doEnrich(m) {
		ms.mx.Lock()
		ms.updates++
		ms.mx.Unlock()
	}
}

func (ms *memStore) doEnrich(m *meta.Meta) bool {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	zi, ok := ms.idx[m.Zid]
	if !ok {
		return false
	}
	var updated bool
	if len(zi.dead) > 0 {
		m.Set(api.KeyDead, zi.dead.String())
		updated = true
	}
	back := removeOtherMetaRefs(m, zi.backward.Copy())
	if len(zi.backward) > 0 {
		m.Set(api.KeyBackward, zi.backward.String())
		updated = true
	}
	if len(zi.forward) > 0 {
		m.Set(api.KeyForward, zi.forward.String())
		back = remRefs(back, zi.forward)
		updated = true
	}
	for k, refs := range zi.meta {
		if len(refs.backward) > 0 {
			m.Set(k, refs.backward.String())
			back = remRefs(back, refs.backward)
			updated = true
		}
	}
	if len(back) > 0 {
		m.Set(api.KeyBack, back.String())
		updated = true
	}
	return updated
}

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchEqual(word string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := id.NewSet()
	if refs, ok := ms.words[word]; ok {
		result.AddSlice(refs)
	}
	if refs, ok := ms.urls[word]; ok {
		result.AddSlice(refs)
	}
	zid, err := id.Parse(word)
	if err != nil {
		return result
	}
	zi, ok := ms.idx[zid]
	if !ok {
		return result
	}

	addBackwardZids(result, zid, zi)
	return result
}

// SearchPrefix returns all zettel that have a word with the given prefix.
// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchPrefix(prefix string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(prefix, strings.HasPrefix)
	l := len(prefix)
	if l > 14 {
		return result
	}
	maxZid, err := id.Parse(prefix + "99999999999999"[:14-l])
	if err != nil {
		return result
	}
	var minZid id.Zid
	if l < 14 && prefix == "0000000000000"[:l] {
		minZid = id.Zid(1)
	} else {
		minZid, err = id.Parse(prefix + "00000000000000"[:14-l])
		if err != nil {
			return result
		}
	}
	for zid, zi := range ms.idx {
		if minZid <= zid && zid <= maxZid {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
}

// SearchSuffix returns all zettel that have a word with the given suffix.
// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchSuffix(suffix string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(suffix, strings.HasSuffix)
	l := len(suffix)
	if l > 14 {
		return result
	}
	val, err := id.ParseUint(suffix)
	if err != nil {
		return result
	}
	modulo := uint64(1)
	for i := 0; i < l; i++ {
		modulo *= 10
	}
	for zid, zi := range ms.idx {
		if uint64(zid)%modulo == val {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
}

// SearchContains returns all zettel that contains the given string.
// The string must be normalized through Unicode NKFD, trimmed and not empty.
func (ms *memStore) SearchContains(s string) id.Set {
	ms.mx.RLock()
	defer ms.mx.RUnlock()
	result := ms.selectWithPred(s, strings.Contains)
	if len(s) > 14 {
		return result
	}
	if _, err := id.ParseUint(s); err != nil {
		return result
	}
	for zid, zi := range ms.idx {
		if strings.Contains(zid.String(), s) {
			addBackwardZids(result, zid, zi)
		}
	}
	return result
}

func (ms *memStore) selectWithPred(s string, pred func(string, string) bool) id.Set {
	// Must only be called if ms.mx is read-locked!
	result := id.NewSet()
	for word, refs := range ms.words {
		if !pred(word, s) {
			continue
		}
		result.AddSlice(refs)
	}
	for u, refs := range ms.urls {
		if !pred(u, s) {
			continue
		}
		result.AddSlice(refs)
	}
	return result
}

func addBackwardZids(result id.Set, zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is read-locked!
	result.Zid(zid)
	result.AddSlice(zi.backward)
	for _, mref := range zi.meta {
		result.AddSlice(mref.backward)
	}
}

func removeOtherMetaRefs(m *meta.Meta, back id.Slice) id.Slice {
	for _, p := range m.PairsRest() {
		switch meta.Type(p.Key) {
		case meta.TypeID:
			if zid, err := id.Parse(p.Value); err == nil {
				back = remRef(back, zid)
			}
		case meta.TypeIDSet:
			for _, val := range meta.ListFromValue(p.Value) {
				if zid, err := id.Parse(val); err == nil {
					back = remRef(back, zid)
				}
			}
		}
	}
	return back
}

func (ms *memStore) UpdateReferences(_ context.Context, zidx *store.ZettelIndex) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()
	zi, ziExist := ms.idx[zidx.Zid]
	if !ziExist || zi == nil {
		zi = &zettelIndex{}
		ziExist = false
	}

	// Is this zettel an old dead reference mentioned in other zettel?
	var toCheck id.Set
	if refs, ok := ms.dead[zidx.Zid]; ok {
		// These must be checked later again
		toCheck = id.NewSet(refs...)
		delete(ms.dead, zidx.Zid)
	}

	ms.updateDeadReferences(zidx, zi)
	ms.updateForwardBackwardReferences(zidx, zi)
	ms.updateMetadataReferences(zidx, zi)
	zi.words = updateWordSet(zidx.Zid, ms.words, zi.words, zidx.GetWords())
	zi.urls = updateWordSet(zidx.Zid, ms.urls, zi.urls, zidx.GetUrls())

	// Check if zi must be inserted into ms.idx
	if !ziExist && !zi.isEmpty() {
		ms.idx[zidx.Zid] = zi
	}

	return toCheck
}

func (ms *memStore) updateDeadReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	drefs := zidx.GetDeadRefs()
	newRefs, remRefs := refsDiff(drefs, zi.dead)
	zi.dead = drefs
	for _, ref := range remRefs {
		ms.dead[ref] = remRef(ms.dead[ref], zidx.Zid)
	}
	for _, ref := range newRefs {
		ms.dead[ref] = addRef(ms.dead[ref], zidx.Zid)
	}
}

func (ms *memStore) updateForwardBackwardReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	brefs := zidx.GetBackRefs()
	newRefs, remRefs := refsDiff(brefs, zi.forward)
	zi.forward = brefs
	for _, ref := range remRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = remRef(bzi.backward, zidx.Zid)
	}
	for _, ref := range newRefs {
		bzi := ms.getEntry(ref)
		bzi.backward = addRef(bzi.backward, zidx.Zid)
	}
}

func (ms *memStore) updateMetadataReferences(zidx *store.ZettelIndex, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	metarefs := zidx.GetMetaRefs()
	for key, mr := range zi.meta {
		if _, ok := metarefs[key]; ok {
			continue
		}
		ms.removeInverseMeta(zidx.Zid, key, mr.forward)
	}
	if zi.meta == nil {
		zi.meta = make(map[string]metaRefs)
	}
	for key, mrefs := range metarefs {
		mr := zi.meta[key]
		newRefs, remRefs := refsDiff(mrefs, mr.forward)
		mr.forward = mrefs
		zi.meta[key] = mr

		for _, ref := range newRefs {
			bzi := ms.getEntry(ref)
			if bzi.meta == nil {
				bzi.meta = make(map[string]metaRefs)
			}
			bmr := bzi.meta[key]
			bmr.backward = addRef(bmr.backward, zidx.Zid)
			bzi.meta[key] = bmr
		}
		ms.removeInverseMeta(zidx.Zid, key, remRefs)
	}
}

func updateWordSet(zid id.Zid, srefs stringRefs, prev []string, next store.WordSet) []string {
	// Must only be called if ms.mx is write-locked!
	newWords, removeWords := next.Diff(prev)
	for _, word := range newWords {
		if refs, ok := srefs[word]; ok {
			srefs[word] = addRef(refs, zid)
			continue
		}
		srefs[word] = id.Slice{zid}
	}
	for _, word := range removeWords {
		refs, ok := srefs[word]
		if !ok {
			continue
		}
		refs2 := remRef(refs, zid)
		if len(refs2) == 0 {
			delete(srefs, word)
			continue
		}
		srefs[word] = refs2
	}
	return next.Words()
}

func (ms *memStore) getEntry(zid id.Zid) *zettelIndex {
	// Must only be called if ms.mx is write-locked!
	if zi, ok := ms.idx[zid]; ok {
		return zi
	}
	zi := &zettelIndex{}
	ms.idx[zid] = zi
	return zi
}

func (ms *memStore) DeleteZettel(_ context.Context, zid id.Zid) id.Set {
	ms.mx.Lock()
	defer ms.mx.Unlock()

	zi, ok := ms.idx[zid]
	if !ok {
		return nil
	}

	ms.deleteDeadSources(zid, zi)
	toCheck := ms.deleteForwardBackward(zid, zi)
	if len(zi.meta) > 0 {
		for key, mrefs := range zi.meta {
			ms.removeInverseMeta(zid, key, mrefs.forward)
		}
	}
	ms.deleteWords(zid, zi.words)
	delete(ms.idx, zid)
	return toCheck
}

func (ms *memStore) deleteDeadSources(zid id.Zid, zi *zettelIndex) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range zi.dead {
		if drefs, ok := ms.dead[ref]; ok {
			drefs = remRef(drefs, zid)
			if len(drefs) > 0 {
				ms.dead[ref] = drefs
			} else {
				delete(ms.dead, ref)
			}
		}
	}
}

func (ms *memStore) deleteForwardBackward(zid id.Zid, zi *zettelIndex) id.Set {
	// Must only be called if ms.mx is write-locked!
	var toCheck id.Set
	for _, ref := range zi.forward {
		if fzi, ok := ms.idx[ref]; ok {
			fzi.backward = remRef(fzi.backward, zid)
		}
	}
	for _, ref := range zi.backward {
		if bzi, ok := ms.idx[ref]; ok {
			bzi.forward = remRef(bzi.forward, zid)
			if toCheck == nil {
				toCheck = id.NewSet()
			}
			toCheck.Zid(ref)
		}
	}
	return toCheck
}

func (ms *memStore) removeInverseMeta(zid id.Zid, key string, forward id.Slice) {
	// Must only be called if ms.mx is write-locked!
	for _, ref := range forward {
		bzi, ok := ms.idx[ref]
		if !ok || bzi.meta == nil {
			continue
		}
		bmr, ok := bzi.meta[key]
		if !ok {
			continue
		}
		bmr.backward = remRef(bmr.backward, zid)
		if len(bmr.backward) > 0 || len(bmr.forward) > 0 {
			bzi.meta[key] = bmr
		} else {
			delete(bzi.meta, key)
			if len(bzi.meta) == 0 {
				bzi.meta = nil
			}
		}
	}
}

func (ms *memStore) deleteWords(zid id.Zid, words []string) {
	// Must only be called if ms.mx is write-locked!
	for _, word := range words {
		refs, ok := ms.words[word]
		if !ok {
			continue
		}
		refs2 := remRef(refs, zid)
		if len(refs2) == 0 {
			delete(ms.words, word)
			continue
		}
		ms.words[word] = refs2
	}
}

func (ms *memStore) ReadStats(st *store.Stats) {
	ms.mx.RLock()
	st.Zettel = len(ms.idx)
	st.Updates = ms.updates
	st.Words = uint64(len(ms.words))
	st.Urls = uint64(len(ms.urls))
	ms.mx.RUnlock()
}

func (ms *memStore) Dump(w io.Writer) {
	ms.mx.RLock()
	defer ms.mx.RUnlock()

	io.WriteString(w, "=== Dump\n")
	ms.dumpIndex(w)
	ms.dumpDead(w)
	dumpStringRefs(w, "Words", "", "", ms.words)
	dumpStringRefs(w, "URLs", "[[", "]]", ms.urls)
}

func (ms *memStore) dumpIndex(w io.Writer) {
	if len(ms.idx) == 0 {
		return
	}
	io.WriteString(w, "==== Zettel Index\n")
	zids := make(id.Slice, 0, len(ms.idx))
	for id := range ms.idx {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, "=====", id)
		zi := ms.idx[id]
		if len(zi.dead) > 0 {
			fmt.Fprintln(w, "* Dead:", zi.dead)
		}
		dumpZids(w, "* Forward:", zi.forward)
		dumpZids(w, "* Backward:", zi.backward)
		for k, fb := range zi.meta {
			fmt.Fprintln(w, "* Meta", k)
			dumpZids(w, "** Forward:", fb.forward)
			dumpZids(w, "** Backward:", fb.backward)
		}
		dumpStrings(w, "* Words", "", "", zi.words)
		dumpStrings(w, "* URLs", "[[", "]]", zi.urls)
	}
}

func (ms *memStore) dumpDead(w io.Writer) {
	if len(ms.dead) == 0 {
		return
	}
	fmt.Fprintf(w, "==== Dead References\n")
	zids := make(id.Slice, 0, len(ms.dead))
	for id := range ms.dead {
		zids = append(zids, id)
	}
	zids.Sort()
	for _, id := range zids {
		fmt.Fprintln(w, ";", id)
		fmt.Fprintln(w, ":", ms.dead[id])
	}
}

func dumpZids(w io.Writer, prefix string, zids id.Slice) {
	if len(zids) > 0 {
		io.WriteString(w, prefix)
		for _, zid := range zids {
			io.WriteString(w, " ")
			w.Write(zid.Bytes())
		}
		fmt.Fprintln(w)
	}
}

func dumpStrings(w io.Writer, title, preString, postString string, slice []string) {
	if len(slice) > 0 {
		sl := make([]string, len(slice))
		copy(sl, slice)
		sort.Strings(sl)
		fmt.Fprintln(w, title)
		for _, s := range sl {
			fmt.Fprintf(w, "** %s%s%s\n", preString, s, postString)
		}
	}

}

func dumpStringRefs(w io.Writer, title, preString, postString string, srefs stringRefs) {
	if len(srefs) == 0 {
		return
	}
	fmt.Fprintln(w, "====", title)
	for _, s := range maps.Keys(srefs) {
		fmt.Fprintf(w, "; %s%s%s\n", preString, s, postString)
		fmt.Fprintln(w, ":", srefs[s])
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































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

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

func refsDiff(refsN, refsO id.Slice) (newRefs, remRefs id.Slice) {
	npos, opos := 0, 0
	for npos < len(refsN) && opos < len(refsO) {
		rn, ro := refsN[npos], refsO[opos]
		if rn == ro {
			npos++
			opos++
			continue
		}
		if rn < ro {
			newRefs = append(newRefs, rn)
			npos++
			continue
		}
		remRefs = append(remRefs, ro)
		opos++
	}
	if npos < len(refsN) {
		newRefs = append(newRefs, refsN[npos:]...)
	}
	if opos < len(refsO) {
		remRefs = append(remRefs, refsO[opos:]...)
	}
	return newRefs, remRefs
}

func addRef(refs id.Slice, ref id.Zid) id.Slice {
	hi := len(refs)
	for lo := 0; lo < hi; {
		m := lo + (hi-lo)/2
		if r := refs[m]; r == ref {
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	refs = append(refs, id.Invalid)
	copy(refs[hi+1:], refs[hi:])
	refs[hi] = ref
	return refs
}

func remRefs(refs, rem id.Slice) id.Slice {
	if len(refs) == 0 || len(rem) == 0 {
		return refs
	}
	result := make(id.Slice, 0, len(refs))
	rpos, dpos := 0, 0
	for rpos < len(refs) && dpos < len(rem) {
		rr, dr := refs[rpos], rem[dpos]
		if rr < dr {
			result = append(result, rr)
			rpos++
			continue
		}
		if dr < rr {
			dpos++
			continue
		}
		rpos++
		dpos++
	}
	if rpos < len(refs) {
		result = append(result, refs[rpos:]...)
	}
	return result
}

func remRef(refs id.Slice, ref id.Zid) id.Slice {
	hi := len(refs)
	for lo := 0; lo < hi; {
		m := lo + (hi-lo)/2
		if r := refs[m]; r == ref {
			copy(refs[m:], refs[m+1:])
			refs = refs[:len(refs)-1]
			return refs
		} else if r < ref {
			lo = m + 1
		} else {
			hi = m
		}
	}
	return refs
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<








































































































































































































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

import (
	"testing"

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

func assertRefs(t *testing.T, i int, got, exp id.Slice) {
	t.Helper()
	if got == nil && exp != nil {
		t.Errorf("%d: got nil, but expected %v", i, exp)
		return
	}
	if got != nil && exp == nil {
		t.Errorf("%d: expected nil, but got %v", i, got)
		return
	}
	if len(got) != len(exp) {
		t.Errorf("%d: expected len(%v)==%d, but got len(%v)==%d", i, exp, len(exp), got, len(got))
		return
	}
	for p, n := range exp {
		if got := got[p]; got != id.Zid(n) {
			t.Errorf("%d: pos %d: expected %d, but got %d", i, p, n, got)
		}
	}
}

func TestRefsDiff(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in1, in2   id.Slice
		exp1, exp2 id.Slice
	}{
		{nil, nil, nil, nil},
		{id.Slice{1}, nil, id.Slice{1}, nil},
		{nil, id.Slice{1}, nil, id.Slice{1}},
		{id.Slice{1}, id.Slice{1}, nil, nil},
		{id.Slice{1, 2}, id.Slice{1}, id.Slice{2}, nil},
		{id.Slice{1, 2}, id.Slice{1, 3}, id.Slice{2}, id.Slice{3}},
		{id.Slice{1, 4}, id.Slice{1, 3}, id.Slice{4}, id.Slice{3}},
	}
	for i, tc := range testcases {
		got1, got2 := refsDiff(tc.in1, tc.in2)
		assertRefs(t, i, got1, tc.exp1)
		assertRefs(t, i, got2, tc.exp2)
	}
}

func TestAddRef(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		ref id.Slice
		zid uint
		exp id.Slice
	}{
		{nil, 5, id.Slice{5}},
		{id.Slice{1}, 5, id.Slice{1, 5}},
		{id.Slice{10}, 5, id.Slice{5, 10}},
		{id.Slice{5}, 5, id.Slice{5}},
		{id.Slice{1, 10}, 5, id.Slice{1, 5, 10}},
		{id.Slice{1, 5, 10}, 5, id.Slice{1, 5, 10}},
	}
	for i, tc := range testcases {
		got := addRef(tc.ref, id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRefs(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in1, in2 id.Slice
		exp      id.Slice
	}{
		{nil, nil, nil},
		{nil, id.Slice{}, nil},
		{id.Slice{}, nil, id.Slice{}},
		{id.Slice{}, id.Slice{}, id.Slice{}},
		{id.Slice{1}, id.Slice{5}, id.Slice{1}},
		{id.Slice{10}, id.Slice{5}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{5}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{5}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{5}, id.Slice{1, 10}},
		{id.Slice{1}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 2, 5}, id.Slice{2, 5}, id.Slice{1}},
		{id.Slice{2, 5, 10}, id.Slice{2, 5}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{2, 5}, id.Slice{1, 10}},
		{id.Slice{1}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 5}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{5, 10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 5, 9}, id.Slice{5, 9}, id.Slice{1}},
		{id.Slice{5, 9, 10}, id.Slice{5, 9}, id.Slice{10}},
		{id.Slice{1, 10}, id.Slice{5, 9}, id.Slice{1, 10}},
	}
	for i, tc := range testcases {
		got := remRefs(tc.in1, tc.in2)
		assertRefs(t, i, got, tc.exp)
	}
}

func TestRemRef(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		ref id.Slice
		zid uint
		exp id.Slice
	}{
		{nil, 5, nil},
		{id.Slice{}, 5, id.Slice{}},
		{id.Slice{5}, 5, id.Slice{}},
		{id.Slice{1}, 5, id.Slice{1}},
		{id.Slice{10}, 5, id.Slice{10}},
		{id.Slice{1, 5}, 5, id.Slice{1}},
		{id.Slice{5, 10}, 5, id.Slice{10}},
		{id.Slice{1, 5, 10}, 5, id.Slice{1, 10}},
	}
	for i, tc := range testcases {
		got := remRef(tc.ref, id.Zid(tc.zid))
		assertRefs(t, i, got, tc.exp)
	}
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<


















































































































































































































































































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

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

36
37
38
39
40
41
42



43
44
45
46
47
48
49
50
51
52



53
54
55
56
57
58
59
}

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

	// DeleteZettel removes index data for given zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	DeleteZettel(context.Context, id.Zid) id.Set




	// ReadStats populates st with store statistics.
	ReadStats(st *Stats)

	// Dump the content to a Writer.
	Dump(io.Writer)
}







>
>
>





|



|
>
>
>







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
}

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

	// DeleteZettel removes index data for given zettel.
	// Returns set of zettel identifier that must also be checked for changes.
	DeleteZettel(context.Context, id.Zid) *id.Set

	// Optimize removes unneeded space.
	Optimize()

	// ReadStats populates st with store statistics.
	ReadStats(st *Stats)

	// Dump the content to a Writer.
	Dump(io.Writer)
}

Changes to box/manager/store/wordset.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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

// WordSet contains the set of all words, with the count of their occurrences.
type WordSet map[string]int


|






>
>
>







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

package store

// WordSet contains the set of all words, with the count of their occurrences.
type WordSet map[string]int

Changes to box/manager/store/wordset_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
31
32
33
34
//-----------------------------------------------------------------------------
// 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_test

import (
	"sort"
	"testing"

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

func equalWordList(exp, got []string) bool {
	if len(exp) != len(got) {
		return false
	}
	if len(got) == 0 {
		return len(exp) == 0
	}
	sort.Strings(got)
	for i, w := range exp {
		if w != got[i] {
			return false
		}
	}
	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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//-----------------------------------------------------------------------------
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2021-present Detlef Stern
//-----------------------------------------------------------------------------

package store_test

import (
	"slices"
	"testing"

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

func equalWordList(exp, got []string) bool {
	if len(exp) != len(got) {
		return false
	}
	if len(got) == 0 {
		return len(exp) == 0
	}
	slices.Sort(got)
	for i, w := range exp {
		if w != got[i] {
			return false
		}
	}
	return true
}

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

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

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


|






>
>
>




>
|
>
>



|
>
|
|
|
|
|



|

|
>
|
|
|





|
<
|
<
|

|
|
|


|




|









|
<
|
>
>


|
<
|
<
|
|
|


|
|
|







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

package 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.Set { return zi.deadrefs }


// 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.Set { return zi.backrefs }



// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetInverseRefs() map[string]*id.Set {
	if len(zi.inverseRefs) == 0 {
		return nil
	}
	result := make(map[string]*id.Set, len(zi.inverseRefs))
	for key, refs := range zi.inverseRefs {
		result[key] = refs
	}
	return result
}

// GetWords returns a reference to the set of words. It must not be modified.
func (zi *ZettelIndex) GetWords() WordSet { return zi.words }

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-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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
33
34
35
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// 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(zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Reason: box.OnZettel, 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(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 {







|



|
|
|






>
>
>
>
>
>
>
>
>



|


















|




















>
|




|

|


|

|

|


|

|

<
|
<
<
<







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
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, reason box.UpdateReason) {
	if notify := mb.cdata.Notify; notify != nil {
		notify(mb, zid, reason)
	}
}

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, box.OnZettel)
	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(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(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.ErrNotFound
	}
	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")
}







|














|


|
















|




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












|




|











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
			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, box.OnZettel)
	mb.log.Trace().Msg("UpdateZettel")
	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, box.OnDelete)
	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
98
99
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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/kernel"
	"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()
	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")

|






>
>
>









<



<





>




|








|

>

|
|
|
|
|




>



|

|







|

>


|
|


>
>
>
>
>
>
>
>




|













|







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

package notify

import (
	"errors"
	"fmt"
	"path/filepath"
	"regexp"

	"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

// Constants for DirServiceState
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    box.UpdateNotifier
	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, notify box.UpdateNotifier) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    notify,
		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")
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
	if ds.entries == nil {
		return ds.logMissingEntry("update")
	}
	ds.entries[entry.Zid] = &entry
	return nil
}

// RenameDirEntry replaces an existing directory entry with a new one.
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,
		// Duplicates must not be set, because duplicates will be deleted
	}
	delete(ds.entries, oldZid)
	ds.entries[newZid] = &newEntry
	return newEntry, nil
}

func renameFilename(name string, curID, newID id.Zid) string {
	if cur := curID.String(); strings.HasPrefix(name, cur) {
		name = newID.String() + name[len(cur):]
	}
	return name
}

// DeleteDirEntry removes a entry from the directory.
func (ds *DirService) DeleteDirEntry(zid id.Zid) error {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	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 r := recover(); r != nil {
			kernel.Main.LogRecover("DirectoryService", r)
			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)







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














|
|




















|






|
|







|


|



















|






|


|














|






|







|


|







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
	if ds.entries == nil {
		return ds.logMissingEntry("update")
	}
	ds.entries[entry.Zid] = &entry
	return nil
}































// DeleteDirEntry removes a entry from the directory.
func (ds *DirService) DeleteDirEntry(zid id.Zid) error {
	ds.mx.Lock()
	defer ds.mx.Unlock()
	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.Error().Err(ev.Err).Msg("Notifier confused")
		}
	case Make:
		newEntries = make(entrySet)
	case List:
		if ev.Name == "" {
			zids := getNewZids(newEntries)
			ds.mx.Lock()
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = DsWorking
			ds.mx.Unlock()
			ds.onCreateDirectory(zids, prevEntries)
			if fromMissing {
				ds.log.Info().Str("path", ds.dirPath).Msg("Zettel directory found")
			}
			return nil, true
		}
		if newEntries != nil {
			ds.onUpdateFileEvent(newEntries, ev.Name)
		}
	case Destroy:
		ds.onDestroyDirectory()
		ds.log.Error().Str("path", ds.dirPath).Msg("Zettel directory missing")
		return nil, true
	case Update:
		ds.mx.Lock()
		zid := ds.onUpdateFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnZettel)
		}
	case Delete:
		ds.mx.Lock()
		zid := ds.onDeleteFileEvent(ds.entries, ev.Name)
		ds.mx.Unlock()
		if zid != id.Invalid {
			ds.notifyChange(zid, box.OnDelete)
		}
	default:
		ds.log.Error().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, box.OnZettel)
		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, box.OnDelete)
	}
}

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, box.OnDelete)
	}
}

var validFileName = regexp.MustCompile(`^(\d{14})`)

func matchValidFileName(name string) []string {
	return validFileName.FindStringSubmatch(name)
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry := fetchdirEntry(entries, zid)
	dupName1, dupName2 := ds.updateEntry(entry, name)
	if dupName1 != "" {
		ds.log.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		if dupName2 != "" {
			ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)")
		}
		return id.Invalid
	}
	return zid
}

func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid {







|

|







342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
	zid := seekZid(name)
	if zid == id.Invalid {
		return id.Invalid
	}
	entry := fetchdirEntry(entries, zid)
	dupName1, dupName2 := ds.updateEntry(entry, name)
	if dupName1 != "" {
		ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		if dupName2 != "" {
			ds.log.Info().Str("name", dupName2).Msg("Duplicate content (is ignored)")
		}
		return id.Invalid
	}
	return zid
}

func (ds *DirService) onDeleteFileEvent(entries entrySet, name string) id.Zid {
588
589
590
591
592
593
594
595
596
597
598
599
600
	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{Reason: box.OnZettel, Zid: zid}
	}
}







|
|
|
|


571
572
573
574
575
576
577
578
579
580
581
582
583
	newLen := len(newExt)
	if oldLen != newLen {
		return newLen < oldLen
	}
	return newExt < oldExt
}

func (ds *DirService) notifyChange(zid id.Zid, reason box.UpdateReason) {
	if notify := ds.infos; notify != nil {
		ds.log.Trace().Zid(zid).Uint("reason", uint64(reason)).Msg("notifyChange")
		notify(ds.box, zid, reason)
	}
}

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/z/domain/id"
	"zettelstore.de/z/domain/meta"
	_ "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.


)

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
31
32
33
//-----------------------------------------------------------------------------
// 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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

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
	}{
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
}

func TestNewExtIsBetter(t *testing.T) {
	extVals := []string{
		// Main Formats
		meta.SyntaxZmk, meta.SyntaxDraw, meta.SyntaxMarkdown, meta.SyntaxMD,
		// Other supported text formats
		meta.SyntaxCSS, meta.SyntaxTxt, meta.SyntaxHTML,
		meta.SyntaxMustache, 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







|
|







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
}

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

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
28
29
30
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package notify

import (
	"path/filepath"

	"t73f.de/r/zsc/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
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

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

	syntax := m.GetDefault(api.KeySyntax, "")
	ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax)
	metaName := e.MetaName
	eimc := extIsMetaAndContent(ext)
	if eimc {
		if metaName != "" {
			ext = contentExtWithMeta(syntax, content)
		}
		e.ContentName = e.calcBaseName(metaName) + "." + ext
		e.ContentExt = ext
	} else {
		if len(content.AsBytes()) > 0 {
			e.ContentName = e.calcBaseName(metaName) + "." + ext
			e.ContentExt = ext
		}
		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
	}







|










|




















|







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

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

	syntax := m.GetDefault(api.KeySyntax, meta.DefaultSyntax)
	ext := calcContentExt(syntax, m.YamlSep, getZettelFileSyntax)
	metaName := e.MetaName
	eimc := extIsMetaAndContent(ext)
	if eimc {
		if metaName != "" {
			ext = contentExtWithMeta(syntax, content)
		}
		e.ContentName = e.calcBaseName(metaName) + "." + ext
		e.ContentExt = ext
	} else {
		if len(content.AsBytes()) > 0 {
			e.ContentName = e.calcBaseName(metaName) + "." + ext
			e.ContentExt = ext
		}
		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
	}

Changes to box/notify/fsdir.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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

import (
	"os"
	"path/filepath"

|






>
>
>







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

package notify

import (
	"os"
	"path/filepath"
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
			log.Error().
				Str("parentDir", absParentDir).Err(errParent).
				Str("path", absPath).Err(err).
				Msg("Unable to access Zettel directory and its parent directory")
			watcher.Close()
			return nil, err
		}
		log.Warn().
			Str("parentDir", absParentDir).Err(errParent).
			Msg("Parent of Zettel directory cannot be supervised")
		log.Warn().Str("path", absPath).
			Msg("Zettelstore might not detect a deletion or movement of the Zettel directory")
	} else if err != nil {
		// Not a problem, if container is not available. It might become available later.
		log.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available")
	}

	fsdn := &fsdirNotifier{
		log:     log,
		events:  make(chan Event),
		refresh: make(chan struct{}),
		done:    make(chan struct{}),







<
|

|



|







54
55
56
57
58
59
60

61
62
63
64
65
66
67
68
69
70
71
72
73
74
			log.Error().
				Str("parentDir", absParentDir).Err(errParent).
				Str("path", absPath).Err(err).
				Msg("Unable to access Zettel directory and its parent directory")
			watcher.Close()
			return nil, err
		}

		log.Info().Str("parentDir", absParentDir).Err(errParent).
			Msg("Parent of Zettel directory cannot be supervised")
		log.Info().Str("path", absPath).
			Msg("Zettelstore might not detect a deletion or movement of the Zettel directory")
	} else if err != nil {
		// Not a problem, if container is not available. It might become available later.
		log.Info().Err(err).Str("path", absPath).Msg("Zettel directory currently not available")
	}

	fsdn := &fsdirNotifier{
		log:     log,
		events:  make(chan Event),
		refresh: make(chan struct{}),
		done:    make(chan struct{}),
90
91
92
93
94
95
96

97
98
99
100
101
102
103
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:







>







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
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:
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
		}
		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
			}
		}







|







165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
		}
		return true
	}

	if ev.Has(fsnotify.Create) {
		err := fsdn.base.Add(fsdn.path)
		if err != nil {
			fsdn.log.Error().Err(err).Str("name", fsdn.path).Msg("Unable to add directory")
			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
			}
		}

Changes to box/notify/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
//-----------------------------------------------------------------------------
// 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

import (
	"archive/zip"
	"os"

	"zettelstore.de/z/logger"
)

// MakeMetaFilename builds the name of the file containing metadata.
func MakeMetaFilename(basename string) string {
	return basename //+ ".meta"
}

// EntryFetcher return a list of (file) names of an directory.
type EntryFetcher interface {
	Fetch() ([]string, error)
}

type dirPathFetcher struct {
	dirPath string

|






>
>
>











<
<
<
<
<







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

package notify

import (
	"archive/zip"
	"os"

	"zettelstore.de/z/logger"
)






// EntryFetcher return a list of (file) names of an directory.
type EntryFetcher interface {
	Fetch() ([]string, error)
}

type dirPathFetcher struct {
	dirPath string

Changes to box/notify/notify.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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 provides some notification services to be used by box services.
package notify

import "fmt"


|






>
>
>







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

// Package notify provides some notification services to be used by box services.
package notify

import "fmt"

Changes to box/notify/simpledir.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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"


|






>
>
>







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

package notify

import (
	"path/filepath"

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
51
52
53
//-----------------------------------------------------------------------------
// 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, meta.SyntaxZmk),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err

|






>
>
>











|
|
|
|
|
|
|












|

|

|


|







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

package cmd

import (
	"context"
	"flag"
	"fmt"
	"io"
	"os"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/encoder"
	"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.DefaultSyntax),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc), &encoder.CreateParameter{Lang: m.GetDefault(api.KeyLang, api.ValueLangEN)})
	if encdr == nil {
		fmt.Fprintf(os.Stderr, "Unknown format %q\n", enc)
		return 2, nil
	}
	_, err = encdr.WriteZettel(os.Stdout, z, parser.ParseMetadata)
	if err != nil {
		return 2, err

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-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"
	"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
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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"
	"fmt"
	"os"

	"golang.org/x/term"

	"t73f.de/r/zsc/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-2023 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed 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
34
35
36
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

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

	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, authManager, boxManager)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
	ucGetAllMeta := usecase.NewGetAllMeta(protectedBoxManager)
	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucListMeta := usecase.NewListMeta(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, ucGetZettel, ucGetMeta, ucListMeta)



	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)
	ucZettelContext := usecase.NewZettelContext(protectedBoxManager, rtConfig)
	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)
	ucRename := usecase.NewRenameZettel(logUc, protectedBoxManager)
	ucUnlinkedRefs := usecase.NewUnlinkedReferences(protectedBoxManager, rtConfig)
	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)

	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	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(
			ucGetMeta, &ucEvaluate))
		webSrv.AddZettelRoute('b', server.MethodPost, wui.MakePostRenameZettelHandler(&ucRename))
		webSrv.AddListRoute('c', server.MethodGet, wui.MakeGetZettelFromListHandler(ucListMeta, &ucEvaluate, ucListRoles, ucListSyntax))
		webSrv.AddListRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('c', server.MethodGet, wui.MakeGetCreateZettelHandler(
			ucGetZettel, &ucCreateZettel, ucListRoles, ucListSyntax))
		webSrv.AddZettelRoute('c', server.MethodPost, wui.MakePostCreateZettelHandler(&ucCreateZettel))
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(
			ucGetMeta, ucGetAllMeta, &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.AddZettelRoute('o', server.MethodGet, a.MakeGetOrderHandler(
		usecase.NewZettelOrder(protectedBoxManager, ucEvaluate)))
	webSrv.AddZettelRoute('u', server.MethodGet, a.MakeListUnlinkedMetaHandler(
		ucGetMeta, ucUnlinkedRefs, &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.MakeQueryHandler(ucListMeta))
	webSrv.AddZettelRoute('z', server.MethodGet, a.MakeGetZettelHandler(ucGetMeta, 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) }







>
|


|
<


|
|
>
>
>


<


<
<

>


















<
<
<
|




|
<





|
|



|
<
<




<
<
<
<


<
|
|




<










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
	protectedBoxManager, authPolicy := authManager.BoxWithPolicy(boxManager, rtConfig)
	kern := kernel.Main
	webLog := kern.GetLogger(kernel.WebService)

	var getUser getUserImpl
	logAuth := kern.GetLogger(kernel.AuthService)
	logUc := kern.GetLogger(kernel.CoreService).WithUser(&getUser)
	ucGetUser := usecase.NewGetUser(authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(protectedBoxManager)

	ucGetZettel := usecase.NewGetZettel(protectedBoxManager)
	ucParseZettel := usecase.NewParseZettel(rtConfig, ucGetZettel)
	ucQuery := usecase.NewQuery(protectedBoxManager)
	ucEvaluate := usecase.NewEvaluate(rtConfig, &ucGetZettel, &ucQuery)
	ucQuery.SetEvaluate(&ucEvaluate)
	ucTagZettel := usecase.NewTagZettel(protectedBoxManager, &ucQuery)
	ucRoleZettel := usecase.NewRoleZettel(protectedBoxManager, &ucQuery)
	ucListSyntax := usecase.NewListSyntax(protectedBoxManager)
	ucListRoles := usecase.NewListRoles(protectedBoxManager)

	ucDelete := usecase.NewDeleteZettel(logUc, protectedBoxManager)
	ucUpdate := usecase.NewUpdateZettel(logUc, protectedBoxManager)


	ucRefresh := usecase.NewRefresh(logUc, protectedBoxManager)
	ucReIndex := usecase.NewReIndex(logUc, protectedBoxManager)
	ucVersion := usecase.NewVersion(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))

	a := api.New(
		webLog.Clone().Str("adapter", "api").Child(),
		webSrv, authManager, authManager, rtConfig, authPolicy)
	wui := webui.New(
		webLog.Clone().Str("adapter", "wui").Child(),
		webSrv, authManager, rtConfig, authManager, boxManager, authPolicy, &ucEvaluate)

	webSrv.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.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, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	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, &ucTagZettel, &ucRoleZettel, &ucReIndex))
	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))

	}

	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
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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package cmd

import (
	"flag"

	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/logger"
)

// Command stores information about commands / sub-commands.
type Command struct {
	Name       string              // command name as it appears on the command line
	Func       CommandFunc         // function that executes a command

Changes to cmd/main.go.

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-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"
	"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
45
46
47
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

package cmd

import (
	"crypto/sha256"
	"flag"
	"fmt"
	"net"
	"net/url"
	"os"
	"runtime/debug"
	"strconv"
	"strings"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"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/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",
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
	})
	RegisterCommand(Command{
		Name: "password",
		Func: cmdPassword,
	})
}

func fetchStartupConfiguration(fs *flag.FlagSet) (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":
			cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
		case "a":
			cfg.Set(keyAdminPort, flg.Value.String())
		case "d":







|



|


|
|











|
|

|


|


|
|







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
	})
	RegisterCommand(Command{
		Name: "password",
		Func: cmdPassword,
	})
}

func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) {
	if configFlag := fs.Lookup("c"); configFlag != nil {
		if filename := configFlag.Value.String(); filename != "" {
			content, err := readConfiguration(filename)
			return filename, createConfiguration(content, err)
		}
	}
	filename, content, err := searchAndReadConfiguration()
	return filename, createConfiguration(content, err)
}

func createConfiguration(content []byte, err error) *meta.Meta {
	if err != nil {
		return meta.New(id.Invalid)
	}
	return meta.NewFromInput(id.Invalid, input.NewInput(content))
}

func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) }

func searchAndReadConfiguration() (string, []byte, error) {
	for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} {
		if content, err := readConfiguration(filename); err == nil {
			return filename, content, nil
		}
	}
	return "", nil, os.ErrNotExist
}

func getConfig(fs *flag.FlagSet) (string, *meta.Meta) {
	filename, cfg := fetchStartupConfiguration(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
		case "a":
			cfg.Set(keyAdminPort, flg.Value.String())
		case "d":
140
141
142
143
144
145
146
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
			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 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 {







|














>









<

>







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
			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"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	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"

	keyReadOnly          = "read-only-mode"
	keyRuntimeProfiling  = "runtime-profiling"
	keyTokenLifetimeHTML = "token-lifetime-html"
	keyTokenLifetimeAPI  = "token-lifetime-api"
	keyURLPrefix         = "url-prefix"
	keyVerbose           = "verbose-mode"
)

func setServiceConfig(cfg *meta.Meta) bool {
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
		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
	}
	cfg := getConfig(fs)
	if !setServiceConfig(cfg) {
		fs.Usage()
		return 2
	}

	kern := kernel.Main
	var createManager kernel.CreateBoxManagerFunc







>
|

>
|















>










|
















|







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
		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, ""))
	err = setConfigValue(err, kernel.WebService, kernel.WebProfiling, debugMode || cfg.GetBool(keyRuntimeProfiling))
	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().Error().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
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)
	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
	}







|











|







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
			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
34
//-----------------------------------------------------------------------------
// 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/mdenc"     // Allow to use markdown encoder.

	_ "zettelstore.de/z/encoder/sexprenc"  // Allow to use sexpr encoder.
	_ "zettelstore.de/z/encoder/textenc"   // Allow to use text encoder.
	_ "zettelstore.de/z/encoder/zjsonenc"  // Allow to use ZJSON encoder.
	_ "zettelstore.de/z/encoder/zmkenc"    // Allow to use zmk encoder.
	_ "zettelstore.de/z/kernel/impl"       // Allow kernel implementation to create itself
	_ "zettelstore.de/z/parser/blob"       // Allow to use BLOB parser.
	_ "zettelstore.de/z/parser/draw"       // Allow to use draw parser.
	_ "zettelstore.de/z/parser/markdown"   // Allow to use markdown parser.
	_ "zettelstore.de/z/parser/none"       // Allow to use none parser.
	_ "zettelstore.de/z/parser/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
35
36
37
//-----------------------------------------------------------------------------
// Copyright (c) 2020-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// 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
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 main is the starting point for the zettelstore command.
package main

import (
	"os"

	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string = ""

func main() {
	exitCode := cmd.Main("Zettelstore", version)
	os.Exit(exitCode)
}

|






>
>
>












|





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.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2020-present Detlef Stern
//-----------------------------------------------------------------------------

// Package main is the starting point for the zettelstore command.
package main

import (
	"os"

	"zettelstore.de/z/cmd"
)

// Version variable. Will be filled by build process.
var version string

func main() {
	exitCode := cmd.Main("Zettelstore", version)
	os.Exit(exitCode)
}

Changes to collect/collect.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.



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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"


|






>
>
>







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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

Changes to collect/collect_test.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore is licensed under the latest version of the EUPL (European Union
// Public License). Please see file LICENSE.txt for your rights and obligations
// under this license.



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

// Package collect_test provides some unit test for collectors.
package collect_test

import (
	"testing"

|






>
>
>







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

// Package collect_test provides some unit test for collectors.
package collect_test

import (
	"testing"

Changes to collect/order.go.

1
2
3
4
5
6
7
8



9
10
11
12
13
14
15
//-----------------------------------------------------------------------------
// 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 collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"


|






>
>
>







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

// Package collect provides functions to collect items from a syntax tree.
package collect

import "zettelstore.de/z/ast"

Deleted collect/split.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 collect provides functions to collect items from a syntax tree.
package collect

import (
	"zettelstore.de/z/ast"
	"zettelstore.de/z/strfun"
)

// DivideReferences divides the given list of rederences into zettel, local, and external References.
func DivideReferences(all []*ast.Reference) (zettel, local, external []*ast.Reference) {
	if len(all) == 0 {
		return nil, nil, nil
	}

	mapZettel := make(strfun.Set)
	mapLocal := make(strfun.Set)
	mapExternal := make(strfun.Set)
	for _, ref := range all {
		if ref.State == ast.RefStateSelf {
			continue
		}
		if ref.IsZettel() {
			zettel = appendRefToList(zettel, mapZettel, ref)
		} else if ref.IsExternal() {
			external = appendRefToList(external, mapExternal, ref)
		} else {
			local = appendRefToList(local, mapLocal, ref)
		}
	}
	return zettel, local, external
}

func appendRefToList(reflist []*ast.Reference, refSet strfun.Set, ref *ast.Reference) []*ast.Reference {
	s := ref.String()
	if !refSet.Has(s) {
		reflist = append(reflist, ref)
		refSet.Set(s)
	}
	return reflist
}
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<




































































































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

// Key values that are supported by Config.Get
const (
	KeyFooterZettel = "footer-zettel"
	KeyHomeZettel   = "home-zettel"




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

|






>
>
>








|




|
|
>
>
>
>

<







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

// 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"
	KeyShowBackLinks      = "show-back-links"
	KeyShowFolgeLinks     = "show-folge-links"
	KeyShowSequelLinks    = "show-sequel-links"
	KeyShowSuccessorLinks = "show-successor-links"
	// 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,

Changes to docs/development/00010000000000.zettel.

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






|




>
1
2
3
4
5
6
7
8
9
10
11
id: 00010000000000
title: Developments Notes
role: zettel
syntax: zmk
created: 00010101000000
modified: 20231218182020

* [[Required Software|20210916193200]]
* [[Fuzzing tests|20221026184300]]
* [[Checklist for Release|20210916194900]]
* [[Development tools|20231218181900]]

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
20
21
22











id: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20230109121417

The following software must be installed:

* A current, supported [[release of Go|https://go.dev/doc/devel/release]],
* [[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``,
* [[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
```
















|




<
<
<




<




>
>
>
>
>
>
>
>
>
>
>
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: 20210916193200
title: Required Software
role: zettel
syntax: zmk
created: 20210916193200
modified: 20231213194509

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/devtools/devtools.go``.

Otherwise you can install the software by hand:

* [[shadow|https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow]] via ``go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest``,
* [[staticcheck|https://staticcheck.io/]] via ``go install honnef.co/go/tools/cmd/staticcheck@latest``,
* [[unparam|https://mvdan.cc/unparam]][^[[GitHub|https://github.com/mvdan/unparam]]] via ``go install mvdan.cc/unparam@latest``,
* [[revive|https://revive.run]] via ``go install github.com/mgechev/revive@vlatest``,
* [[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
13
14
15
16
17
18
19
20
21
22
23
24
25
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``
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
# All internal tests must succeed:
#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``).
# The API tests must succeed on every development platform:
#* ``go run tools/build.go testapi`` (alternatively: ``make api``).
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''.
#* Try to resolve other error messages and warnings
#* Warnings about empty content can be ignored




>
|








|

|

|







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: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20231213194631

# Sync with the official repository
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
#* ``ls ..`` must not have a file ''go.work'', in no parent folder.
# Make sure that all dependencies are up-to-date.
#* ``cat go.mod``
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# All internal tests must succeed:
#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``).
# The API tests must succeed on every development platform:
#* ``go run tools/testapi/testapi.go`` (alternatively: ``make api``).
# Run [[linkchecker|https://linkchecker.github.io/linkchecker/]] with the manual:
#* ``go run -race cmd/zettelstore/main.go run -d docs/manual``
#* ``linkchecker http://127.0.0.1:23123 2>&1 | tee lc.txt``
#* Check all ""Error: 404 Not Found""
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/p'' with encoding ''html'' for those zettel that are accessible only in ''expert-mode''.
#* Try to resolve other error messages and warnings
#* Warnings about empty content can be ignored
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# Disable Fossil autosync mode:
#* ``fossil setting autosync off``
# Commit the new release version:
#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"``
#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''.
   Otherwise client will not be able to import ''zettelkasten.de/z''.
# Clean up your Go workspace:
#* ``go run tools/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``







|

|



|






40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Disable Fossil autosync mode:
#* ``fossil setting autosync off``
# Commit the new release version:
#* ``fossil commit --tag release --tag vVERSION -m "Version VERSION"``
#* **Important:** the tag must follow the given pattern, e.g. ''v0.0.15''.
   Otherwise client will not be able to import ''zettelkasten.de/z''.
# Clean up your Go workspace:
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``).
# Create the release:
#* ``go run tools/build/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/20231218181900.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
116
id: 20231218181900
title: Development tools
role: zettel
syntax: zmk
created: 20231218181956
modified: 20231218184500

The source code contains some tools to assist the development of Zettelstore.
These are located in the ''tools'' directory.

Most tool support the generic option ``-v``, which log internal activities.

Some of the tools can be called easier by using ``make``, that reads in a provided ''Makefile''.

=== Check
The ""check"" tool automates some testing activities.
It is called via the command line:
```
# go run tools/check/check.go
```
There is an additional option ``-r`` to check in advance of a release.

The following checks are executed:
* Execution of unit tests, like ``go test ./...``
* Analyze the source code for general problems, as in ``go vet ./...``
* Tries to find shadowed variable, via ``shadow ./...``
* Performs some additional checks on the source code, via ``staticcheck ./...``
* Checks the usage of function parameters and usage of return values, via ``unparam ./...``.
  In case the option ''-r'' is set, the check includes exported functions and internal tests.
* In case option ''-r'' is set, the source code is checked against the vulnerability database, via ``govulncheck ./...``

Please note, that most of the tools above are not automatically installed in a standard Go distribution.
Use the command ""devtools"" to install them.

=== Devtools
The following command installs all needed tools:
```
# go run tooles/devtools/devtools.go
```
It will also automatically update these tools.

=== TestAPI
The following command will perform some high-level tests:
```sh
# go run tools/testapi/testapi.go
```
Basically, a Zettelstore will be started and then API calls will be made to simulate some typical activities with the Zettelstore.

If a Zettelstore is already running on port 23123, this Zettelstore will be used instead.
Even if the API test should clean up later, some zettel might stay created if a test fails.
This feature is used, if you want to have more control on the running Zettelstore.
You should start it with the following command:
```sh
# go run -race cmd/zettelstore/main.go run -c testdata/testbox/19700101000000.zettel
```
This allows you to debug failing API tests.

=== HTMLlint
The following command will check the generated HTML code for validity:
```sh
# go run tools/htmllint/htmllint.go
```
In addition, you might specify the URL od a running Zettelstore.
Otherwise ''http://localhost:23123'' is used.

This command fetches first the list of all zettel.
This list is used to check the generated HTML code (''ZID'' is the paceholder for the zettel identification):

* Check all zettel HTML encodings, via the path ''/z/ZID?enc=html&part=zettel''
* Check all zettel web views, via the path ''/h/ZID''
* The info page of all zettel is checked, via path ''/i/ZID''
* A subset of max. 100 zettel will be checked for the validity of their edit page, via ''/e/ZID''
* 10 random zettel are checked for a valid create form, via ''/c/ZID''
* A maximum of 200 random zettel are checked for a valid delete dialog, via ''/d/ZID''

Depending on the selected Zettelstore, the command might take a long time.

You can shorten the time, if you disable any zettel query in the footer.

=== Build
The ""build"" tool allows to build the software, either for tests or for a release.

The following command will create a Zettelstore executable for the architecture of the current computer:
```sh
# go tools/build/build.go build
```
You will find the executable in the ''bin'' directory.

A full release will be build in the directory ''releases'', containing ZIP files for the computer architectures ""Linux/amd64"", ""Linux/arm"", ""MacOS/arm64"", ""MacOS/amd64"", and ""Windows/amd64"".
In addition, the manual is also build as a ZIP file:
```sh
# go run tools/build/build.go release
```

If you just want the ZIP file with the manual, please use:
```sh
# go run tools/build/build.go manual
```

In case you want to check the version of the Zettelstore to be build, use:
```sh
# go run tools/build/build.go version
```

=== Clean
To remove the directories ''bin'' and ''releases'', as well as all cached Go libraries used by Zettelstore, execute:
```sh
# go run tools/clean/clean.go
```

Internally, the following commands are executed
```sh
# rm -rf bin releases
# go clean ./...
# go clean -cache -modcache -testcache
```

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
created: 00010101000000
default-copyright: (c) 2020-2022 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




|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00000000000100
title: Zettelstore Runtime Configuration
role: configuration
syntax: none
created: 20210126175322
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

Changes to docs/manual/00001000000000.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: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk

modified: 20220803183647


* [[Introduction|00001001000000]]
* [[Design goals|00001002000000]]
* [[Installation|00001003000000]]
* [[Configuration|00001004000000]]
* [[Structure of Zettelstore|00001005000000]]
* [[Layout of a zettel|00001006000000]]
* [[Zettelmarkup|00001007000000]]
* [[Other markup languages|00001008000000]]
* [[Security|00001010000000]]
* [[API|00001012000000]]
* [[Web user interface|00001014000000]]
* [[Tips and Tricks|00001017000000]]
* [[Troubleshooting|00001018000000]]
* Frequently asked questions



Licensed under the EUPL-1.2-or-later.





>
|
>
















>
>

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: 00001000000000
title: Zettelstore Manual
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141924
show-back-links: false

* [[Introduction|00001001000000]]
* [[Design goals|00001002000000]]
* [[Installation|00001003000000]]
* [[Configuration|00001004000000]]
* [[Structure of Zettelstore|00001005000000]]
* [[Layout of a zettel|00001006000000]]
* [[Zettelmarkup|00001007000000]]
* [[Other markup languages|00001008000000]]
* [[Security|00001010000000]]
* [[API|00001012000000]]
* [[Web user interface|00001014000000]]
* [[Tips and Tricks|00001017000000]]
* [[Troubleshooting|00001018000000]]
* Frequently asked questions

Version: {{00001000000001}}

Licensed under the EUPL-1.2-or-later.

Added docs/manual/00001000000001.zettel.

















>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
id: 00001000000001
title: Manual Version
role: configuration
syntax: zmk
created: 20231002142915
modified: 20231002142948

To be set by build tool.

Added docs/manual/00001000000002.zettel.















>
>
>
>
>
>
>
1
2
3
4
5
6
7
id: 00001000000002
title: manual
role: role
syntax: zmk
created: 20231128184200

Zettel with the role ""manual"" contain the manual of the zettelstore.

Changes to docs/manual/00001001000000.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: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk



[[Personal knowledge
management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] is
about collecting, classifying, storing, searching, retrieving, assessing,
evaluating, and sharing knowledge as a daily activity. Personal knowledge
management is done by most people, not necessarily as part of their main
business. It is essential for knowledge workers, like students, researchers,
lecturers, software developers, scientists, engineers, architects, to name
a few. Many hobbyists build up a significant amount of knowledge, even if the
do not need to think for a living. Personal knowledge management can be seen as
a prerequisite for many kinds of collaboration.

Zettelstore is a software that collects and relates your notes (""zettel"")
to represent and enhance your knowledge. It helps with many tasks of personal
knowledge management by explicitly supporting the ""[[Zettelkasten
method|https://en.wikipedia.org/wiki/Zettelkasten]]"". The method is based on
creating many individual notes, each with one idea or information, that are
related to each other. Since knowledge is typically build up gradually, one
major focus is a long-term store of these notes, hence the name
""Zettelstore"".





>
>

<
|
<
<
|
<
|
|
<
|

|
<
<
<
|
<
|
<
1
2
3
4
5
6
7
8

9


10

11
12

13
14
15



16

17

id: 00001001000000
title: Introduction to the Zettelstore
role: manual
tags: #introduction #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240710184612


[[Personal knowledge management|https://en.wikipedia.org/wiki/Personal_knowledge_management]] involves collecting, classifying, storing, searching, retrieving, assessing, evaluating, and sharing knowledge as a daily activity.


It's done by most individuals, not necessarily as part of their main business.

It's essential for knowledge workers, such as students, researchers, lecturers, software developers, scientists, engineers, architects, etc.
Many hobbyists build up a significant amount of knowledge, even if they do not need to think for a living.

Personal knowledge management can be seen as a prerequisite for many kinds of collaboration.

Zettelstore is software that collects and relates your notes (""zettel"") to represent and enhance your knowledge, supporting the ""[[Zettelkasten method|https://en.wikipedia.org/wiki/Zettelkasten]]"".



The method is based on creating many individual notes, each with one idea or piece of information, that is related to each other.

Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"".

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
id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221018105415

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should be not hard to write other software that works with your zettel.




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






|






>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id: 00001002000000
title: Design goals for the Zettelstore
role: manual
tags: #design #goal #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230624171152

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should be not hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel.
: If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel.
; Ease of installation
: If you want to use the Zettelstore software, all you need is to copy the executable to an appropriate file directory and start working.
31
32
33
34
35
36
37


; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what use are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.









>
>
35
36
37
38
39
40
41
42
43
; Simple service
: The purpose of Zettelstore is to safely store your zettel and to provide some initial relations between them.
: External software can be written to deeply analyze your zettel and the structures they form.
; Security by default
: Without any customization, Zettelstore provides its services in a safe and secure manner and does not expose you (or other users) to security risks.
: If you know what use are doing, Zettelstore allows you to relax some security-related preferences.
  However, even in this case, the more secure way is chosen.
: The Zettelstore software uses a minimal design and uses other software dependencies only is essential needed.
: There will be no plugin mechanism, which allows external software to control the inner workings of the Zettelstore software.

Changes to docs/manual/00001003000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220119145756

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.]





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220119145756

=== The curious user
You just want to check out the Zettelstore software

* Grab the appropriate executable and copy it into any directory
* Start the Zettelstore software, e.g. with a double click[^On Windows and macOS, the operating system tries to protect you from possible malicious software. If you encounter problem, please take a look on the [[Troubleshooting|00001018000000]] page.]

Changes to docs/manual/00001003300000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220114175754

You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003300000
title: Zettelstore installation for the intermediate user
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20220114175754

You already tried the Zettelstore software and now you want to use it permanently.
Zettelstore should start automatically when you log into your computer.

* Grab the appropriate executable and copy it into the appropriate directory
* If you want to place your zettel into another directory, or if you want more than one [[Zettelstore box|00001004011200]], or if you want to [[enable authentication|00001010040100]], or if you want to tweak your Zettelstore in some other way, create an appropriate [[startup configuration file|00001004010000]].

Changes to docs/manual/00001003305000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220218125541

Windows is a complicated beast. There are several ways to automatically start Zettelstore.

=== Startup folder

One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]].





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20220218125541

Windows is a complicated beast. There are several ways to automatically start Zettelstore.

=== Startup folder

One way is to use the [[autostart folder|https://support.microsoft.com/en-us/windows/add-an-app-to-run-automatically-at-startup-in-windows-10-150da165-dcd9-7230-517b-cf3c295d89dd]].

Changes to docs/manual/00001003310000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003310000
title: Enable Zettelstore to start automatically on macOS
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220119124635

There are several ways to automatically start Zettelstore.

* [[Login Items|#login-items]]
* [[Launch Agent|#launch-agent]]






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003310000
title: Enable Zettelstore to start automatically on macOS
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20220114181521
modified: 20220119124635

There are several ways to automatically start Zettelstore.

* [[Login Items|#login-items]]
* [[Launch Agent|#launch-agent]]

Changes to docs/manual/00001003315000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003315000
title: Enable Zettelstore to start automatically on Linux
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20220114181521
modified: 20220307104944

Since there is no such thing as the one Linux, there are too many different ways to automatically start Zettelstore.

* One way is to interpret your Linux desktop system as a server and use the [[recipe to install Zettelstore on a server|00001003600000]].
** See below for a lighter alternative.
* If you are using the [[Gnome Desktop|https://www.gnome.org/]], you could use the tool [[Tweak|https://wiki.gnome.org/action/show/Apps/Tweaks]] (formerly known as ""GNOME Tweak Tool"" or just ""Tweak Tool"").

Changes to docs/manual/00001003600000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001003600000
title: Installation of Zettelstore on a server
role: manual
tags: #installation #manual #zettelstore
syntax: zmk

modified: 20211125185833

You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Grab the appropriate executable and copy it into the appropriate directory:
```sh





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001003600000
title: Installation of Zettelstore on a server
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20211125185833

You want to provide a shared Zettelstore that can be used from your various devices.
Installing Zettelstore as a Linux service is not that hard.

Grab the appropriate executable and copy it into the appropriate directory:
```sh

Changes to docs/manual/00001004000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20210510153233

There are some levels to change the behavior and/or the appearance of Zettelstore.

# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface).
#* [[Command line parameters|00001004050000]]
# As an intermediate user, you usually want to have more control over how Zettelstore is started.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004000000
title: Configuration of Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210510153233

There are some levels to change the behavior and/or the appearance of Zettelstore.

# The first level is the way to start Zettelstore services and to manage it via command line (and, in part, via a graphical user interface).
#* [[Command line parameters|00001004050000]]
# As an intermediate user, you usually want to have more control over how Zettelstore is started.

Changes to docs/manual/00001004010000.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
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
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.

The file for startup configuration must be created via a text editor in advance.

The syntax of the configuration file is the same as for any zettel metadata.
The following keys are supported:

; [!admin-port|''admin-port'']
: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
  A value of ""0"" (the default) disables the administrator console.
  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].

  On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended).

  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a note to users.
  Examples would be presentation files, PDF files, music files or video files.

  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix, then the contents of the directory are listed to the user.
  To avoid this, create an empty file in the directory named ""index.html"".

  Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid.
; [!base-url|''base-url'']
: Sets the absolute base URL for the service.

  Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start.
  
  Default: ""http://127.0.0.1:23123/"".
; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one
: Specifies a [[box|00001004011200]] where zettel are stored.
  During startup __X__ is counted up, starting with one, until no key is found.
  This allows to configure more than one box.

  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"".
  In this case, even a key ''box-uri-2'' will be ignored.
; [!debug-mode|''debug-mode'']
: Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]]
  Disables any timeout values of the internal web server and does not send some security-related data.
  Sets [[''log-level''|#log-level]] to ""debug"".


  Do not enable it for a production server.

  Default: ""false""
; [!default-dir-box-type|''default-dir-box-type'']
: Specifies the default value for the (sub-) type of [[directory boxes|00001004011400#type]].
  Zettel are typically stored in such boxes.

  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). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if owner is set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes.
  Otherwise the Zettelstore will not start for security reasons.
; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these devices, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

  If ""true"", a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token (see option ''token-lifetime-html'') by 30 seconds.

  Default: ""false""
; [!read-only-mode|''read-only-mode'']
: Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]].
  No changes are possible.

  Default: ""false"".





; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be modified by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

  ''token-lifetime-html'' specifies the lifetime for the HTML views.
  It is automatically extended, when a new HTML view is rendered.
  Default: ""60"".
; [!url-prefix|''url-prefix'']
: Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations.
  Must begin and end with a slash character (""''/''"", U+002F).

  Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start.

  Default: ""/"".

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; [!verbose-mode|''verbose-mode'']
: Be more verbose when logging data, if set to a [[true value|00001006030500]].

  Default: ""false""






|

|
|
|

|








|








|
<

|

|











|
|




|


>





|
<



|
|














|







|



>
|

|

|






|

|
<



|



|



|



>
>
>
>
>

|


|








|



|










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
id: 00001004010000
title: Zettelstore startup configuration
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240926144803

The configuration file, specified by the ''-c CONFIGFILE'' [[command line option|00001004051000]], allows you to specify some startup options.
These cannot be stored in a [[configuration zettel|00001004020000]] because they are needed before Zettelstore can start or because of security reasons.
For example, Zettelstore needs to know in advance on which network address it 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.

The file for startup configuration must be created via a text editor in advance.

The syntax of the configuration file is the same as for any zettel metadata.
The following keys are supported:

; [!admin-port|''admin-port'']
: Specifies the TCP port through which you can reach the [[administrator console|00001004100000]].
  A value of ""0"" (the default) disables it.
  The administrator console will only be enabled if Zettelstore is started with the [[''run'' sub-command|00001004051000]].

  On most operating systems, the value must be greater than ""1024"" unless you start Zettelstore with the full privileges of a system administrator (which is not recommended).

  Default: ""0""
; [!asset-dir|''asset-dir'']
: Allows to specify a directory whose files are allowed be transferred directly with the help of the web server.
  The URL prefix for these files is ''/assets/''.
  You can use this if you want to transfer files that are too large for a zettel, such as presentation, PDF, music or video files.


  Files within the given directory will not be managed by Zettelstore.[^They will be managed by Zettelstore just in the very special case that the directory is one of the configured [[boxes|#box-uri-x]].]

  If you specify only the URL prefix in your web client, the contents of the directory are listed.
  To avoid this, create an empty file in the directory named ""index.html"".

  Default: """", no asset directory is set, the URL prefix ''/assets/'' is invalid.
; [!base-url|''base-url'']
: Sets the absolute base URL for the service.

  Note: [[''url-prefix''|#url-prefix]] must be the suffix of ''base-url'', otherwise the web service will not start.
  
  Default: ""http://127.0.0.1:23123/"".
; [!box-uri-x|''box-uri-X''], where __X__ is a number greater or equal to one
: Specifies a [[box|00001004011200]] where zettel are stored.
  During startup, __X__ is incremented, starting with one, until no key is found.
  This allows to configuring than one box.

  If no ''box-uri-1'' key is given, the overall effect will be the same as if only ''box-uri-1'' was specified with the value ""dir://.zettel"".
  In this case, even a key ''box-uri-2'' will be ignored.
; [!debug-mode|''debug-mode'']
: If set to [[true|00001006030500]], allows to debug the Zettelstore software (mostly used by Zettelstore developers).
  Disables any timeout values of the internal web server and does not send some security-related data.
  Sets [[''log-level''|#log-level]] to ""debug"".
  Enables [[''runtime-profiling''|#runtime-profiling]].

  Do not enable it for a production server.

  Default: ""false""
; [!default-dir-box-type|''default-dir-box-type'']
: Specifies the default value for the (sub-)type of [[directory boxes|00001004011400#type]], in which Zettel are typically stored.


  Default: ""notify""
; [!insecure-cookie|''insecure-cookie'']
: Must be set to [[true|00001006030500]] if authentication is enabled and Zettelstore is not accessible via HTTPS (but via HTTP).
  Otherwise web browsers 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.
  The 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 consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.

  Default: ""info"".

  Examples: ""error"" will produce just error messages (e.g. no ""info"" messages).
  ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar with operating the Zettelstore, you might set the level to ""error"" to receive fewer noisy messages from it.
; [!max-request-size|''max-request-size'']
: It 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). 
; [!owner|''owner'']
: [[Identifier|00001006050000]] of a zettel that contains data about the owner of the Zettelstore.
  The owner has full authorization for the Zettelstore.
  Only if set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that the key [[''secret''|#secret]] is set to a value of at least 16 bytes, otherwise the Zettelstore will not start for security reasons.

; [!persistent-cookie|''persistent-cookie'']
: A [[boolean value|00001006030500]] to make the access cookie persistent.
  This is helpful if you access the Zettelstore via a mobile device.
  On these, the operating system is free to stop the web browser and to remove temporary cookies.
  Therefore, an authenticated user will be logged off.

  If ""true"", a persistent cookie is used.
  Its lifetime exceeds the lifetime of the authentication token by 30 seconds (see option ''token-lifetime-html'').

  Default: ""false""
; [!read-only-mode|''read-only-mode'']
: If set to a [[true value|00001006030500]] the Zettelstore service puts into a read-only mode.
  No changes are possible.

  Default: ""false"".
; [!runtime-profiling|''runtime-profiling'']
: A boolean value that enables a web interface to obtain [[runtime profiling information|00001004010200]].

  Default: ""false"", but it is set to ""true"" if [[''debug-mode''|#debug-mode]] is enabled.
  In this case, it cannot be disabled.
; [!secret|''secret'']
: A string value to make the communication with external clients strong enough so that sessions of the [[web user interface|00001014000000]] or [[API access token|00001010040700]] cannot be altered by some external unfriendly party.
  The string must have a length of at least 16 bytes.

  This value is only needed to be set if [[authentication is enabled|00001010040100]] by setting the key [[''owner''|#owner]] to some user identification value.
; [!token-lifetime-api|''token-lifetime-api''], [!token-lifetime-html|''token-lifetime-html'']
: Define lifetime of access tokens in minutes.
  Values are only valid if authentication is enabled, i.e. key ''owner'' is set.

  ''token-lifetime-api'' is for accessing Zettelstore via its [[API|00001012000000]].
  Default: ""10"".

  ''token-lifetime-html'' specifies the lifetime for the HTML views.
  It is automatically extended when a new HTML view is rendered.
  Default: ""60"".
; [!url-prefix|''url-prefix'']
: Add the given string as a prefix to the local part of a Zettelstore local URL/URI when rendering zettel representations.
  It must begin and end with a slash character (""''/''"", U+002F).

  Note: ''url-prefix'' must be the suffix of [[''base-url''|#base-url]], otherwise the web service will not start.

  Default: ""/"".

  This allows to use a forwarding proxy [[server|00001010090100]] in front of the Zettelstore.
; [!verbose-mode|''verbose-mode'']
: Be more verbose when logging data, if set to a [[true value|00001006030500]].

  Default: ""false""

Added docs/manual/00001004010200.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
id: 00001004010200
title: Zettelstore runtime profiling
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20240926144556
modified: 20240926144951

For debugging purposes, you can enable runtime profiling by setting the startup configuration [[''runtime-profiling''|00001004010000#runtime-profiling]].
Typically, a Zettelstore developer will do this.
In certain cases, a Zettelstore developer will ask you to enable runtime profiling, because you encountered a hard error.

Runtime profiling will generate some data that can be retrieved through the builtin web server.
The following URL paths are valid:

|=Path|Description
|''/rtp/''|Show an index page, where you can navigate to detailed information
|''/rtp/allocs''|Show a sampling of all past memory allocations
|''/rtp/block''|Show stack traces that led to internal blocking
|''/rtp/cmdline''|Show the running Zettelstore command line, with arguments separated by NUL bytes
|''/rtp/goroutine''|Show stack traces of all current internal activities
|''/rtp/heap''|Show a sampling of memory allocations of live objects
|''/rtp/mutex''|Show stack traces of holders of contended mutexes
|''/rtp/profile''|Execute a CPU profile
|''/rtp/symbol''|Shows function names for given program counter value
|''/rtp/trace''|Show trace of execution of the current program
|''/rtp/threadcreate''|Show stack traces that led to the creation of new OS threads

See documentation for Go standard package [[''net/http/pprof''|https://pkg.go.dev/net/http/pprof]].

Changes to docs/manual/00001004011200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004011200
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220307121547

A Zettelstore must store its zettel somehow and somewhere.
In most cases you want to store your zettel as files in a directory.
Under certain circumstances you may want to store your zettel elsewhere.

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004011200
title: Zettelstore boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220307121547

A Zettelstore must store its zettel somehow and somewhere.
In most cases you want to store your zettel as files in a directory.
Under certain circumstances you may want to store your zettel elsewhere.

An example are the [[predefined zettel|00001005090000]] that come with a Zettelstore.

Changes to docs/manual/00001004011400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220724200512

Under certain circumstances, it is preferable to further configure a file directory box.
This is done by appending query parameters after the base box URI ''dir:\//DIR''.

The following parameters are supported:

|= Parameter:|Description|Default value:|





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001004011400
title: Configure file directory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240710180215

Under certain circumstances, it is preferable to further configure a file directory box.
This is done by appending query parameters after the base box URI ''dir:\//DIR''.

The following parameters are supported:

|= Parameter:|Description|Default value:|
52
53
54
55
56
57
58
59

=== Readonly
Sometimes you may want to provide zettel from a file directory box, but you want to disallow any changes.
If you provide the query parameter ''readonly'' (with or without a corresponding value), the box will disallow any changes.
```
box-uri-1: dir:///home/zettel?readonly
```
If you put the whole Zettelstore in [[read-only|00001004010000]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured.







|
53
54
55
56
57
58
59
60

=== Readonly
Sometimes you may want to provide zettel from a file directory box, but you want to disallow any changes.
If you provide the query parameter ''readonly'' (with or without a corresponding value), the box will disallow any changes.
```
box-uri-1: dir:///home/zettel?readonly
```
If you put the whole Zettelstore in [[read-only|00001004010000#read-only-mode]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured.

Changes to docs/manual/00001004011600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004011600
title: Configure memory boxes
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20220307112918
modified: 20220307122554

Under most circumstances, it is preferable to further configure a memory box.
This is done by appending query parameters after the base box URI ''mem:''.

The following parameters are supported:

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
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: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221219175720


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 JSON, 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.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].
; [!marker-external|''marker-external'']
: Some HTML code that is displayed after a [[reference to external material|00001007040310]].

  May be [[overwritten|00001004020200]] in a user zettel.

  Default: ""&\#10138;"", to display a ""&#10138;"" sign.
; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".














; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

  Default: ""Zettelstore"".
; [!yaml-header|''yaml-header'']
: If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n'').

  Default: ""False"".

  You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata.
; [!zettel-file-syntax|''zettel-file-syntax'']
: If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files:
  one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value).
  If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key.
  All values are case-insensitive, duplicate values are removed.

  For example, you could use this key if you're working with Markdown syntax and you want to store metadata and content in one ''.zettel'' file.

  If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file.






|
>















>



>




>



|




















<
<
<
<
<
<



>

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



>



>












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
id: 00001004020000
title: Configure the running Zettelstore
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175216
show-back-links: false

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.

  Use values according to the language definition of [[RFC-5646|https://tools.ietf.org/html/rfc5646]].






; [!max-transclusions|''max-transclusions'']
: Maximum number of indirect transclusion.
  This is used to avoid an exploding ""transclusion bomb"", a form of a [[billion laughs attack|https://en.wikipedia.org/wiki/Billion_laughs_attack]].

  Default: ""1024"".
; [!show-back-links|''show-back-links''], [!show-folge-links|''show-folge-links''], [!show-sequel-links|''show-sequel-links''], [!show-successor-links|''show-successor-links'']
: When displaying a zettel in the web user interface, references to other zettel are normally shown below the content of the zettel.
  This affects the metadata keys [[''back''|00001006020000#back]], [[''folge''|00001006020000#folge]], [[''sequel''|00001006020000#sequel]], and  [[''prequel''|00001006020000#prequel]].

  These configuration keys may be used to show, not to show, or to close the list of referenced zettel.

  Allowed values are: ""false"" (will not show the list), ""close"" (will show the list closed), and ""open"" / """" (will show the list).

  Default: """".

  May be [[overwritten|00001004020200]] in a user zettel, so that setting will only affect the given user.
  Alternatively, it may be overwritten in a zettel, so that that the setting will affect only the given zettel.

  This zettel is an example of a zettel that sets ''show-back-links'' to ""false"".
; [!site-name|''site-name'']
: Name of the Zettelstore instance.
  Will be used when displaying some lists.

  Default: ""Zettelstore"".
; [!yaml-header|''yaml-header'']
: If [[true|00001006030500]], metadata and content will be separated by ''---\\n'' instead of an empty line (''\\n\\n'').

  Default: ""False"".

  You will probably use this key, if you are working with another software processing [[Markdown|https://daringfireball.net/projects/markdown/]] that uses a subset of [[YAML|https://yaml.org/]] to specify metadata.
; [!zettel-file-syntax|''zettel-file-syntax'']
: If you create a new zettel with a syntax different to ""zmk"", Zettelstore will store the zettel as two files:
  one for the metadata (file without a filename extension) and another for the content (file extension based on the syntax value).
  If you want to specify alternative syntax values, for which you want new zettel to be stored in one file (file extension ''.zettel''), you can use this key.
  All values are case-insensitive, duplicate values are removed.

  For example, you could use this key if you're working with Markdown syntax and you want to store metadata and content in one ''.zettel'' file.

  If ''yaml-header'' evaluates to true, a zettel is always stored in one ''.zettel'' file.

Changes to docs/manual/00001004020200.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



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

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
|[[''marker-external''|00001004020000#marker-external]]|Y|Y|









|











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

Some metadata of the [[runtime configuration|00001004020000]] may be overwritten in an [[user zettel|00001010040200]].
A subset of those may be overwritten in zettel that is currently used.
This allows to specify user specific or zettel specific behavior.

The following metadata keys are supported to provide a more specific behavior:

|=Key|User:|Zettel:|Remarks
|[[''footer-zettel''|00001004020000#footer-zettel]]|Y|N|
|[[''home-zettel''|00001004020000#home-zettel]]|Y|N|
|[[''lang''|00001004020000#lang]]|Y|Y|Making it user-specific could make zettel for other user less useful
|[[''show-back-links''|00001004020000#show-back-links]]|Y|Y|
|[[''show-folge-links''|00001004020000#show-folge-links]]|Y|Y|
|[[''show-sequel-links''|00001004020000#show-sequel-links]]|Y|Y|
|[[''show-successor-links''|00001004020000#show-successor-links]]|Y|Y|

Changes to docs/manual/00001004050200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20210712233414

Lists all implemented sub-commands.

Example:
```
# zettelstore help





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050200
title: The ''help'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210712233414

Lists all implemented sub-commands.

Example:
```
# zettelstore help

Changes to docs/manual/00001004050400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20211124182041

Emits some information about the Zettelstore's version.
This allows you to check, whether your installed Zettelstore is 

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004050400
title: The ''version'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124182041

Emits some information about the Zettelstore's version.
This allows you to check, whether your installed Zettelstore is 

The name of the software (""Zettelstore"") and the build version information is given, as well as the compiler version, and an indication about the operating system and the processor architecture of that computer.

Changes to docs/manual/00001004051000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20220724162050

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051000
title: The ''run'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220724162050

=== ``zettelstore run``
This starts the web service.

```
zettelstore run [-a PORT] [-c CONFIGFILE] [-d DIR] [-debug] [-p PORT] [-r] [-v]

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
29
id: 00001004051200
title: The ''file'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109105434

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

  [[''sexpr''|00001012920516]],
  [[''text''|00001012920519]],
  [[''zjson''|00001012920503]] (deprecated in v0.11),
  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.







|












>
|

<







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

Changes to docs/manual/00001004051400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20210712234305

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004051400
title: The ''password'' sub-command
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210712234305

This sub-command is used to create a hashed password for to be authenticated users.

It reads a password from standard input (two times, both must be equal) and writes the hashed password to standard output.

The general usage is:

Changes to docs/manual/00001004059700.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: 00001004059700
title: List of supported logging levels
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220113183606

Zettelstore supports various levels of logging output.
This allows you to see the inner workings of Zettelstore, or to avoid it.

Each level has an associated name and number.
A lower number signals more logging output.

|= Name | Number >| Description
| Trace | 1 | Show most of the inner workings
| Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]].
| Sense | 3 | Display sensing events, which are not essential information.
| Info  | 4 | Display information about an event. In most cases, there is no required action expected from you.
| Warn  | 5 | Show a warning, i.e. an event that might become an error or more. Mostly invalid data.
| Error | 6 | Notify about an error, which was handled automatically. Something is broken. User intervention is not required, in most cases. Monitor the application.
| Fatal | 7 | Notify about a significant error that cannot be handled automatically. At least some important functionality is disabled.
| Panic | 8 | The application is in an uncertain state and notifies you about its panic. At least some part of the application is possibly restarted.
| Mandatory | 9 | Important message will be shown, e.g. the Zettelstore version at startup time.
| Disabled | 10 | No messages will be shown

If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown.
E.g. if you set the logging level to ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages.





>
|







|


<
|
<
|
<
<
|
|


|
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: 00001004059700
title: List of supported logging levels
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20211204182643
modified: 20240221134619

Zettelstore supports various levels of logging output.
This allows you to see the inner workings of Zettelstore, or to avoid it.

Each level has an associated name and number.
A lower number signals more logging output.

|= Name | Number :| Description
| Trace | 1 | Show most of the inner workings
| Debug | 2 | Show many internal values that might be interesting for a [[Zettelstore developer|00000000000005]].

| Info  | 3 | Display information about an event. In most cases, there is no required action expected from you.

| Error | 4 | Notify about an error, which was handled automatically. Something is broken. User intervention may be required, some important functionality may be disabled. Monitor the application.


| Mandatory | 5 | Important message will be shown, e.g. the Zettelstore version at startup time.
| Disabled | 6 | No messages will be shown

If you set the logging level to a certain value, only messages with the same or higher numerical value will be shown.
E.g. if you set the logging level to ""error"", no ""trace"", ""debug"", and ""info"" messages are shown, but ""error"" and ""mandatory"" messages.

Changes to docs/manual/00001004059900.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004059900
title: Command line flags for profiling the application
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk

modified: 20211122174951

If you want to measure potential bottlenecks within the software Zettelstore,
there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__):

; ''-cpuprofile FILE''
: Enables CPU profiling.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004059900
title: Command line flags for profiling the application
role: manual
tags: #command #configuration #manual #zettelstore
syntax: zmk
created: 20211122170506
modified: 20211122174951

If you want to measure potential bottlenecks within the software Zettelstore,
there are two [[command line|00001004050000]] flags for enabling the measurement (also called __profiling__):

; ''-cpuprofile FILE''
: Enables CPU profiling.

Changes to docs/manual/00001004100000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20211103162926

The administrator console is a service accessible only on the same computer on which Zettelstore is running.
It allows an experienced user to monitor and control some of the inner workings of Zettelstore.

You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]].






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004100000
title: Zettelstore Administrator Console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210510141304
modified: 20211103162926

The administrator console is a service accessible only on the same computer on which Zettelstore is running.
It allows an experienced user to monitor and control some of the inner workings of Zettelstore.

You enable the administrator console by specifying a TCP port number greater than zero (better: greater than 1024) for it, either via the [[command-line parameter ''-a''|00001004051000#a]] or via the ''admin-port'' key of the [[startup configuration file|00001004010000#admin-port]].

Changes to docs/manual/00001004101000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk

modified: 20220823194553

; [!bye|''bye'']
: Closes the connection to the administrator console.
; [!config|''config SERVICE'']
: Displays all valid configuration keys for the given service.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001004101000
title: List of supported commands of the administrator console
role: manual
tags: #configuration #manual #zettelstore
syntax: zmk
created: 20210510141304
modified: 20220823194553

; [!bye|''bye'']
: Closes the connection to the administrator console.
; [!config|''config SERVICE'']
: Displays all valid configuration keys for the given service.

Changes to docs/manual/00001005000000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20220104213511

Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141740

Zettelstore is a software that manages your zettel.
Since every zettel must be readable without any special tool, most zettel has to be stored as ordinary files within specific directories.
Typically, file names and file content must comply to specific rules so that Zettelstore can manage them.
If you add, delete, or change zettel files with other tools, e.g. a text editor, Zettelstore will monitor these actions.

Zettelstore provides additional services to the user.
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the [[web user interface|00001014000000]] or via the [[API|00001012053200]], the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel.
You can create these special zettel identifiers either with the __rename__ function of Zettelstore or by manually renaming the underlying zettel files.

It is allowed that the file name contains other characters after the 14 digits.
These are ignored by Zettelstore.

Two filename extensions are used by Zettelstore:
# ''.zettel'' is a format that stores metadata and content together in one file,
# the empty file extension is used, when the content must be stored in its own file, e.g. image data;







|







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

Every file in this directory that should be monitored by Zettelstore must have a file name that begins with 14 digits (0-9), the [[zettel identifier|00001006050000]].
If you create a new zettel via the [[web user interface|00001014000000]] or via the [[API|00001012053200]], the zettel identifier will be the timestamp of the current date and time (format is ''YYYYMMDDhhmmss'').
This allows zettel to be sorted naturally by creation time.

Since the only restriction on zettel identifiers are the 14 digits, you are free to use other digit sequences.
The [[configuration zettel|00001004020000]] is one prominent example, as well as these manual zettel.
You can create these special zettel by manually renaming the underlying zettel files.

It is allowed that the file name contains other characters after the 14 digits.
These are ignored by Zettelstore.

Two filename extensions are used by Zettelstore:
# ''.zettel'' is a format that stores metadata and content together in one file,
# the empty file extension is used, when the content must be stored in its own file, e.g. image data;
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier.
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]







|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

To allow changing predefined zettel, both the file store and the internal zettel store are internally chained together.
If you change a zettel, it will be always stored as a file.
If a zettel is requested, Zettelstore will first try to read that zettel from a file.
If such a file was not found, the internal zettel store is searched secondly.

Therefore, the file store ""shadows"" the internal zettel store.
If you want to read the original zettel, you have to delete the zettel (which removes it from the file directory).
Now we have two places where zettel are stored: in the specific directory and within the Zettelstore software.

* [[List of predefined zettel|00001005090000]]

=== Boxes: alternative ways to store zettel
As described above, a zettel may be stored as a file inside a directory or inside the Zettelstore software itself.
Zettelstore allows other ways to store zettel by providing an abstraction called __box__.[^Formerly, zettel were stored physically in boxes, often made of wood.]

Changes to docs/manual/00001005090000.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: 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
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages


| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]

| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[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.






|

|









>
>





>






<

>
>
>


<

>
>
>
>
|
|

>
>
>



>


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
id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240711183318

The following table lists all predefined zettel with their purpose.[^Zettel identifier format will be migrated to a new format after version 0.19.]

|= 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
| [[00000000000004]] | Zettelstore License | Lists the license of Zettelstore
| [[00000000000005]] | Zettelstore Contributors | Lists all contributors of Zettelstore
| [[00000000000006]] | Zettelstore Dependencies | Lists all licensed content
| [[00000000000007]] | Zettelstore Log | Lists the last 8192 log messages
| [[00000000000008]] | Zettelstore Memory | Some statistics about main memory usage
| [[00000000000009]] | Zettelstore Sx Engine | Statistics about the [[Sx|https://t73f.de/r/sx]] engine, which interprets symbolic expressions
| [[00000000000020]] | Zettelstore Box Manager | Contains some statistics about zettel boxes and the the index process
| [[00000000000090]] | Zettelstore Supported Metadata Keys | Contains all supported metadata keys, their [[types|00001006030000]], and more
| [[00000000000092]] | Zettelstore Supported Parser | Lists all supported values for metadata [[syntax|00001006020000#syntax]] that are recognized by Zettelstore
| [[00000000000096]] | Zettelstore Startup Configuration | Contains the effective values of the [[startup configuration|00001004010000]]
| [[00000000000100]] | Zettelstore Runtime Configuration | Allows to [[configure Zettelstore at runtime|00001004020000]]
| [[00000000000102]] | Zettelstore Warnings | Warnings about potential problematic zettel identifier
| [[00000000010100]] | Zettelstore Base HTML Template | Contains the general layout of the HTML view
| [[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

| [[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
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000090000]] | New Menu | Contains items that should be in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100#zettel]]""
| [[00000000090002]] | New User | Template for a new [[user zettel|00001010040200]]
| [[00000000090003]] | New Tag | Template for a new [[tag zettel|00001006020100#tag]]
| [[00000000090004]] | New Role | Template for a new [[role zettel|00001006020100#role]]
| [[00009999999998]] | Zettelstore Application Directory | Maps application name to application specific zettel
| [[00010000000000]] | Home | Default home zettel, contains some welcome information

If a zettel is not linked, it is not accessible for the current user.
In most cases, you must at least enable [[''expert-mode''|00001004020000#expert-mode]].

**Important:** All identifier may change until a stable version of the software is released.

Changes to docs/manual/00001006000000.zettel.

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

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.
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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|00001003000000]]"".

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 additional query parameter.
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.







|


|











28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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/00001006010000.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: 00001006010000
title: Syntax of Metadata
role: manual
tags: #manual #syntax #zettelstore
syntax: zmk

modified: 20220218131923

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed.
It begins at the first position of a new line.


A key is separated from its value either by
* a colon character (""'':''""),
* a non-empty sequence of space characters,
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A Value is a sequence of printable characters.
If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters.
The rest of the following line will be interpreted as the next part of the value.
There can be more than one continuation line for a value.

A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line.
It will be ignored.






>
|






>






|







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: 00001006010000
title: Syntax of Metadata
role: manual
tags: #manual #syntax #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240219193158

The metadata of a zettel is a collection of key-value pairs.
The syntax roughly resembles the internal header of an email ([[RFC5322|https://tools.ietf.org/html/rfc5322]]).

The key is a sequence of alphanumeric characters, a hyphen-minus character (""''-''"", U+002D) is also allowed.
It begins at the first position of a new line.
Uppercase letters of a key are translated to their lowercase equivalence.

A key is separated from its value either by
* a colon character (""'':''""),
* a non-empty sequence of space characters,
* a sequence of space characters, followed by a colon, followed by a sequence of space characters.

A value is a sequence of printable characters.
If the value should be continued in the following line, that following line (""continuation line"") must begin with a non-empty sequence of space characters.
The rest of the following line will be interpreted as the next part of the value.
There can be more than one continuation line for a value.

A non-continuation line that contains a possibly empty sequence of characters, followed by the percent sign character (""''%''"") is treated as a comment line.
It will be ignored.

Changes to docs/manual/00001006020000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221004134841

Although you are free to define your own metadata, by using any key (according to the [[syntax|00001006010000]]), some keys have a special meaning that is enforced by Zettelstore.
See the [[computed list of supported metadata keys|00000000000090]] for details.

Most keys conform to a [[type|00001006030000]].

; [!author|''author'']






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006020000
title: Supported Metadata Keys
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241118175033

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'']
33
34
35
36
37
38
39






40
41
42
43
44
45
46







47
48


49
50
51
52
53
54
55
  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.






; [!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.







>
>
>
>
>
>







>
>
>
>
>
>
>


>
>







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
  This is a computed value.
  There is no need to set it via Zettelstore.
  
  If it is not stored within a zettel, it will be computed based on the value of the [[Zettel Identifier|00001006050000]]: if it contains a value >= 19700101000000, it will be coerced to da date/time; otherwise the version time of the running software will be used.

  Please note that the value von ''created'' will be different (in most cases) to the value of [[''id''|#id]] / the zettel identifier, because it is exact up to the second.
  When calculating a zettel identifier, Zettelstore tries to set the second value to zero, if possible.
; [!created-missing|''created-missing'']
: If set to ""true"", the value of [[''created''|#created]] was not stored within a zettel.
  To allow the migration of [[zettel identifier|00001006050000]] to a new scheme, you should update the value of ''created'' to a reasonable value.
  Otherwise you might lose that information in future releases.

  This key will be removed when the migration to a new zettel identifier format has been completed.
; [!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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
; [!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.



; [!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.


; [!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.



; [!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]].
  If a zettel is renamed or deleted, these files will be deleted.
; [!user-id|''user-id'']
: Provides some unique user identification for an [[user zettel|00001010040200]].
  It is used as a user name for authentication.

  It is only used for zettel with a ''role'' value of ""user"".
; [!user-role|''user-role'']
: Defines the basic privileges of an authenticated user, e.g. reading / changing zettel.







>
>
>











>
>
>








>
>






>
>
>










<
<







|







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
; [!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]].
; [!prequel|''prequel'']
: Specifies a zettel that is conceptually a prequel zettel.
  This is a zettel that occured somehow before the current zettel.
; [!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.
; [!sequel|''sequel'']
: Is a property that contains identifier of all zettel that reference this zettel through the [[''prequel''|#prequel]] 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.
; [!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]].
  If a zettel is deleted, these files will also be deleted.
; [!user-id|''user-id'']
: Provides some unique user identification for an [[user zettel|00001010040200]].
  It is used as a user name for authentication.

  It is only used for zettel with a ''role'' value of ""user"".
; [!user-role|''user-role'']
: Defines the basic privileges of an authenticated user, e.g. reading / changing zettel.

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
27
28
29
30
31
32
33
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.
; [!literature|''literature'']
: Contains some remarks about a book, a paper, a web page, etc.
  You should add a citation key for citing it.
; [!zettel|''zettel'']
: A real zettel that contains your own thoughts.

However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ...





>
|








|




>
>
>
>
>
>
>
>
>










|
|


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: 00001006020100
title: Supported Zettel Roles
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231129173620

The [[''role'' key|00001006020000#role]] defines what kind of zettel you are writing.
You are free to define your own roles.
It is allowed to set an empty value or to omit the role.

Some roles are defined for technical reasons:

; [!configuration|''configuration'']
: A zettel that contains some configuration data / information for the Zettelstore.
  Most prominent is [[00000000000100]], as described in [[00001004020000]].
; [!manual|''manual'']
: All zettel that document the inner workings of the Zettelstore software.
  This role is only used in this specific Zettelstore.
; [!role|''role'']
: A zettel with the role ""role"" and a title, which names a [[role|00001006020000#role]], is treated as a __role zettel__.
  Basically, role zettel describe the role, and form a hierarchiy of meta-roles.
; [!tag|''tag'']
: A zettel with the role ""tag"" and a title, which names a [[tag|00001006020000#tags]], is treated as a __tag zettel__.
  Basically, tag zettel describe the tag, and form a hierarchiy of meta-tags.
; [!zettel|''zettel'']
: A zettel that contains your own thoughts.
  The real reason to use this software.

If you adhere to the process outlined by Niklas Luhmann, a zettel could have one of the following three roles:

; [!note|''note'']
: A small note, to remember something.
  Notes are not real zettel, they just help to create a real zettel.
  Think of them as Post-it notes.
; [!literature|''literature'']
: Contains some remarks about a book, a paper, a web page, etc.
  You should add a citation key for citing it.
; ''zettel''
: (as described above)

However, you are free to define additional roles, e.g. ''material'' for literature that is web-based only, ''slide'' for presentation slides, ''paper'' for the text of a scientific paper, ''project'' to define a project, ...

Changes to docs/manual/00001006020400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk

modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001006020400
title: Supported values for metadata key ''read-only''
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124132040

A zettel can be marked as read-only, if it contains a metadata value for key
[[''read-only''|00001006020000#read-only]].
If user authentication is [[enabled|00001010040100]], it is possible to allow some users to change the zettel,
depending on their [[user role|00001010070300]].
Otherwise, the read-only mark is just a binary value.

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
34
35
36
37
38
39
40
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]]
* [[String|00001006033500]]
* [[TagSet|00001006034000]]
* [[Timestamp|00001006034500]]
* [[URL|00001006035000]]
* [[Word|00001006035500]]
* [[WordSet|00001006036000]]
* [[Zettelmarkup|00001006036500]]





>
|






>


|










|












<

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: 00001006030000
title: Supported Key Types
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210126175322
modified: 20240219161909

All [[supported metadata keys|00001006020000]] conform to a type.

User-defined metadata keys conform also to a type, based on the suffix of the key.

|=Suffix|Type
| ''-date'' | [[Timestamp|00001006034500]]
| ''-number'' | [[Number|00001006033000]]
| ''-role'' | [[Word|00001006035500]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-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]]
* [[String|00001006033500]]
* [[TagSet|00001006034000]]
* [[Timestamp|00001006034500]]
* [[URL|00001006035000]]
* [[Word|00001006035500]]

* [[Zettelmarkup|00001006036500]]

Changes to docs/manual/00001006030500.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001006030500
title: Boolean Value
role: manual
tags: #manual #reference #zettel #zettelstore
syntax: zmk

modified: 20220304114040

On some places, metadata values are interpreted as a truth value.

Every character sequence that begins with a ""0"", ""F"", ""N"", ""f"", or a ""n"" is interpreted as the boolean ""false"" value.
All values are interpreted as the boolean ""true"" value.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001006030500
title: Boolean Value
role: manual
tags: #manual #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20220304114040

On some places, metadata values are interpreted as a truth value.

Every character sequence that begins with a ""0"", ""F"", ""N"", ""f"", or a ""n"" is interpreted as the boolean ""false"" value.
All values are interpreted as the boolean ""true"" value.

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







|








|







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

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







|







|







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

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


id: 00001006034500
title: Timestamp Key Type
role: manual
tags: #manual #meta #reference #zettel #zettelstore
syntax: zmk
created: 20210212135017
modified: 20231030182858

Values of this type denote a point in time.

=== Allowed values
Must be a sequence of 4, 6, 8, 10, 12, or 14 digits (""0""--""9"") (similar to an [[Identifier|00001006032000]]), with the restriction that it must conform to the pattern ""YYYYMMDDhhmmss"".

* YYYY is the year,
* MM is the month,
* DD is the day,
* hh is the hour,
* mm is the minute,
* ss is the second.

If the sequence is less than 14 digits, they are expanded with the following rule:

* YYYY is expanded to YYYY0101000000
* YYYYMM is expanded to YYYYMM01000000
* YYYYMMDD is expanded to YYYYMMDD000000
* YYYYMMDDhh is expanded to YYYYMMDDhh0000
* YYYYMMDDhhmm is expanded to YYYYMMDDhhmm00

=== Query comparison
[[Search values|00001007706000]] with more than 14 characters are truncated to contain exactly 14 characters.
Then, they are treated as timestamp data, as describe above, if they contain 4, 6, 8, 10, or 12 digits.

Comparison is done through the string representation.
In case of the search operators ""less"", ""not less"", ""greater"", and ""not greater"", this is the same as a numerical comparison.

=== Sorting
Sorting is done by comparing the possibly expanded values.


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.

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








































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






|







|







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

Changes to docs/manual/00001006050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20210721123222

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001006050000
title: Zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20241128141443

Each zettel is given a unique identifier.
To some degree, the zettel identifier is part of the metadata.
Basically, the identifier is given by the [[Zettelstore|00001005000000]] software.

Every zettel identifier consists of 14 digits.
They resemble a timestamp: the first four digits could represent the year, the

Changes to docs/manual/00001006055000.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: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk

modified: 20220311111751

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming a zettel, you are able to provide any sequence of 14 digits.
If no other zettel has the same identifier, you are allowed to rename a zettel.

To make things easier, you normally should not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need a range of specific zettel identifier to work properly.
Identifier that begin with ''00009'' can be used for such purpose.
To request a reservation, please send an email to the maintainer of Zettelstore.
The request must include the following data:
; Title
: Title of you application
; Description
: A brief description what the application is used for and why you need to reserve some zettel identifier
; Number
: Specify the amount of zettel identifier you are planning to use.
  Minimum size is 100.
  If you need more than 10.000, your justification will contain more words.

=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 0000000000000 | This is an invalid zettel identifier
| 00000000000001 | 0000009999999 | [[Predefined zettel|00001005090000]]
| 00000100000000 | 0000019999999 | This [[Zettelstore manual|00001000000000]]
| 00000200000000 | 0000899999999 | Reserved for future use
| 00009000000000 | 0000999999999 | Reserved for applications

This list may change in the future.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow
| 00009000002000 | 00009000002999 | [[Zettel Blog|https://zettelstore.de/contrib]], an application to collect and transform zettel into a blog





>
|


|
<

|




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





|
|
|


|




<
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: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210721105704
modified: 20241128141327

[[Zettel identifier|00001006050000]] are typically created by examine the current date and time.
By renaming the name of the underlying zettel file, you are able to provide any sequence of 14 digits.


To make things easier, you must not use zettel identifier that begin with four zeroes (''0000'').

All zettel provided by an empty zettelstore begin with six zeroes[^Exception: the predefined home zettel ''00010000000000''. But you can [[configure|00001004020000#home-zettel]] another zettel with another identifier as the new home zettel.].
Zettel identifier of this manual have be chosen to begin with ''000010''.

However, some external applications may need at least one defined zettel identifier to work properly.




Zettel [[Zettelstore Application Directory|00009999999998]] (''00009999999998'') can be used to associate a name to a zettel identifier.

For example, if your application is named ""app"", you create a metadata key ''app-zid''.

Its value is the zettel identifier of the zettel that configures your application.



=== Reserved Zettel Identifier

|= From | To | Description
| 00000000000000 | 0000000000000 | This is an invalid zettel identifier
| 00000000000001 | 0000099999999 | [[Predefined zettel|00001005090000]]
| 00001000000000 | 0000109999999 | This [[Zettelstore manual|00001000000000]]
| 00001100000000 | 0000899999999 | Reserved, do not use
| 00009000000000 | 0000999999999 | Reserved for applications

Since the format of zettel identifier will change in the near future, no external application is allowed to use the range ''00000000000001'' &hellip; ''0000999999999''.

==== External Applications
|= From | To | Description
| 00009000001000 | 00009000001999 | [[Zettel Presenter|https://zettelstore.de/contrib]], an application to display zettel as a HTML-based slideshow

Changes to docs/manual/00001007010000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20211124175047

Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124175047

Any document can be thought as a sequence of paragraphs and other [[block-structured elements|00001007030000]] (""blocks""), such as [[headings|00001007030300]], [[lists|00001007030200]], quotations, and code blocks.
Some of these blocks can contain other blocks, for example lists may contain other lists or paragraphs.
Other blocks contain [[inline-structured elements|00001007040000]] (""inlines""), such as text, [[links|00001007040310]], emphasized text, and images.

With the exception of lists and tables, the markup for blocks always begins at the first position of a line with three or more identical characters.

Changes to docs/manual/00001007020000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007020000
title: Zettelmarkup: Basic Definitions
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130713

Every Zettelmarkup content consists of a sequence of Unicode code-points.
Unicode code-points are called in the following as **character**s.

Characters are encoded with UTF-8.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007020000
title: Zettelmarkup: Basic Definitions
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218130713

Every Zettelmarkup content consists of a sequence of Unicode code-points.
Unicode code-points are called in the following as **character**s.

Characters are encoded with UTF-8.

Changes to docs/manual/00001007030000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311181036

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220311181036

Every markup for blocks-structured elements (""blocks"") begins at the very first position of a line.

There are five kinds of blocks: lists, one-line blocks, line-range blocks, tables, and paragraphs.

=== Lists

Changes to docs/manual/00001007030100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131155

A description list is a sequence of terms to be described together with the descriptions of each term.
Every term can described in multiple ways.

A description term (short: __term__) is specified with one semicolon (""'';''"", U+003B) at the first position, followed by a space character and the described term, specified as a sequence of line elements.
If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030100
title: Zettelmarkup: Description Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131155

A description list is a sequence of terms to be described together with the descriptions of each term.
Every term can described in multiple ways.

A description term (short: __term__) is specified with one semicolon (""'';''"", U+003B) at the first position, followed by a space character and the described term, specified as a sequence of line elements.
If the following lines should also be part of the term, exactly two spaces must be given at the beginning of each following line.

Changes to docs/manual/00001007030200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133902

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sing (""''>''"", U+003E).
Let's call these three characters __list characters__.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218133902

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.

Ordered lists are specified with the number sign (""''#''"", U+0023), unordered lists use the asterisk (""''*''"", U+002A), and quotation lists are specified with the greater-than sing (""''>''"", U+003E).
Let's call these three characters __list characters__.

Changes to docs/manual/00001007030300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133755

To specify a (sub-) section of a zettel, you should use the headings syntax: at
the beginning of a new line type at least three equal signs (""''=''"", U+003D), plus at least one
space and enter the text of the heading as [[inline elements|00001007040000]].

```zmk





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030300
title: Zettelmarkup: Headings
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218133755

To specify a (sub-) section of a zettel, you should use the headings syntax: at
the beginning of a new line type at least three equal signs (""''=''"", U+003D), plus at least one
space and enter the text of the heading as [[inline elements|00001007040000]].

```zmk

Changes to docs/manual/00001007030500.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131500

Verbatim blocks are used to enter text that should not be interpreted.
They begin with at least three grave accent characters (""''`''"", U+0060) at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", U+02CB) is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030500
title: Zettelmarkup: Verbatim Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131500

Verbatim blocks are used to enter text that should not be interpreted.
They begin with at least three grave accent characters (""''`''"", U+0060) at the first position of a line.
Alternatively, a modifier letter grave accent (""''Ë‹''"", U+02CB) is also allowed[^On some devices, such as an iPhone / iPad, a grave accent character is harder to enter and is often confused with a modifier letter grave accent.].

You can add some [[attributes|00001007050000]] on the beginning line of a verbatim block, following the initiating characters.

Changes to docs/manual/00001007030600.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131806

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to someone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030600
title: Zettelmarkup: Quotation Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131806

A simple way to enter a quotation is to use the [[quotation list|00001007030200]].
A quotation list loosely follows the convention of quoting text within emails.
However, if you want to attribute the quotation to someone, a quotation block is more appropriately.

This kind of line-range block begins with at least three less-than characters (""''<''"", U+003C) at the first position of a line.

Changes to docs/manual/00001007030700.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030700
title: Zettelmarkup: Verse Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218132432

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030700
title: Zettelmarkup: Verse Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218132432

Sometimes, you want to enter text with significant space characters at the beginning of each line and with significant line endings.
Poetry is one typical example.
Of course, you could help yourself with hard space characters and hard line breaks, by entering a backslash character before a space character and at the end of each line.
Using a verse block might be easier.

Changes to docs/manual/00001007030800.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220323190829

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007030800
title: Zettelmarkup: Region Blocks
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220323190829

Region blocks does not directly have a visual representation.
They just group a range of lines.
You can use region blocks to enter [[attributes|00001007050000]] that apply only to this range of lines.
One example is to enter a multi-line warning that should be visible.

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

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131107

Tables are used to show some data in a two-dimensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of __table cells__.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131107

Tables are used to show some data in a two-dimensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
Therefore, a table can be seen as a sequence of table rows.
A table row is nothing as a sequence of __table cells__.
The length of a table is the number of table rows, the width of a table is the maximum length of its rows.

Changes to docs/manual/00001007031140.zettel.

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






|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007031140
title: Zettelmarkup: Query Transclusion
role: manual
tags: #manual #search #zettelmarkup #zettelstore
syntax: zmk
created: 20220809132350
modified: 20240219161800

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.
45
46
47
48
49
50
51



52
53
54



55
56
57
58
59
60
61
62
63
: 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}}}
:::







>
>
>
|


>
>
>









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
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'', ''REINDEX'' (aggregate)
: Will be ignored.
  These actions may have been copied from an existing [[API query call|00001012051400]] (or from a WebUI query), but are here superfluous (and possibly harmful).
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or of type [[TagSet|00001006034000]] (aggregates)
: 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.].

To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored.
In this case the list of selected zettel is returned.

Example:
```zmk
{{{query:tags:#search | tags}}}
```
This is a tag cloud of all tags that are used together with the tag #search:
:::example
{{{query:tags:#search | tags}}}
:::

Changes to docs/manual/00001007031300.zettel.

10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 [[Sexpr 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:







|







10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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 [[Sexpr 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:







|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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

1
2
3
4
5

6
7
8
9
10
11
12
13
id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218131003

Text formatting is the way to make your text visually different.
Every text formatting element begins with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.





>
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
id: 00001007040100
title: Zettelmarkup: Text Formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20231113191353

Text formatting is the way to make your text visually different.
Every text formatting element begins with two same characters.
It ends when these two same characters occur the second time.
It is possible that some [[attributes|00001007050000]] follow immediately, without any separating character.

Text formatting can be nested, up to a reasonable limit.
25
26
27
28
29
30
31



32
33
* The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]].
** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}.
** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}.



* The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi.







>
>
>


26
27
28
29
30
31
32
33
34
35
36
37
* The circumflex accent character (""''^''"", U+005E) allows to enter super-scripted text.
** Example: ``e=mc^^2^^`` is rendered in HTML as: ::e=mc^^2^^::{=example}.
* The comma character (""'',''"", U+002C) produces sub-scripted text.
** Example: ``H,,2,,O`` is rendered in HTML as: ::H,,2,,O::{=example}.
* The quotation mark character (""''"''"", U+0022) marks an inline quotation, according to the [[specified language|00001007050100]].
** Example: ``""To be or not""`` is rendered in HTML as: ::""To be or not""::{=example}.
** Example: ``""Sein oder nicht""{lang=de}`` is rendered in HTML as: ::""Sein oder nicht""{lang=de}::{=example}.
* The number sign (""''#''"", U+0023) marks the text visually, where the mark does not belong to the text itself.
  It is typically used to highlight some text that is important for you, but was not important for the original author.
** Example: ``abc ##def## ghi`` is rendered in HTML as: ::abc ##def## ghi::{=example}.
* The colon character (""'':''"", U+003A) mark some text that should belong together. It fills a similar role as [[region blocks|00001007030800]], but just for inline elements.
** Example: ``abc ::def::{=example} ghi`` is rendered in HTML as: abc ::def::{=example} ghi.

Changes to docs/manual/00001007040200.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220311185110

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.






>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040200
title: Zettelmarkup: Literal-like formatting
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220311185110

There are some reasons to mark text that should be rendered as uninterpreted:
* Mark text as literal, sometimes as part of a program.
* Mark text as input you give into a computer via a keyboard.
* Mark text as output from some computer, e.g. shown at the command line.

Changes to docs/manual/00001007040300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20210810172531

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* [[Links to other zettel or to (external) material|00001007040310]]
* [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"")





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040300
title: Zettelmarkup: Reference-like text
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210810172531

An important aspect of knowledge work is to interconnect your zettel as well as provide links to (external) material.

There are several kinds of references that are allowed in Zettelmarkup:
* [[Links to other zettel or to (external) material|00001007040310]]
* [[Embedded zettel or (external) material|00001007040320]] (""inline transclusion"")

Changes to docs/manual/00001007040324.zettel.

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.






|







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

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.
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
   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]].







|








34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
   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}}{alt=Emoji}`` is rendered as ::{{//z/00000000040001}}{alt=Emoji}::{=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/00001007040330.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040330
title: Zettelmarkup: Footnotes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218130100

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040330
title: Zettelmarkup: Footnotes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218130100

A footnote begins with a left square bracket, followed by a circumflex accent (""''^''"", U+005E), followed by some text, and ends with a right square bracket.

Example:

``Main text[^Footnote text.].`` is rendered in HTML as: ::Main text[^Footnote text.].::{=example}.

Changes to docs/manual/00001007040340.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040340
title: Zettelmarkup: Citation Key
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218133447

A citation key references some external material that is part of a bibliographical collection.

Currently, Zettelstore implements this only partially, it is ""work in progress"".

However, the syntax is: beginning with a left square bracket and followed by an at sign character (""''@''"", U+0040), a the citation key is given.

Changes to docs/manual/00001007040350.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220218133206

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021).
Now the optional mark name follows.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007040350
title: Zettelmarkup: Mark
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210810155955
modified: 20220218133206

A mark allows to name a point within a zettel.
This is useful if you want to reference some content in a zettel, either with a [[link|00001007040310]] or with an [[inline-mode transclusion|00001007040324]].

A mark begins with a left square bracket, followed by an exclamation mark character (""''!''"", U+0021).
Now the optional mark name follows.

Changes to docs/manual/00001007050000.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk

modified: 20220630194106

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.
To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]].

Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text.





>







1
2
3
4
5
6
7
8
9
10
11
12
13
id: 00001007050000
title: Zettelmarkup: Attributes
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220630194106

Attributes allows to modify the way how material is presented.
Alternatively, they provide additional information to markup elements.
To some degree, attributes are similar to [[HTML attributes|https://html.spec.whatwg.org/multipage/dom.html#global-attributes]].

Typical use cases for attributes are to specify the (natural) [[language|00001007050100]] for a text region, to specify the [[programming language|00001007050200]] for highlighting program code, or to make white space visible in plain text.

Changes to docs/manual/00001007050200.zettel.

1
2
3
4
5

6
7
id: 00001007050200
title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual


TBD





>


1
2
3
4
5
6
7
8
id: 00001007050200
title: Zettelmarkup: Supported Attribute Values for Programming Languages
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
role: manual
created: 20210126175322

TBD

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
41
42
43
44
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.

  If no random order is effective, a ``ORDER REVERSE id`` will be added.
  This makes the sort stable.

  Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
id: 00001007702000
title: Search term
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20230925173539

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 REVERSE published'' will return a reversed result order.

  An explicit order field will take precedence over the random order described below.

  If no random order is effective, a ``ORDER REVERSE id`` will be added.
  This makes the sort stable.

  Example: ``ORDER created`` will be interpreted as ``ORDER created ORDER REVERSE id``.
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.

Changes to docs/manual/00001007706000.zettel.

1
2
3
4
5

6
7
8
9
10
id: 00001007706000
title: Search value
role: manual
tags: #manual #search #zettelstore
syntax: zmk

modified: 20220807162031

A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]].

A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned.





>





1
2
3
4
5
6
7
8
9
10
11
id: 00001007706000
title: Search value
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20220807162031

A search value specifies a value to be searched for, depending on the [[search operator|00001007705000]].

A search value should be lower case, because all comparisons are done in a case-insensitive way and there are some upper case keywords planned.

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
38
39
40
41
id: 00001007720300
title: Query: Context Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707204706
modified: 20241118174741

A context directive calculates the __context__ of a list of zettel identifier.
It starts with the keyword ''CONTEXT''.

Optionally you may specify some context details, after the keyword ''CONTEXT'', separated by space characters.
These are:
* ''FULL'': additionally search for zettel with the same tags,
* ''BACKWARD'': search for context only though backward links,
* ''FORWARD'': search for context only through forward links,
* ''COST'': one or more space characters, and a positive integer: set the maximum __cost__ (default: 17),
* ''MAX'': one or more space characters, and a positive integer: set the maximum number of context zettel (default: 2