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-12-16
17:27
Refactor config handling in WebUI package ... (Leaf check-in: 87c8fd7048 user: stern tags: trunk)
10:03
Fix some typos in the manual and in predefined zettel ... (check-in: 96c60bbcc2 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
1
2



-
bin/*
releases/*
parser/pikchr/*.out

Changes to LICENSE.txt.

1

2
3
4
5
6
7
8

1
2
3
4
5
6
7
8
-
+







Copyright (c) 2020-2023 Detlef Stern
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

1

2
3
4
5
6
7
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
## Copyright (c) 2020-present Detlef Stern
##
## This file is part of Zettelstore.
##
## Zettelstore is licensed under the latest version of the EUPL (European Union
## Public License). Please see file LICENSE.txt for your rights and obligations
## under this license.

.PHONY:  check relcheck api build release clean
.PHONY:  check relcheck api version build release clean

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

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

api:
	go run tools/build.go testapi
	go run tools/testapi/testapi.go

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

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

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

clean:
	go run tools/build.go 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

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
“Zettelstore”.

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
[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](https://twitter.com/zettelstore)
<a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a>&nbsp;&hellip;
[Stay tuned](https://mastodon.social/tags/Zettelstore)&nbsp;&hellip;

Changes to VERSION.

1


1
-
+
0.10.1
0.20.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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"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 domain.Content // Original content
	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
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-2023 Detlef Stern
// 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 "zettelstore.de/c/attrs"
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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 (
	"unicode/utf8"

	"zettelstore.de/c/attrs"
	"t73f.de/r/zsc/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?
}

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









206
207
208
209
210
211
212
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.
	FormatSpan              // Generic inline container.
	FormatEmph              // Emphasized text
	FormatStrong            // Strongly emphasized text
	FormatInsert            // Inserted text
	FormatDelete            // Deleted text
	FormatSuper             // Superscripted text
	FormatSub               // SubscriptedText
	FormatQuote             // Quoted text
	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
1

2
3
4
5
6
7
8
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
// 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/domain/id"
	"zettelstore.de/z/zettel/id"
)

// QueryPrefix is the prefix that denotes a query expression.
const QueryPrefix = "query:"
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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

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

func BenchmarkWalk(b *testing.B) {
	root := ast.BlockSlice{
		&ast.HeadingNode{
			Inlines: ast.CreateInlineSliceFromWords("A", "Simple", "Heading"),
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "A Simple Heading"}},
		},
		&ast.ParaNode{
			Inlines: ast.CreateInlineSliceFromWords("This", "is", "the", "introduction."),
			Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is the introduction."}},
		},
		&ast.NestedListNode{
			Kind: ast.NestedListUnordered,
			Items: []ast.ItemSlice{
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.CreateInlineSliceFromWords("Item", "1"),
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 1"}},
					},
				},
				[]ast.ItemNode{
					&ast.ParaNode{
						Inlines: ast.CreateInlineSliceFromWords("Item", "2"),
						Inlines: ast.InlineSlice{&ast.TextNode{Text: "Item 2"}},
					},
				},
			},
		},
		&ast.ParaNode{
			Inlines: ast.CreateInlineSliceFromWords("This", "is", "some", "intermediate", "text."),
			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.CreateInlineSliceFromWords("This", "is", "some", "emphasized", "text."),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "This is some emphasized text."}},
			},
			&ast.SpaceNode{Lexeme: " "},
			&ast.TextNode{Text: " "},
			&ast.LinkNode{
				Ref:     &ast.Reference{Value: "http://zettelstore.de"},
				Inlines: ast.CreateInlineSliceFromWords("URL", "text."),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "URL text."}},
			},
		),
	}
	v := benchVisitor{}
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
	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
1

2
3
4
5
6
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
// 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/domain/id"
	"zettelstore.de/z/domain/meta"
	"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
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
	KindJSON
	KindHTML
	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
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
}

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
1

2
3
4
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
// 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/domain/id"
	"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
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.WriteString(zid.String())
	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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"github.com/pascaldekloe/jwt"

	"zettelstore.de/c/api"
	"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/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"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
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 }

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

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

// ErrOtherKind signals that the token was defined for another token kind.
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)),
	sClaim := sx.MakeList(
		sx.Int64(kind),
		sx.MakeString(subject),
		sx.Int64(now.Unix()),
		sx.Int64(now.Add(d).Unix()),
			Issued:  jwt.NewNumericTime(now),
		},
		Set: map[string]interface{}{
			"zid": ident.Zid.String(),
		sx.Int64(ident.Zid),
			"_tk": int(kind),
		},
	}
	token, err := claims.HMACSign(reqHash, a.secret)
	if err != nil {
		return nil, err
	)
	return sign(sClaim, a.secret)
	}
	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)
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 auth.TokenData{}, err
		return tokenData, err
	}
	claims, err := h.Check(token)

	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 auth.TokenData{}, err
		return ErrMalformedToken
	}
	now := time.Now().Round(time.Second)
	expires := claims.Expires.Time()
	if expires.Before(now) {
		return auth.TokenData{}, ErrTokenExpired
	if auth.TokenKind(vals[0].(sx.Int64)) != k {
		return ErrOtherKind
	}
	ident := claims.Subject
	ident := vals[1].(sx.String).GetValue()
	if ident == "" {
		return auth.TokenData{}, ErrNoIdent
		return ErrNoIdent
	}
	if zidS, ok := claims.Set["zid"].(string); ok {
		if zid, err2 := id.Parse(zidS); err2 == nil {
	issued := time.Unix(int64(vals[2].(sx.Int64)), 0)
	expires := time.Unix(int64(vals[3].(sx.Int64)), 0)
			if kind, ok2 := claims.Set["_tk"].(float64); ok2 {
				if auth.TokenKind(kind) == k {
					return auth.TokenData{
						Token:   token,
						Now:     now,
	now := time.Now().Round(time.Second)
						Issued:  claims.Issued.Time(),
						Expires: expires,
						Ident:   ident,
	if expires.Before(now) {
		return ErrTokenExpired
						Zid:     zid,
					}, nil
				}
	}
			}
			return auth.TokenData{}, ErrOtherKind
		}
	}
	return auth.TokenData{}, ErrNoZid
	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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38




39
40
41
42
43
44
45

-
+






+
+
+







-
+



















-
-
-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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/domain/meta"
	"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) 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

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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"zettelstore.de/z/web/server"
	"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(
func BoxWithPolicy(manager auth.AuthzManager, box box.Box, authConfig config.AuthConfig) (box.Box, auth.Policy) {
	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








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

func (pp *polBox) GetAllZettel(ctx context.Context, zid id.Zid) ([]domain.Zettel, error) {
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) GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	return pp.box.GetAllMeta(ctx, zid)
}

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

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

func (pp *polBox) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
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 domain.Zettel) error {
func (pp *polBox) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	zid := zettel.Meta.Zid
	user := server.GetUser(ctx)
	if !zid.IsValid() {
		return &box.ErrInvalidID{Zid: zid}
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	// Write existing zettel
	oldMeta, err := pp.box.GetMeta(ctx, zid)
	oldZettel, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
	}
	if pp.policy.CanWrite(user, oldMeta, zettel.Meta) {
	if pp.policy.CanWrite(user, oldZettel.Meta, zettel.Meta) {
		return pp.box.UpdateZettel(ctx, zettel)
	}
	return box.NewErrNotAllowed("Write", user, zid)
}

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)
	z, err := pp.box.GetZettel(ctx, zid)
	if err != nil {
		return err
	}
	user := server.GetUser(ctx)
	if pp.policy.CanDelete(user, meta) {
	if pp.policy.CanDelete(user, z.Meta) {
		return pp.box.DeleteZettel(ctx, zid)
	}
	return box.NewErrNotAllowed("Delete", user, zid)
}

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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
	"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) 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 {

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
1

2
3
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
// 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/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"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
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) 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
	}

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
1

2
3
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
// 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/domain/meta"
	"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
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) 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)
}

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
1

2
3
4
5
6
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"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
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)
			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 {
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
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 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) {

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

type roPolicy struct{}

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

Changes to box/box.go.

1
2

3
4
5
6
7
8



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





28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"
	"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"
	"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

	// 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)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

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

	// CanUpdateZettel returns true, if box could possibly update the given zettel.
	CanUpdateZettel(ctx context.Context, zettel 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
}

// 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 an already started box is not 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)
	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, q *query.Query) ([]*meta.Meta, error)
	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) ([]domain.Zettel, error)
	GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)

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

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

	// 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
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 a zettel happened
	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    Box
	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
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

// DoNotEnrich determines if the context is marked to not enrich metadata.
func DoNotEnrich(ctx context.Context) bool {
// 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
	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
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")

// ErrNotFound is returned if a zettel was not found in the box.
var ErrNotFound = errors.New("zettel not found")
// 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")

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

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

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

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

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

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register(
		" comp",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		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(*meta.Meta) []byte
	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.ZidBoxManager):           {genManagerM, genManagerC},
	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) box.ManagedBox {
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 (*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) {
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("GetMeta/Content")
				return domain.Zettel{
				cb.log.Trace().Msg("GetZettel/Content")
				return zettel.Zettel{
					Meta:    m,
					Content: domain.NewContent(genContent(m)),
					Content: zettel.NewContent(genContent(ctx, cb)),
				}, nil
			}
			cb.log.Trace().Msg("GetMeta/NoContent")
			return domain.Zettel{Meta: m}, nil
			cb.log.Trace().Msg("GetZettel/NoContent")
			return zettel.Zettel{Meta: m}, nil
		}
	}
	err := box.ErrZettelNotFound{Zid: zid}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel/Err")
	return domain.Zettel{}, box.ErrNotFound
	cb.log.Trace().Err(err).Msg("GetZettel/Err")
	return zettel.Zettel{}, err
}

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

	}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta/Err")
	return nil, box.ErrNotFound
}

func (cb *compBox) ApplyZid(_ context.Context, handle box.ZidFunc, constraint query.RetrievePredicate) error {
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyMeta")
	cb.log.Trace().Int("entries", int64(len(myZettel))).Msg("ApplyZid")
	for zid, gen := range myZettel {
		if !constraint(zid) {
			continue
		}
		if genMeta := gen.meta; genMeta != nil {
			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
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) 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 {
func (cb *compBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {
	err := box.ErrNotFound
	if _, ok := myZettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

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
1

2
3
4
5
6
7
8
9
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
// 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/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
	"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")
	return getTitledMeta(zid, "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 {
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"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 := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Supported Metadata Keys")
	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(*meta.Meta) []byte {
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|%v|%v|%v\n", kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
			"|[[%v|query:%v?]]|%v|%v|%v\n", kd.Name, kd.Name, kd.Type.Name, kd.IsComputed(), kd.IsProperty())
	}
	return buf.Bytes()
}

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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"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 := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Log")
	m := getTitledMeta(zid, "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))
	m.Set(api.KeyModified, kernel.Main.GetLastLogTime().Local().Format(id.TimestampLayout))
	return m
}

func genLogC(*meta.Meta) []byte {
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
1

2
3
4
5
6
7
8
9
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
// 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/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

func genManagerC(*meta.Meta) []byte {
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"
	"sort"
	"slices"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"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 := meta.New(zid)
	m.Set(api.KeyTitle, "Zettelstore Supported Parser")
	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(*meta.Meta) []byte {
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()
	sort.Strings(syntaxes)
	slices.Sort(syntaxes)
	for _, syntax := range syntaxes {
		info := parser.Get(syntax)
		if info.Name != syntax {
			continue
		}
		altNames := info.AltNames
		sort.Strings(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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"

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

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

func genVersionBuildM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Version")
	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(*meta.Meta) []byte {
func genVersionBuildC(context.Context, *compBox) []byte {
	return []byte(kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
}

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

func genVersionOSM(zid id.Zid) *meta.Meta {
	m := getVersionMeta(zid, "Zettelstore Operating System")
	return getTitledMeta(zid, "Zettelstore Operating System")
	m.Set(api.KeyCreated, kernel.Main.GetConfig(kernel.CoreService, kernel.CoreStarted).(string))
	return m
}
func genVersionOSC(*meta.Meta) []byte {
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
1
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-size: 1rem;
    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
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 }
  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 }
  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: .5rem 0 0 0 }
  dt { margin: .5em 0 0 0 }
  dt+dd { margin-top: 0 }
  dd { margin: .5rem 0 0 2rem }
  dd { margin: .5em 0 0 2em }
  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;
    border-left: .5em solid lightgray;
    padding-left: 1em;
    margin-left: 1em;
    margin-right: 2em;
    font-style: italic;
  }
  blockquote p { margin-bottom: .5rem }
  blockquote p { margin-bottom: .5em }
  blockquote cite { font-style: normal }
  table {
    border-collapse: collapse;
    border-spacing: 0;
    max-width: 100%;
  }
  td, th {text-align: left; padding: .25em .5em;}
  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 }
  th { font-weight: bold }
  thead th { border-bottom: 2px solid hsl(0, 0%, 70%) }
  td {
    text-align: left;
    padding: .25rem .5rem;
    border-bottom: 1px solid hsl(0, 0%, 85%)
  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
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: .5rem;
    padding-top: .5em;
    border-top: 1px solid;
  }
  kbd { font-family:monospace }
  code,pre {
    font-family: monospace;
    font-size: 85%;
  }
  code {
    padding: .1rem .2rem;
    padding: .1em .2em;
    background: #f0f0f0;
    border: 1px solid #ccc;
    border-radius: .25rem;
    border-radius: .25em;
  }
  pre {
    padding: .5rem .7rem;
    padding: .5em .7em;
    max-width: 100%;
    overflow: auto;
    border: 1px solid #ccc;
    border-radius: .5rem;
    border-radius: .5em;
    background: #f0f0f0;
  }
  pre code {
    font-size: 95%;
    position: relative;
    padding: 0;
    border: none;
  }
  div.zs-indication {
    padding: .5rem .7rem;
    padding: .5em .7em;
    max-width: 100%;
    border-radius: .5rem;
    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: .25rem;
    padding: .1rem .2rem;
    border-radius: .25em;
    padding: .1rem .2em;
    font-size: 95%;
  }
  .zs-info {
    background-color: lightblue;
    padding: .5rem 1rem;
    padding: .5em 1em;
  }
  .zs-warning {
    background-color: lightyellow;
    padding: .5rem 1rem;
    padding: .5em 1em;
  }
  .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 }
  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: .2rem }
  .zs-deprecated { border-style: dashed; padding: .2em }
  .zs-meta {
    font-size:.75rem;
    color:#444;
    margin-bottom:1rem;
    margin-bottom:1em;
  }
  .zs-meta a { color:#444 }
  h1+.zs-meta { margin-top:-1rem }
  nav > details { margin-top:1rem }
  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:2rem;
    padding-left:2em;
    background-color: #eee;
  }
  footer { padding: 0 1rem }
  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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register(
		" const",
		func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		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 domain.Content
	content zettel.Content
}

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

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

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

func (cb *constBox) CreateZettel(context.Context, 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) {
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 domain.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
		return zettel.Zettel{Meta: meta.NewWithData(zid, z.header), Content: z.content}, nil
	}
	err := box.ErrZettelNotFound{Zid: zid}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetZettel")
	return domain.Zettel{}, box.ErrNotFound
	cb.log.Trace().Err(err).Msg("GetZettel/Err")
	return zettel.Zettel{}, err
}

func (cb *constBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	if z, ok := cb.zettel[zid]; ok {
func (cb *constBox) HasZettel(_ context.Context, zid id.Zid) bool {
	_, found := cb.zettel[zid]
		cb.log.Trace().Msg("GetMeta")
		return meta.NewWithData(zid, z.header), nil
	}
	cb.log.Trace().Err(box.ErrNotFound).Msg("GetMeta")
	return nil, box.ErrNotFound
	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
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







-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-


-
+
-


+
+




















-
+











-
+










-
+







-
+

-
+

-
+




-
-
+
+
+


-
+




-
+

+


-
+




-
-
+
+
+


-
+




-
+

-
-
-
-
-
-
-
-
-
+


-
+




-
+

-
-
-
-
-
-
-
-
-
+


-
+




-
+

+


-
+




-
-
+
+
+


-
+




-
+

+


-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






+


-
+








-
+
-
-
-
-
-
-
-
-
-









-
+







+


-
+


-
-
-
-
-
+
+
+
+
+
+
+

-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+











-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
-
-
+
+
+
+
+
+

-
+











-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+

+
+
+
-
-
+
+

-
-
+
+

-
-
+
+

-
-
+
+









+
+
+
+
+
+
+
+
+
+
+
+



			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 {
func (cb *constBox) DeleteZettel(_ context.Context, zid id.Zid) (err error) {
	err := box.ErrNotFound
	if _, ok := cb.zettel[zid]; ok {
		err = box.ErrReadOnly
	} else {
		err = box.ErrZettelNotFound{Zid: zid}
	}
	cb.log.Trace().Err(err).Msg("DeleteZettel")
	return err
}

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)},
		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,
		},
		domain.NewContent(contentLicense)},
		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,
		},
		domain.NewContent(contentContributors)},
		zettel.NewContent(contentContributors)},
	id.MustParse(api.ZidDependencies): {
		constHeader{
			api.KeyTitle:      "Zettelstore Dependencies",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyLang:       api.ValueLangEN,
			api.KeyReadOnly:   api.ValueTrue,
			api.KeyVisibility: api.ValueVisibilityLogin,
			api.KeyVisibility: api.ValueVisibilityPublic,
			api.KeyCreated:    "20210504135842",
			api.KeyModified:   "20221013105100",
			api.KeyModified:   "20240418095500",
		},
		domain.NewContent(contentDependencies)},
		zettel.NewContent(contentDependencies)},
	id.BaseTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Base HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeyCreated:    "20210504135842",
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155100",
			api.KeyModified:   "20240219145300",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentBaseMustache)},
		zettel.NewContent(contentBaseSxn)},
	id.LoginTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Login Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentLoginMustache)},
		zettel.NewContent(contentLoginSxn)},
	id.ZettelTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeyCreated:    "20200804111624",
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230510155300",
			api.KeyModified:   "20241127170400",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentZettelMustache)},
		zettel.NewContent(contentZettelSxn)},
	id.InfoTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Info HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeySyntax:     meta.SyntaxSxn,
			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.KeyModified:   "20241127170500",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentContextMustache)},
		zettel.NewContent(contentInfoSxn)},
	id.FormTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Form HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeySyntax:     meta.SyntaxSxn,
			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.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentRenameMustache)},
		zettel.NewContent(contentFormSxn)},
	id.DeleteTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Delete HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20200804111624",
			api.KeyModified:   "20241127170530",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentDeleteMustache)},
		zettel.NewContent(contentDeleteSxn)},
	id.ListTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore List Zettel HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeyCreated:    "20200804111624",
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20230704122100",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentListZettelMustache)},
		zettel.NewContent(contentListZettelSxn)},
	id.ErrorTemplateZid: {
		constHeader{
			api.KeyTitle:      "Zettelstore Error HTML Template",
			api.KeyRole:       api.ValueRoleConfiguration,
			api.KeySyntax:     meta.SyntaxMustache,
			api.KeySyntax:     meta.SyntaxSxn,
			api.KeyCreated:    "20210305133215",
			api.KeyModified:   "20240219145200",
			api.KeyVisibility: api.ValueVisibilityExpert,
		},
		domain.NewContent(contentErrorMustache)},
		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,
		},
		domain.NewContent(contentBaseCSS)},
		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,
		},
		domain.NewContent([]byte("/* User-defined CSS */"))},
		zettel.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)},
		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,
		},
		domain.NewContent(contentNewTOCZettel)},
		zettel.NewContent(contentNewTOCZettel)},
	id.MustParse(api.ZidTemplateNewZettel): {
		constHeader{
			api.KeyTitle:      "New Zettel",
			api.KeyRole:       api.ValueRoleZettel,
			api.KeySyntax:     meta.SyntaxZmk,
			api.KeyCreated:    "20201028185209",
			api.KeyVisibility: api.ValueVisibilityCreator,
			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,
		},
		domain.NewContent(nil)},
		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,
		},
		domain.NewContent(nil)},
		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:    "20241213103100",
			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",
			api.KeyTitle:    "Home",
			api.KeyRole:     api.ValueRoleZettel,
			api.KeySyntax:   meta.SyntaxZmk,
			api.KeyLang:     api.ValueLangEN,
			api.KeyCreated:  "20210210190757",
			api.KeyModified: "20241216105800",
		},
		domain.NewContent(contentHomeZettel)},
		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.mustache
var contentBaseMustache []byte
//go:embed base.sxn
var contentBaseSxn []byte

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

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

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

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

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

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

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

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

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

//go:embed error.mustache
var contentErrorMustache []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
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.
```

=== 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
170
171
172
173
174
175
176















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

Changes to box/constbox/home.zettel.

1
2
3
4
5
6
7




8
9
10

11
12
13
14
15
16
17
1
2
3




4
5
6
7
8


9
10
11
12
13
14
15
16



-
-
-
-
+
+
+
+

-
-
+







=== Thank you for using Zettelstore!

You will find the lastest information about Zettelstore at [[https://zettelstore.de]].
Check that website regulary for [[upgrades|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version.
You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before upgrading.
Sometimes, you have to edit some of your Zettelstore-related zettel before upgrading.
Since Zettelstore is currently in a development state, every upgrade might fix some of your problems.
Check this website regularly for [[updates|https://zettelstore.de/home/doc/trunk/www/download.wiki]] to the latest version.
You should consult the [[change log|https://zettelstore.de/home/doc/trunk/www/changes.wiki]] before updating.
Sometimes, you have to edit some of your Zettelstore-related zettel before updating.
Since Zettelstore is currently in a development state, every update might fix some of your problems.

If you have problems concerning Zettelstore,
do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]].
If you have problems concerning Zettelstore, do not hesitate to get in [[contact with the main developer|mailto:ds@zettelstore.de]].

=== Reporting errors
If you have encountered an error, please include the content of the following zettel in your mail (if possible):
* [[Zettelstore Version|00000000000001]]: {{00000000000001}}
* [[Zettelstore Operating System|00000000000003]]
* [[Zettelstore Startup Configuration|00000000000096]]
* [[Zettelstore Runtime Configuration|00000000000100]]

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

1
2
3
4
5
6
7
8
-
+







Copyright (c) 2020-2023 Detlef Stern
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
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
21
22
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
There is some CSS code to make the default web user interface a litte bit nicer.
The default image to signal a broken image can be configured too.

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

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	manager.Register("dir", func(u *url.URL, cdata *manager.ConnectData) (box.ManagedBox, error) {
		var log *logger.Logger
		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
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 count := 0; count < 2; count++ {
	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
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 := uint32(0); i < dp.fSrvs; i++ {
	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.Fatal().Err(err).Msg("Unable to create directory supervisor")
		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
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) {
	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) 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 domain.Zettel) (id.Zid, error) {
func (dp *dirBox) CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	if dp.readonly {
		return id.Invalid, box.ErrReadOnly
	}

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

	err = dp.srvSetZettel(ctx, &entry, zettel)
	if err == nil {
		err = dp.dirSrv.UpdateDirEntry(&entry)
	}
	dp.notifyChanged(meta.Zid)
	dp.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) (domain.Zettel, error) {
func (dp *dirBox) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return domain.Zettel{}, box.ErrNotFound
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	m, c, err := dp.srvGetMetaContent(ctx, entry, zid)
	if err != nil {
		return domain.Zettel{}, err
		return zettel.Zettel{}, err
	}
	zettel := domain.Zettel{Meta: m, Content: domain.NewContent(c)}
	zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(c)}
	dp.log.Trace().Zid(zid).Msg("GetZettel")
	return zettel, nil
}

func (dp *dirBox) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
func (dp *dirBox) HasZettel(_ context.Context, zid id.Zid) bool {
	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)
	return dp.dirSrv.GetDirEntry(zid).IsValid()
	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)
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
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, domain.Zettel) bool {
func (dp *dirBox) CanUpdateZettel(context.Context, zettel.Zettel) bool {
	return !dp.readonly
}

func (dp *dirBox) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
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.ErrInvalidID{Zid: zid}
		return box.ErrInvalidZid{Zid: zid.String()}
	}
	entry := dp.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		// Existing zettel, but new in this box.
		entry = &notify.DirEntry{Zid: zid}
	}
	dp.updateEntryFromMetaContent(entry, meta, zettel.Content)
	dp.dirSrv.UpdateDirEntry(entry)
	err := dp.srvSetZettel(ctx, entry, zettel)
	if err == nil {
		dp.notifyChanged(zid)
		dp.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 domain.Content) {
func (dp *dirBox) updateEntryFromMetaContent(entry *notify.DirEntry, m *meta.Meta, content zettel.Content) {
	entry.SetupFromMetaContent(m, content, dp.cdata.Config.GetZettelFileSyntax)
}

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

func (dp *dirBox) RenameZettel(ctx context.Context, curZid, newZid id.Zid) error {
	if curZid == newZid {
		return nil
	}
	curEntry := dp.dirSrv.GetDirEntry(curZid)
	if !curEntry.IsValid() {
		return box.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
		return box.ErrZettelNotFound{Zid: zid}
	}
	err := dp.dirSrv.DeleteDirEntry(zid)
	if err != nil {
		return nil
	}
	err = dp.srvDeleteZettel(ctx, entry, zid)
	if err == nil {
		dp.notifyChanged(zid)
		dp.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
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 := uint32(0); i < 1500; i++ {
	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
1

2
3
4
5
6
7
8
9
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
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
	"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)
		if ri := recover(); ri != nil {
			kernel.Main.LogRecover("FileService", ri)
			go fileService(i, log, dirPath, cmds)
		}
	}()

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

type fileCmd interface {
	run(*logger.Logger, string)
	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
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(log *logger.Logger, dirPath string) {
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 == "" {
			log.Panic().Zid(zid).Msg("No meta, no content in getMeta")
			err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid)
		}
		if entry.HasMetaInContent() {
		} 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
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(log *logger.Logger, dirPath string) {
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 == "" {
			log.Panic().Zid(zid).Msg("No meta, no content in getMetaContent")
			err = fmt.Errorf("no meta, no content in getMetaContent, zid=%v", zid)
		}
		if entry.HasMetaInContent() {
		} 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
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 domain.Zettel) error {
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 domain.Zettel
	zettel zettel.Zettel
	rc     chan<- resSetZettel
}
type resSetZettel = error

func (cmd *fileSetZettel) run(log *logger.Logger, dirPath string) {
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 == "" {
			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)
			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)
	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
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
	}
	if err == nil {
		err = writeMetaHeader(zettelFile, m)
	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
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(log *logger.Logger, dirPath string) {
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 == "" {
			log.Panic().Zid(entry.Zid).Msg("No meta, no content in getMetaContent")
		}
		err = os.Remove(contentPath)
			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
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, 0600)
	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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/query"
)

type zipBox struct {
	log      *logger.Logger
	number   int
	name     string
	enricher box.Enricher
	notify   chan<- box.UpdateInfo
	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.log, zipNotifier, zb.notify)
	zb.dirSrv = notify.NewDirService(zb, zb.log, zipNotifier, zb.notify)
	zb.dirSrv.Start()
	return nil
}

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

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

	zb.dirSrv = nil
func (*zipBox) CanCreateZettel(context.Context) bool { return false }

}
func (zb *zipBox) CreateZettel(context.Context, 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) {
func (zb *zipBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	entry := zb.dirSrv.GetDirEntry(zid)
	if !entry.IsValid() {
		return domain.Zettel{}, box.ErrNotFound
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	reader, err := zip.OpenReader(zb.name)
	if err != nil {
		return domain.Zettel{}, err
		return zettel.Zettel{}, err
	}
	defer reader.Close()

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

	contentName := entry.ContentName
	if metaName := entry.MetaName; metaName == "" {
		if contentName == "" {
			zb.log.Panic().Zid(zid).Msg("No meta, no content in zipBox.GetZettel")
			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 domain.Zettel{}, err
			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 domain.Zettel{}, err
			return zettel.Zettel{}, err
		}
		inMeta = true
		if contentName != "" {
			src, err = readZipFileContent(reader, entry.ContentName)
			if err != nil {
				return domain.Zettel{}, err
				return zettel.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
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(src)}, nil
}

func (zb *zipBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
	entry := zb.dirSrv.GetDirEntry(zid)
func (zb *zipBox) HasZettel(_ context.Context, zid id.Zid) bool {
	return zb.dirSrv.GetDirEntry(zid).IsValid()
	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)
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
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) 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
		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 == "" {
			zb.log.Panic().Zid(zid).Msg("No meta, no content in getMeta")
			err = fmt.Errorf("no meta, no content in getMeta, zid=%v", zid)
		}
		if entry.HasMetaInContent() {
		} 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

























1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/domain/id"
	"zettelstore.de/z/zettel/id"
)

// GetNewZid calculates a new and unused zettel identifier, based on the current date and time.
func GetNewZid(testZid func(id.Zid) (bool, error)) (id.Zid, error) {
	withSeconds := false
	for i := 0; i < 90; i++ { // Must be completed within 9 seconds (less than web/server.writeTimeout)
	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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/domain/id"
	"zettelstore.de/z/zettel/id"
)

type arAction int

const (
	arNothing arAction = iota
	arReload
	arZettel
)

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

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

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

func (ar *anterooms) EnqueueZettel(zid id.Zid) {
func (ar *anteroomQueue) EnqueueZettel(zid id.Zid) {
	if !zid.IsValid() {
		return
	}
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
		ar.first = ar.makeAnteroom(zid)
		ar.last = ar.first
		return
	}
	for room := ar.first; room != nil; room = room.next {
		if room.reload {
			continue // Do not put zettel in reload room
		}
		if _, ok := room.waiting[zid]; ok {
		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.Zid(zid)
		room.waiting.Add(zid)
		room.curLoad++
		return
	}
	room := ar.makeAnteroom(zid)
	ar.last.next = room
	ar.last = room
}

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

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

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

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

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

func (ar *anterooms) deleteReloadedRooms() {
	}
}

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 *anterooms) Dequeue() (arAction, id.Zid, uint64) {
func (ar *anteroomQueue) Dequeue() (arAction, id.Zid, bool) {
	ar.mx.Lock()
	defer ar.mx.Unlock()
	if ar.first == nil {
	first := ar.first
		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 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() {
		if len(ar.first.waiting) == 0 {
			ar.removeFirst()
		}
		return arZettel, zid, roomNo
	}
	ar.removeFirst()
	return arNothing, id.Invalid, 0
				ar.removeFirst()
			}
			return arZettel, zid, first.reload
		}
		ar.removeFirst()
	}
	return arNothing, id.Invalid, false
}

func (ar *anterooms) removeFirst() {
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/domain/id"
	"zettelstore.de/z/zettel/id"
)

func TestSimple(t *testing.T) {
	t.Parallel()
	ar := newAnterooms(2)
	ar := newAnteroomQueue(2)
	ar.EnqueueZettel(id.Zid(1))
	action, zid, rno := ar.Dequeue()
	if zid != id.Zid(1) || action != arZettel || rno != 1 {
		t.Errorf("Expected arZettel/1/1, but got %v/%v/%v", action, zid, rno)
	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
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 := newAnterooms(1)
	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
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 = newAnterooms(1)
	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 = newAnterooms(1)
	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











1

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

-
+






+
+
+





-


+


-
-
-
-
+
+
+
+









-
-
+
+

-
+

-
+

-
+




+
+
+


+
-
+
+
+



-
+

+
+
+


+
+
+
-
-
+
+
+
+

-
+



-
+

+
+
+


-
-
-
+
+
+

+
-
+






-
+



-
+

+
+
+


-
-
-
-
+









-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-


-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
-
+
-

-
+

-
+







+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+


+
+
+



-
-
-
-
+
+
+
+
+
+
-
-
+

-
+














-
+




-
-
+
+







+
+
-
+



-
+
+
+
+


+
-
+
+
+
+



-
+

-
+
-
-
-
+

-
-
-
-
-
-
-
-
+

-
-
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-




-
+
-
-


+
+











+
+
+


-
-
-



+
-
+

+
-
+



-
+

+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 (
	"bytes"
	"context"
	"errors"
	"strings"

	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"zettelstore.de/z/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 buf bytes.Buffer
	for i := 0; i < len(mgr.boxes)-2; i++ {
	var sb strings.Builder
	for i := range len(mgr.boxes) - 2 {
		if i > 0 {
			buf.WriteString(", ")
			sb.WriteString(", ")
		}
		buf.WriteString(mgr.boxes[i].Location())
		sb.WriteString(mgr.boxes[i].Location())
	}
	return buf.String()
	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 mgr.started && mgr.boxes[0].CanCreateZettel(ctx)
		return box.CanCreateZettel(ctx)
	}
	return false
}

// CreateZettel creates a new zettel.
func (mgr *Manager) CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
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)
		zid, err := box.CreateZettel(ctx, ztl)
	if !mgr.started {
		return id.Invalid, box.ErrStopped
		if err == nil {
			mgr.idxUpdateZettel(ctx, ztl)
		}
		return zid, err
	}
	return mgr.boxes[0].CreateZettel(ctx, zettel)
	return id.Invalid, box.ErrReadOnly
}

// GetZettel retrieves a specific zettel.
func (mgr *Manager) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
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()
	if !mgr.started {
		return domain.Zettel{}, box.ErrStopped
	}
	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); err != box.ErrNotFound {
		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 domain.Zettel{}, box.ErrNotFound
	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) ([]domain.Zettel, error) {
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()
	if !mgr.started {
		return nil, box.ErrStopped
	}
	var result []domain.Zettel
	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
}

// 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")
// 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
	}
	return mgr.doGetMeta(ctx, zid)
}

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

// 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 {
	return mgr.fetchZids(ctx)
}
func (mgr *Manager) fetchZids(ctx context.Context) (*id.Set, error) {
	numZettel := 0
	for _, 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
}

		var mbstats box.ManagedBoxStats
// 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()
		p.ReadStats(&mbstats)
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		numZettel += mbstats.Zettel
		return nil, box.ErrStopped
	}
	result := id.Set{}
	result := id.NewSetCap(numZettel)
	for _, p := range mgr.boxes {
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Zid(zid) }, func(id.Zid) bool { return true })
		err := p.ApplyZid(ctx, func(zid id.Zid) { result.Add(zid) }, query.AlwaysIncluded)
		if err != nil {
			return nil, err
		}
	}
	return result, nil
}

func (mgr *Manager) hasZettel(ctx context.Context, zid id.Zid) bool {
	mgr.mgrLog.Debug().Zid(zid).Msg("HasZettel")
	if 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
}
type metaMap map[id.Zid]*meta.Meta


// 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, q *query.Query) ([]*meta.Meta, error) {
func (mgr *Manager) SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error) {
	if msg := mgr.mgrLog.Debug(); msg.Enabled() {
		msg.Str("query", q.String()).Msg("SelectMeta")
	}
	if err := mgr.checkContinue(ctx); err != nil {
		return nil, err
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return nil, box.ErrStopped
	}


	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
	}
	compSearch := q.RetrieveAndCompile(mgr)
	selected := metaMap{}
	selected := map[id.Zid]*meta.Meta{}
	for _, term := range compSearch.Terms {
		rejected := id.Set{}
		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.Zid(zid)
				rejected.Add(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
			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 q.Sort(result), nil
	return result, nil
}

// CanUpdateZettel returns true, if box could possibly update the given zettel.
func (mgr *Manager) CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool {
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 mgr.started && mgr.boxes[0].CanUpdateZettel(ctx, zettel)
		return box.CanUpdateZettel(ctx, zettel)
	}
	return false

}

// UpdateZettel updates an existing zettel.
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel domain.Zettel) error {
func (mgr *Manager) UpdateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrLog.Debug().Zid(zettel.Meta.Zid).Msg("UpdateZettel")
	mgr.mgrMx.RLock()
	if err := mgr.checkContinue(ctx); err != nil {
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return box.ErrStopped
		return err
	}
	// 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)
	return mgr.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 {
func (mgr *Manager) updateZettel(ctx context.Context, zettel zettel.Zettel) error {
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}
	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
		}
	for _, p := range mgr.boxes {
		if !p.AllowRenameZettel(ctx, zid) {
			return false
		}
		mgr.idxUpdateZettel(ctx, zettel)
		return nil
	}
	}
	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
	return box.ErrReadOnly
	}
	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()
	if err := mgr.checkContinue(ctx); err != nil {
	defer mgr.mgrMx.RUnlock()
	if !mgr.started {
		return false
	}
	mgr.mgrMx.RLock()
	defer mgr.mgrMx.RUnlock()
	for _, p := range mgr.boxes {
		if p.CanDeleteZettel(ctx, zid) {
			return true
		}
	}
	return false
}

// DeleteZettel removes the zettel from the box.
func (mgr *Manager) DeleteZettel(ctx context.Context, zid id.Zid) error {
	mgr.mgrLog.Debug().Zid(zid).Msg("DeleteZettel")
	if err := mgr.checkContinue(ctx); err != nil {
		return err
	}
	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 {
			mgr.idxDeleteZettel(ctx, zid)
			return nil
			return err
		}
		var errZNF box.ErrZettelNotFound
		if !errors.Is(err, box.ErrNotFound) && !errors.Is(err, box.ErrReadOnly) {
		if !errors.As(err, &errZNF) && !errors.Is(err, box.ErrReadOnly) {
			return err
		}
	}
	return box.ErrNotFound
	return box.ErrZettelNotFound{Zid: zid}
}

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

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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20


21
22
23
24
25

26
27
28
29
30
31
32
33

-
+






+
+
+









-
-
+
+



-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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/domain/id"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type collectData struct {
	refs  id.Set
	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
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.Zid(zid)
		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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

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

	// Calculate computed, but stored values.
	if _, ok := m.Get(api.KeyCreated); !ok {
	_, hasCreated := m.Get(api.KeyCreated)
	if !hasCreated {
		m.Set(api.KeyCreated, computeCreated(m.Zid))
	}

	if box.DoNotEnrich(ctx) {
	if box.DoEnrich(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)
		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 artificaial Zid.
		// 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
1

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

-
+






+
+
+












-
-
-



+
+
+




-
+

-
+








-
+

-
+








-
+

-
+








-
+

-
+











-
-
+
+








-
+



-




-


-
+




-



-
+
-
-











-
-
-
-
+



+

-
+




-














+










-
+


+
-
-
+
+
+

-
+





+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// SearchEqual returns all zettel that contains the given exact word.
// The word must be normalized through Unicode NKFD, trimmed and not empty.
func (mgr *Manager) SearchEqual(word string) id.Set {
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")
	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 {
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")
	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 {
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")
	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 {
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")
	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 r := recover(); r != nil {
			kernel.Main.LogRecover("Indexer", r)
		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 {
		// Sleep first, so the indexer will wait for boxes to initialize.
		mgr.idxWorkService(ctx)
		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 {
		switch action, zid, lastReload := 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 {
				mgr.idxAr.Reload(zids)
					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)
				mgr.idxDeleteZettel(ctx, zid)
				continue
			}
			mgr.idxLog.Trace().Zid(zid).Msg("update")
			mgr.idxUpdateZettel(ctx, zettel)
			mgr.idxMx.Lock()
			if arRoomNum == roomNum {
			if lastReload {
				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
		}
		// 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 domain.Zettel) {
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)

		collectZettelIndexData(parser.ParseZettel(ctx, zettel, "", mgr.rtConfig), &cData)
	}

	m := zettel.Meta
	zi := store.NewZettelIndex(m.Zid)
	zi := store.NewZettelIndex(m)
	mgr.idxCollectFromMeta(ctx, m, zi, &cData)
	mgr.idxProcessData(ctx, zi, &cData)
	toCheck := mgr.idxStore.UpdateReferences(ctx, zi)
	mgr.idxCheckZettel(toCheck)
}

func mustIndexZettel(m *meta.Meta) bool {
	return m.Zid >= id.Zid(999999900)
}

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







+
-
-
+
+
+
+
+





+
+
+
+
+
+
+
+
+
+

-
-
+
+




-
+









-
+







-
+


-
-
+
+



-
-
+
+

-
+

			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 _, word := range strfun.NormalizeWords(pair.Value) {
				cData.words.Add(word)
				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) {
	for ref := range cData.refs {
		if _, err := mgr.GetMeta(ctx, ref); err == nil {
	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 _, err = mgr.GetMeta(ctx, zid); err != nil {
	if !mgr.hasZettel(ctx, zid) {
		zi.AddDeadRef(zid)
		return
	}
	if inverseKey == "" {
		zi.AddBackRef(zid)
		return
	}
	zi.AddMetaRef(inverseKey, zid)
	zi.AddInverseRef(inverseKey, zid)
}

func (mgr *Manager) idxDeleteZettel(zid id.Zid) {
	toCheck := mgr.idxStore.DeleteZettel(context.Background(), zid)
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) {
	for zid := range s {
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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/c/maps"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager/memstore"
	"zettelstore.de/z/box/manager/mapstore"
	"zettelstore.de/z/box/manager/store"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ConnectData contains all administration related values.
type ConnectData struct {
	Number   int // number of the box, starting with 1.
	Config   config.Config
	Enricher box.Enricher
	Notify   chan<- box.UpdateInfo
	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
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
}

// 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
	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    *anterooms
	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: memstore.New(),
		idxAr:    newAnterooms(1000),
		idxStore: createIdxStore(rtConfig),
		idxAr:    newAnteroomQueue(1000),
		idxReady: make(chan struct{}, 1),
	}

	cdata := ConnectData{Number: 1, Config: rtConfig, Enricher: mgr, Notify: mgr.infos}
	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
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 r := recover(); r != nil {
			kernel.Main.LogRecover("Notifier", r)
		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
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)
					mgr.notifyObserver(&ci)
				}
			}
		case <-mgr.done:
			return
		}
	}
}

224
225
226
227
228
229
230


231
232
233
234


235

236
237
238
239
240
241
242
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270







+
+




+
+

+







		reason: reason,
	}
	return false
}

func (mgr *Manager) idxEnqueue(reason box.UpdateReason, zid id.Zid) {
	switch reason {
	case box.OnReady:
		return
	case box.OnReload:
		mgr.idxAr.Reset()
	case box.OnZettel:
		mgr.idxAr.EnqueueZettel(zid)
	case box.OnDelete:
		mgr.idxAr.EnqueueZettel(zid)
	default:
		mgr.mgrLog.Error().Uint("reason", uint64(reason)).Zid(zid).Msg("Unknown notification reason")
		return
	}
	select {
	case mgr.idxReady <- struct{}{}:
	default:
	}
}
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
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()
	if mgr.started {
		mgr.mgrMx.Unlock()
	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.mgrMx.Unlock()
		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

	mgr.started = true
	mgr.mgrMx.Unlock()
	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 !mgr.started {
	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.started = false
	mgr.setState(box.StartStateStopped)
}

// Refresh internal box data.
func (mgr *Manager) Refresh(ctx context.Context) error {
	mgr.mgrLog.Debug().Msg("Refresh")
	mgr.mgrMx.Lock()
	if err := mgr.checkContinue(ctx); err != nil {
	defer mgr.mgrMx.Unlock()
	if !mgr.started {
		return box.ErrStopped
		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













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
1

2
3
4
5
6
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
// 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/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"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
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
	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
	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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
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
// 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 (
	"sort"
	"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
	}
	sort.Strings(got)
	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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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 (
import "zettelstore.de/z/domain/id"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// ZettelIndex contains all index data of a zettel.
type ZettelIndex struct {
	Zid      id.Zid            // zid of the indexed zettel
	backrefs id.Set            // set of back references
	metarefs map[string]id.Set // references to inverse keys
	deadrefs id.Set            // set of dead references
	words    WordSet
	urls     WordSet
	Zid         id.Zid             // zid of the indexed zettel
	meta        *meta.Meta         // full metadata
	backrefs    *id.Set            // set of back references
	inverseRefs map[string]*id.Set // references of inverse keys
	deadrefs    *id.Set            // set of dead references
	words       WordSet
	urls        WordSet
}

// NewZettelIndex creates a new zettel index.
func NewZettelIndex(zid id.Zid) *ZettelIndex {
func NewZettelIndex(m *meta.Meta) *ZettelIndex {
	return &ZettelIndex{
		Zid:      zid,
		backrefs: id.NewSet(),
		metarefs: make(map[string]id.Set),
		deadrefs: id.NewSet(),
		Zid:         m.Zid,
		meta:        m,
		backrefs:    id.NewSet(),
		inverseRefs: make(map[string]*id.Set),
		deadrefs:    id.NewSet(),
	}
}

// AddBackRef adds a reference to a zettel where the current zettel links to
// without any more information.
func (zi *ZettelIndex) AddBackRef(zid id.Zid) {
func (zi *ZettelIndex) AddBackRef(zid id.Zid) { zi.backrefs.Add(zid) }
	zi.backrefs.Zid(zid)
}


// AddMetaRef adds a named reference to a zettel. On that zettel, the given
// AddInverseRef adds a named reference to a zettel. On that zettel, the given
// metadata key should point back to the current zettel.
func (zi *ZettelIndex) AddMetaRef(key string, zid id.Zid) {
	if zids, ok := zi.metarefs[key]; ok {
		zids.Zid(zid)
func (zi *ZettelIndex) AddInverseRef(key string, zid id.Zid) {
	if zids, ok := zi.inverseRefs[key]; ok {
		zids.Add(zid)
		return
	}
	zi.metarefs[key] = id.NewSet(zid)
	zi.inverseRefs[key] = id.NewSet(zid)
}

// AddDeadRef adds a dead reference to a zettel.
func (zi *ZettelIndex) AddDeadRef(zid id.Zid) {
	zi.deadrefs.Zid(zid)
	zi.deadrefs.Add(zid)
}

// SetWords sets the words to the given value.
func (zi *ZettelIndex) SetWords(words WordSet) { zi.words = words }

// SetUrls sets the words to the given value.
func (zi *ZettelIndex) SetUrls(urls WordSet) { zi.urls = urls }

// GetDeadRefs returns all dead references as a sorted list.
func (zi *ZettelIndex) GetDeadRefs() id.Slice {
func (zi *ZettelIndex) GetDeadRefs() *id.Set { return zi.deadrefs }
	return zi.deadrefs.Sorted()
}

// GetMeta return just the raw metadata.
func (zi *ZettelIndex) GetMeta() *meta.Meta { return zi.meta }

// GetBackRefs returns all back references as a sorted list.
func (zi *ZettelIndex) GetBackRefs() id.Slice {
func (zi *ZettelIndex) GetBackRefs() *id.Set { return zi.backrefs }
	return zi.backrefs.Sorted()
}


// GetMetaRefs returns all meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetMetaRefs() map[string]id.Slice {
	if len(zi.metarefs) == 0 {
// GetInverseRefs returns all inverse meta references as a map of strings to a sorted list of references
func (zi *ZettelIndex) GetInverseRefs() map[string]*id.Set {
	if len(zi.inverseRefs) == 0 {
		return nil
	}
	result := make(map[string]id.Slice, len(zi.metarefs))
	for key, refs := range zi.metarefs {
		result[key] = refs.Sorted()
	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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

func init() {
	manager.Register(
		"mem",
		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
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]domain.Zettel
	zettel    map[id.Zid]zettel.Zettel
	curBytes  int
}

func (mb *memBox) notifyChanged(zid id.Zid) {
	if chci := mb.cdata.Notify; chci != nil {
		chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid}
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]domain.Zettel)
	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 domain.Zettel) (id.Zid, error) {
func (mb *memBox) CreateZettel(_ context.Context, zettel zettel.Zettel) (id.Zid, error) {
	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if mb.maxZettel < len(mb.zettel) || mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return id.Invalid, box.ErrCapacity
	}
	zid, err := box.GetNewZid(func(zid id.Zid) (bool, error) {
		_, ok := mb.zettel[zid]
		return !ok, nil
	})
	if err != nil {
		mb.mx.Unlock()
		return id.Invalid, err
	}
	meta := zettel.Meta.Clone()
	meta.Zid = zid
	zettel.Meta = meta
	mb.zettel[zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()

	mb.notifyChanged(zid)
	mb.notifyChanged(zid, box.OnZettel)
	mb.log.Trace().Zid(zid).Msg("CreateZettel")
	return zid, nil
}

func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (domain.Zettel, error) {
func (mb *memBox) GetZettel(_ context.Context, zid id.Zid) (zettel.Zettel, error) {
	mb.mx.RLock()
	zettel, ok := mb.zettel[zid]
	z, ok := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return domain.Zettel{}, box.ErrNotFound
		return zettel.Zettel{}, box.ErrZettelNotFound{Zid: zid}
	}
	zettel.Meta = zettel.Meta.Clone()
	z.Meta = z.Meta.Clone()
	mb.log.Trace().Msg("GetZettel")
	return zettel, nil
	return z, nil
}

func (mb *memBox) GetMeta(_ context.Context, zid id.Zid) (*meta.Meta, error) {
func (mb *memBox) HasZettel(_ context.Context, zid id.Zid) bool {
	mb.mx.RLock()
	zettel, ok := mb.zettel[zid]
	_, found := mb.zettel[zid]
	mb.mx.RUnlock()
	if !ok {
		return nil, box.ErrNotFound
	return found
	}
	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 {
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
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 domain.Zettel) bool {
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 domain.Zettel) error {
func (mb *memBox) UpdateZettel(_ context.Context, zettel zettel.Zettel) error {
	m := zettel.Meta.Clone()
	if !m.Zid.IsValid() {
		return &box.ErrInvalidID{Zid: m.Zid}
		return box.ErrInvalidZid{Zid: m.Zid.String()}
	}

	mb.mx.Lock()
	newBytes := mb.curBytes + zettel.Length()
	if prevZettel, found := mb.zettel[m.Zid]; found {
		newBytes -= prevZettel.Length()
	}
	if mb.maxBytes < newBytes {
		mb.mx.Unlock()
		return box.ErrCapacity
	}

	zettel.Meta = m
	mb.zettel[m.Zid] = zettel
	mb.curBytes = newBytes
	mb.mx.Unlock()
	mb.notifyChanged(m.Zid)
	mb.notifyChanged(m.Zid, box.OnZettel)
	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
		return box.ErrZettelNotFound{Zid: zid}
	}
	delete(mb.zettel, zid)
	mb.curBytes -= oldZettel.Length()
	mb.mx.Unlock()
	mb.notifyChanged(zid)
	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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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"
	"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"
	"zettelstore.de/z/zettel/id"
)

type entrySet map[id.Zid]*DirEntry

// directoryState signal the internal state of the service.
// 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 directoryState uint8
type DirServiceState uint8

// Constants for DirServiceState
const (
	dsCreated  directoryState = iota
	dsStarting                // Reading inital scan
	dsWorking                 // Initial scan complete, fully operational
	dsMissing                 // Directory is missing
	dsStopping                // Service is shut down
	DsCreated  DirServiceState = iota
	DsStarting                 // Reading inital scan
	DsWorking                  // Initial scan complete, fully operational
	DsMissing                  // Directory is missing
	DsStopping                 // Service is shut down
)

// DirService specifies a directory service for file based zettel.
type DirService struct {
	box      box.ManagedBox
	log      *logger.Logger
	dirPath  string
	notifier Notifier
	infos    chan<- box.UpdateInfo
	infos    box.UpdateNotifier
	mx       sync.RWMutex // protects status, entries
	state    directoryState
	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(log *logger.Logger, notifier Notifier, chci chan<- box.UpdateInfo) *DirService {
func NewDirService(box box.ManagedBox, log *logger.Logger, notifier Notifier, notify box.UpdateNotifier) *DirService {
	return &DirService{
		box:      box,
		log:      log,
		notifier: notifier,
		infos:    chci,
		state:    dsCreated,
		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.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.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
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
}

// 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)
		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 {
	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")
		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
			fromMissing := ds.state == DsMissing
			prevEntries := ds.entries
			ds.entries = newEntries
			ds.state = dsWorking
			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)
			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)
			ds.notifyChange(zid, box.OnDelete)
		}
	default:
		ds.log.Warn().Str("event", fmt.Sprintf("%v", ev)).Msg("Unknown zettel notification event")
		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)
		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)
		ds.notifyChange(zid, box.OnDelete)
	}
}

func (ds *DirService) onDestroyDirectory() {
	ds.mx.Lock()
	entries := ds.entries
	ds.entries = nil
	ds.state = dsMissing
	ds.state = DsMissing
	ds.mx.Unlock()
	for zid := range entries {
		ds.notifyChange(zid)
		ds.notifyChange(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
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.Warn().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		ds.log.Info().Str("name", dupName1).Msg("Duplicate content (is ignored)")
		if dupName2 != "" {
			ds.log.Warn().Str("name", dupName2).Msg("Duplicate content (is ignored)")
			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
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) {
	if chci := ds.infos; chci != nil {
		ds.log.Trace().Zid(zid).Msg("notifyChange")
		chci <- box.UpdateInfo{Reason: box.OnZettel, Zid: zid}
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
1

2
3
4
5
6
7
8
9
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 Detlef Stern
// 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/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.
	"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
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.SyntaxTxt, meta.SyntaxHTML,
		meta.SyntaxMustache, meta.SyntaxText, meta.SyntaxPlain,
		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
1

2
3
4
5
6
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"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
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 domain.Content, getZettelFileSyntax func() []string) {
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, "")
	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 domain.Content) string {
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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
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.Warn().
			Str("parentDir", absParentDir).Err(errParent).
		log.Info().Str("parentDir", absParentDir).Err(errParent).
			Msg("Parent of Zettel directory cannot be supervised")
		log.Warn().Str("path", absPath).
		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.Warn().Err(err).Str("path", absPath).Msg("Zettel directory not available")
		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
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
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.IfErr(err).Str("name", fsdn.path).Msg("Unable to add directory")
			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
1

2
3
4
5
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-2022 Detlef Stern
// 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"
)

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

Changes to box/notify/notify.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"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"
	"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(),
		domain.Zettel{
		zettel.Zettel{
			Meta:    m,
			Content: domain.NewContent(inp.Src[inp.Pos:]),
			Content: zettel.NewContent(inp.Src[inp.Pos:]),
		},
		m.GetDefault(api.KeySyntax, meta.SyntaxZmk),
		m.GetDefault(api.KeySyntax, meta.DefaultSyntax),
		nil,
	)
	encdr := encoder.Create(api.Encoder(enc))
	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
1

2
3
4
5
6
7
8
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
// 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"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
	"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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter/api"
	"zettelstore.de/z/web/adapter/webui"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

// ---------- Subcommand: run ------------------------------------------------

func flgRun(fs *flag.FlagSet) {
	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
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, authManager, boxManager)
	ucAuthenticate := usecase.NewAuthenticate(logAuth, authManager, &ucGetUser)
	ucIsAuth := usecase.NewIsAuthenticated(logUc, &getUser, authManager)
	ucCreateZettel := usecase.NewCreateZettel(logUc, rtConfig, protectedBoxManager)
	ucGetMeta := usecase.NewGetMeta(protectedBoxManager)
	ucGetAllZettel := usecase.NewGetAllZettel(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)
	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)
	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)
	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.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.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(
		webSrv.AddZettelRoute('d', server.MethodGet, wui.MakeGetDeleteZettelHandler(ucGetZettel, ucGetAllZettel))
			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('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, ucGetMeta, ucGetAllMeta, ucUnlinkedRefs))
		ucParseZettel, &ucEvaluate, ucGetZettel, ucGetAllZettel, &ucQuery))
	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))
	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))
		webSrv.AddZettelRoute('z', server.MethodMove, a.MakeRenameZettelHandler(&ucRename))
	}

	if authManager.WithAuth() {
		webSrv.SetUserRetriever(usecase.NewGetUserByZid(boxManager))
	}
}

type getUserImpl struct{}

func (*getUserImpl) GetUser(ctx context.Context) *meta.Meta { return server.GetUser(ctx) }

Changes to cmd/command.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

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

	"zettelstore.de/c/maps"
	"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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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"

	"zettelstore.de/c/api"
	"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/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

const strRunSimple = "run-simple"

func init() {
	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
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) (cfg *meta.Meta) {
func fetchStartupConfiguration(fs *flag.FlagSet) (string, *meta.Meta) {
	if configFlag := fs.Lookup("c"); configFlag != nil {
		if filename := configFlag.Value.String(); filename != "" {
			content, err := readConfiguration(filename)
			return createConfiguration(content, err)
			return filename, createConfiguration(content, err)
		}
	}
	content, err := searchAndReadConfiguration()
	return createConfiguration(content, err)
	filename, content, err := searchAndReadConfiguration()
	return filename, createConfiguration(content, err)
}

func createConfiguration(content []byte, err error) *meta.Meta {
	if err != nil {
		return meta.New(id.Invalid)
	}
	return meta.NewFromInput(id.Invalid, input.NewInput(content))
}

func readConfiguration(filename string) ([]byte, error) { return os.ReadFile(filename) }

func searchAndReadConfiguration() ([]byte, error) {
	for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg"} {
func searchAndReadConfiguration() (string, []byte, error) {
	for _, filename := range []string{"zettelstore.cfg", "zsconfig.txt", "zscfg.txt", "_zscfg", ".zscfg"} {
		if content, err := readConfiguration(filename); err == nil {
			return content, nil
			return filename, content, nil
		}
	}
	return readConfiguration(".zscfg")
	return "", nil, os.ErrNotExist
}

func getConfig(fs *flag.FlagSet) *meta.Meta {
	cfg := fetchStartupConfiguration(fs)
func getConfig(fs *flag.FlagSet) (string, *meta.Meta) {
	filename, cfg := fetchStartupConfiguration(fs)
	fs.Visit(func(flg *flag.Flag) {
		switch flg.Name {
		case "p":
			cfg.Set(keyListenAddr, net.JoinHostPort("127.0.0.1", flg.Value.String()))
		case "a":
			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
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 cfg
	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"
	keyBoxOneURI         = kernel.BoxURIs + "1"
	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
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 = setConfigValue(err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))
		err, kernel.ConfigService, kernel.ConfigInsecureHTML, cfg.GetDefault(keyInsecureHTML, kernel.ConfigSecureHTML))

	err = setConfigValue(
	err = setConfigValue(err, kernel.WebService, kernel.WebListenAddress, cfg.GetDefault(keyListenAddr, "127.0.0.1:23123"))
		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().Fatal().Str("key", key).Str("value", fmt.Sprint(val)).Err(err).Msg("Unable to set configuration")
			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
	}
	cfg := getConfig(fs)
	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
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)
	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 {
	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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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/sexprenc"  // Allow to use sexpr 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/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.
)

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
1

2
3
4
5
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
// 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 = ""
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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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
1

2
3
4
5
6
7
8
9
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
// 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/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// Key values that are supported by Config.Get
const (
	KeyFooterZettel = "footer-zettel"
	KeyHomeZettel   = "home-zettel"
	KeyFooterZettel       = "footer-zettel"
	KeyHomeZettel         = "home-zettel"
	KeyShowBackLinks      = "show-back-links"
	KeyShowFolgeLinks     = "show-folge-links"
	KeyShowSequelLinks    = "show-sequel-links"
	KeyShowSuccessorLinks = "show-successor-links"
	// 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,

Changes to docs/development/00010000000000.zettel.

1
2
3
4
5
6

7
8
9
10

1
2
3
4
5

6
7
8
9
10
11





-
+




+
id: 00010000000000
title: Developments Notes
role: zettel
syntax: zmk
created: 00010101000000
modified: 20221026184905
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











1
2
3
4
5

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: 20230109121417
modified: 20241213124936

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

The internal build tool needs the following software tools.
They 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
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
1
2
3
4
5

6
7

8
9

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




+
-
+

-
+

-
+

-
+


-
+

-
+

-
+




-
+



-
+

-
+


-
-
-
-
-
+
+
+
+
+

-
+





-
+

-
+

-
+



-
+






id: 20210916194900
title: Checklist for Release
role: zettel
syntax: zmk
created: 20210916194900
modified: 20220309105459
modified: 20241213125640

# Sync with the official repository
# Sync with the official repository:
#* ``fossil sync -u``
# Make sure that there is no workspace defined.
# 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.
# 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``).
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``)
# All internal tests must succeed:
#* ``go run tools/build.go relcheck`` (alternatively: ``make relcheck``).
#* ``go run tools/check/check.go -r`` (alternatively: ``make relcheck``)
# The API tests must succeed on every development platform:
#* ``go run tools/build.go testapi`` (alternatively: ``make api``).
#* ``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''.
#* Check all ""Error: 403 Forbidden"": allowed for endpoint ''/z'' 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
# On every development platform, the box with 10.000 zettel must run, with ''-race'' enabled:
#* ``go run -race cmd/zettelstore/main.go run -d DIR``.
#* ``go run -race cmd/zettelstore/main.go run -d DIR``
# Create a development release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
#* ``go run tools/build.go release`` (alternatively: ``make release``)
# On every platform (esp. macOS), the box with 10.000 zettel must run properly:
#* ``./zettelstore -d DIR``
# Update files in directory ''www''
#* index.wiki
#* download.wiki
#* changes.wiki
#* plan.wiki
# Update files in directory ''www'':
#* ''index.wiki''
#* ''download.wiki''
#* ''changes.wiki''
#* ''plan.wiki''
# Set file ''VERSION'' to the new release version.
  It _must_ consist of three digits: MAJOR.MINOR.PATCH, even if PATCH is zero
  It **must** consists of three numbers: ''MAJOR.MINOR.PATCH'', even if ''PATCH'' is zero.
# 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''.
   Otherwise client software will not be able to import ''zettelstore.de/z''.
# Clean up your Go workspace:
#* ``go run tools/build.go clean`` (alternatively: ``make clean``).
#* ``go run tools/clean/clean.go`` (alternatively: ``make clean``)
# Create the release:
#* ``go run tools/build.go release`` (alternatively: ``make release``).
#* ``go run tools/build/build.go release`` (alternatively: ``make release``)
# Remove previous executables:
#* ``fossil uv remove --glob '*-PREVVERSION*'``
# Add executables for release:
#* ``cd 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
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>
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
1
2
3
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: 20220803183647
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
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]] is
[[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.
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
It's done by most individuals, not necessarily as part of their main business.
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
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.
do not need to think for a living. Personal knowledge management can be seen as
a prerequisite for many kinds of collaboration.
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"")
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]]"".
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
The method is based on creating many individual notes, each with one idea or piece of information, that is related to each other.
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
Since knowledge is typically built up gradually, one major focus is a long-term store of these notes, hence the name ""Zettelstore"".
""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
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: 20221018105415
modified: 20230624171152

Zettelstore supports the following design goals:

; Longevity of stored notes / zettel
: Every zettel you create should be readable without the help of any tool, even without Zettelstore.
: It should be not hard to write other software that works with your zettel.
: Normal zettel should be stored in a single file.
  If this is not possible: at most in two files: one for the metadata, one for the content.
  The only exception are [[predefined zettel|00001005090000]] stored in the Zettelstore executable.
: There is no additional database.
; Single user
: All zettel belong to you, only to you.
  Zettelstore provides its services only to one person: you.
  If the computer running Zettelstore is securely configured, there should be no risk that others are able to read or update your zettel.
: If you want, you can customize Zettelstore in a way that some specific or all persons are able to read some of your zettel.
; 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


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


13
14
15
16
17
18
19
1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16
17
18
19
20
21





+
-
+





-
+
+







id: 00001003000000
title: Installation of the Zettelstore software
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220119145756
modified: 20241213101917

=== 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.]
* 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 a problem, please take a look on the [[Troubleshooting|00001018000000]] page.]
* A sub-directory ""zettel"" will be created in the directory where you put the executable.
  It will contain your future zettel.
* Open the URI [[http://localhost:23123]] with your web browser.
  It will present you a mostly empty Zettelstore.
  There will be a zettel titled ""[[Home|00010000000000]]"" that contains some helpful information.
* Please read the instructions for the [[web-based user interface|00001014000000]] and learn about the various ways to write zettel.
* If you restart your device, please make sure to start your Zettelstore again.

Changes to docs/manual/00001003300000.zettel.

1
2
3
4
5

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

7
8
9
10
11
12
13
14





+
-
+







id: 00001003305000
title: Enable Zettelstore to start automatically on Windows
role: manual
tags: #installation #manual #zettelstore
syntax: zmk
created: 20211125191727
modified: 20220218125541
modified: 20241213103259

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]].
Open the folder where you have placed in the Explorer.
30
31
32
33
34
35
36
37

38
39
40
41
42
43
44
31
32
33
34
35
36
37

38
39
40
41
42
43
44
45







-
+








=== Task scheduler

The Windows Task scheduler allows you to start Zettelstore as an background task.

This is both an advantage and a disadvantage.

On the plus side, Zettelstore runs in the background, and it does not disturbs you.
On the plus side, Zettelstore runs in the background, and it does not disturb you.
All you have to do is to open your web browser, enter the appropriate URL, and there you go.

On the negative side, you will not be notified when you enter the wrong data in the Task scheduler and Zettelstore fails to start.
This can be mitigated by first using the command line prompt to start Zettelstore with the appropriate options.
Once everything works, you can register Zettelstore to be automatically started by the task scheduler.
There you should make sure that you have followed the first steps as described on the [[parent page|00001003300000]].

67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
68
69
70
71
72
73
74

75
76
77
78
79
80
81
82







-
+








Create a new action.

{{00001003305112}}

The next steps are the trickiest.

If you did not created a startup configuration file, then create an action that starts a program.
If you did not create a startup configuration file, then create an action that starts a program.
Enter the file path where you placed the Zettelstore executable.
The ""Browse ..."" button helps you with that.[^I store my Zettelstore executable in the sub-directory ''bin'' of my home directory.]

It is essential that you also enter a directory, which serves as the environment for your zettelstore.
The (sub-) directory ''zettel'', which will contain your zettel, will be placed in this directory.
If you leave the field ""Start in (optional)"" empty, the directory will be an internal Windows system directory (most likely: ''C:\\Windows\\System32'').

Changes to docs/manual/00001003310000.zettel.

1
2
3
4
5

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

7
8



9
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: 20221128155143
modified: 20240926144803

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.
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.
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.
  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 note to users.
  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.
  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]].]
  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, then the contents of the directory are listed to the user.
  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 counted up, starting with one, until no key is found.
  This allows to configure more than one box.
  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'']
: Allows to debug the Zettelstore software (mostly used by the developers) if set to [[true|00001006030500]]
: 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]].
: Specifies the default value for the (sub-)type of [[directory boxes|00001004011400#type]], in which Zettel are typically stored.
  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.
: 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.
  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.
  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 specification consists of an optional service name, together with the colon character (""'':''"", U+003A), followed by the logging level.
  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).
  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.
  ""error;web:debug"" will emit debugging messages for the web component of Zettelstore while still producing error messages for all other components.

  When you are familiar to operate the Zettelstore, you might set the level to ""warn"" or ""error"" to receive less noisy messages from the Zettelstore.
  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'']
: 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.
: 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 owner is set to some value, user [[authentication|00001010000000]] is enabled.
  Only if set to some value, user [[authentication|00001010000000]] is enabled.

  Ensure that key [[''secret''|#secret]] is set to a value of at least 16 bytes.
  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.
  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.
  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 (see option ''token-lifetime-html'') by 30 seconds.
  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'']
: Puts the Zettelstore service into a read-only mode, if set to a [[true value|00001006030500]].
: 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 modified by some external unfriendly party.
: 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.

  It is only needed to set this value, if [[authentication is enabled|00001010040100]] by setting key [[''owner''|#owner]] to some user identification.
  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.
  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).
  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
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
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: 20220724200512
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

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]] [[mode|00001004051000]], all configured file directory boxes will be in read-only mode too, even if not explicitly configured.
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
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
1
2
3
4
5
6

7
8
9
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: 20221219175720
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 JSON, etc. is not affected.
  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]].
; [!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"".
; [!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




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: 20221212195530
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
|[[''marker-external''|00001004020000#marker-external]]|Y|Y|
|[[''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
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
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
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
1
2
3
4
5
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
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]],
  [[''sexpr''|00001012920516]],
  [[''sz''|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.

Changes to docs/manual/00001004051400.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
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

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: 20220113183606
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
|= 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.
| Info  | 3 | 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.
| 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.
| 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
| 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 ""warn"", no ""trace"", ""debug"", ""sense", and ""info"" messages are shown, but ""warn"", ""error"", ""fatal"", ""panic"", and ""mandatory"" messages.
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
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
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
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
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
1
2
3
4
5
6

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





+
-
+













-
+














-
+















-
+







id: 00001005000000
title: Structure of Zettelstore
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220104213511
modified: 20241213101751

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.
Via the builtin [[web user interface|00001014000000]] you can work with zettel in various ways.
For example, you are able to list zettel, to create new zettel, to edit them, or to delete them.
You can view zettel details and relations between zettel.

In addition, Zettelstore provides an ""application programming interface"" ([[API|00001012000000]]) that allows other software to communicate with the Zettelstore.
Zettelstore becomes extensible by external software.
For example, a more sophisticated user interface could be build, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel.
For example, a more sophisticated user interface could be built, or an application for your mobile device that allows you to send content to your Zettelstore as new zettel.

=== Where zettel are stored

Your zettel are stored typically as files in a specific directory.
If you have not explicitly specified the directory, a default directory will be used.
The directory has to be specified at [[startup time|00001004010000]].
Nested directories are not supported (yet).

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.
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;
  in this case, the filename just the 14 digits of the zettel identifier, and optional characters except the period ''"."''.  
Other filename extensions are used to determine the ""syntax"" of a zettel.
This allows to use other content within the Zettelstore, e.g. images or HTML templates.

For example, you want to store an important figure in the Zettelstore that is encoded as a ''.png'' file.
Since each zettel contains some metadata, e.g. the title of the figure, the question arises where these data should be stores.
The solution is a meta-file with the same zettel identifier, but without a filename extension.
Zettelstore recognizes this situation and reads in both files for the one zettel containing the figure.
It maintains this relationship as long as theses files exists.
It maintains this relationship as long as these files exists.

In case of some textual zettel content you do not want to store the metadata and the zettel content in two different files.
Here the ''.zettel'' extension will signal that the metadata and the zettel content will be put in the same file, separated by an empty line or a line with three dashes (""''-\-\-''"", also known as ""YAML separator"").

=== Predefined zettel

Zettelstore contains some [[predefined zettel|00001005090000]] to work properly.
69
70
71
72
73
74
75
76

77
78
79
80
81
82
83
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 either have to delete the zettel (which removes it from the file directory), or you have to rename it to another zettel identifier.
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
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

36
37
38
39
40
41

42
43
44
45
46


47
48
49
50
51
52
53
54
55
56
57
58






-
+


+
+
+
+









+
+











-

+
+
+


-

+
+
+
+
-
-
+
+

+
+
+



+


id: 00001005090000
title: List of predefined zettel
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220909180240
modified: 20241202102358

The following table lists all predefined zettel with their purpose.

The content of most[^To be more exact: zettel with an identifier greater or equal ''00000999999900'' will have their content indexed.] of these zettel will not be indexed by Zettelstore.
You will not find zettel when searched for some content, e.g. ""[[query:european]]"" will not find the [[Zettelstore License|00000000000004]].
However, metadata is always indexed, e.g. ""[[query:title:license]]"" will find the Zettelstore License zettel.

|= 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]]
| [[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
| [[00000000010700]] | Zettelstore Error HTML Template | View to show an error message
| [[00000000019000]] | Zettelstore Sxn Start Code | Starting point of sxn functions to build the templates
| [[00000000019990]] | Zettelstore Sxn Base Code | Base sxn functions to build the templates
| [[00000000020001]] | Zettelstore Base CSS | System-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000025001]] | Zettelstore User CSS | User-defined CSS file that is included by the [[Base HTML Template|00000000010100]]
| [[00000000029000]] | Zettelstore Role to CSS Map | [[Maps|00001017000000#role-css]] [[role|00001006020000#role]] to a zettel identifier that is included by the [[Base HTML Template|00000000010100]] as an CSS file 
| [[00000000040001]] | Generic Emoji | Image that is shown if [[original image reference|00001007040322]] is invalid
| [[00000000060010]] | zettel | [[Role zettel|00001012051800]] for the role ""[[zettel|00001006020100#zettel]]""
| [[00000000060020]] | confguration | [[Role zettel|00001012051800]] for the role ""[[confguration|00001006020100#confguration]]""
| [[00000000060030]] | role | [[Role zettel|00001012051800]] for the role ""[[role|00001006020100#role]]""
| [[00000000060040]] | tag | [[Role zettel|00001012051800]] for the role ""[[tag|00001006020100#tag]]""
| [[00000000090000]] | New Menu | Contains items that should contain in the zettel template menu
| [[00000000090001]] | New Zettel | Template for a new zettel with role ""[[zettel|00001006020100]]""
| [[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]]
| [[00000999999999]] | 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
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
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
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]]"".
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 additional query parameter.
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
1
2
3
4
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: 20220218131923
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.
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
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
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
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
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.

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

7
8
9
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: 20220623183234
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 for the Zettelstore.
: 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|''zettel'']
: A real zettel that contains your own thoughts.
; ''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
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
1
2
3
4
5
6

7
8
9
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: 20220304114106
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]]
| ''-set'' | [[WordSet|00001006036000]]
| ''-time'' | [[Timestamp|00001006034500]]
| ''-title'' | [[Zettelmarkup|00001006036500]]
| ''-url'' | [[URL|00001006035000]]
| ''-zettel''  | [[Identifier|00001006032000]]
| ''-zid''  | [[Identifier|00001006032000]]
| ''-zids''  | [[IdentifierSet|00001006032500]]
| any other suffix | [[EString|00001006031500]]

The name of the metadata key is bound to the key type

Every key type has an associated validation rule to check values of the given type.
There is also a rule how values are matched, e.g. against a search term when selecting some zettel.
There is also a rule how values are matched, e.g. against a [[search value|00001007706000]] when selecting some zettel.
And there is a rule how values compare for sorting.

* [[Credential|00001006031000]]
* [[EString|00001006031500]]
* [[Identifier|00001006032000]]
* [[IdentifierSet|00001006032500]]
* [[Number|00001006033000]]
* [[String|00001006033500]]
* [[TagSet|00001006034000]]
* [[Timestamp|00001006034500]]
* [[URL|00001006035000]]
* [[Word|00001006035500]]
* [[WordSet|00001006036000]]
* [[Zettelmarkup|00001006036500]]

Changes to docs/manual/00001006030500.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
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
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
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 operators
=== 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
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
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 operator
=== 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
1
2
3
4
5
6

7
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: 20220914134914
modified: 20230612183459

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.
=== 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
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
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 operator
=== 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
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: 20220914131211
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 operator
All comparisons are done on the given string representation of the number, ""+12"" will be treated as a different number of ""12"".
=== 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
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
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 operator
=== 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
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
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 operator
=== 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
1
2
3
4
5
6

7
8
9
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: 20220914130919
modified: 20231030182858

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"".
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 operator
All comparisons assume that up to 14 digits are given.
=== 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 [[String|00001006033500]] values.
Sorting is done by comparing the possibly expanded values.

If both values are timestamp values, this works well because both have the same length.

Changes to docs/manual/00001006035000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14

15
16
17
18
19
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
modified: 20230419175725

Values of this type denote an URL.

=== Allowed values
All characters of an URL / URI are allowed.

=== Query operator
=== 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
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
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 operator
=== 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
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
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 operator
=== 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
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: 20210721123222
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
1
2
3
4
5
6

7
8
9

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: 00001006055000
title: Reserved zettel identifier
role: manual
tags: #design #manual #zettelstore
syntax: zmk
created: 20210721105704
modified: 20220311111751
modified: 20241202100917

[[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.
By renaming the name of the underlying zettel file, 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'').
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 a range of specific zettel identifier to work properly.
However, some external applications may need at least one defined 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
Zettel [[Zettelstore Application Directory|00000999999999]] (''00000999999999'') can be used to associate a name to a zettel identifier.
; Description
: A brief description what the application is used for and why you need to reserve some zettel identifier
For example, if your application is named ""app"", you create a metadata key ''app-zid''.
; Number
: Specify the amount of zettel identifier you are planning to use.
Its value is the zettel identifier of the zettel that configures your application.
  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
| 00000000000000 | 00000000000000 | This is an invalid zettel identifier
| 00000000000001 | 00000999999999 | [[Predefined zettel|00001005090000]]
| 00001000000000 | 00001099999999 | This [[Zettelstore manual|00001000000000]]
| 00001100000000 | 00008999999999 | Reserved, do not use
| 00009000000000 | 00009999999999 | 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

Changes to docs/manual/00001007000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23






-
+








-
+







id: 00001007000000
title: Zettelmarkup
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221209192105
modified: 20241212152823

Zettelmarkup is a rich plain-text based markup language for writing zettel content.
Besides the zettel content, Zettelmarkup is also used for specifying the title of a zettel, regardless of the syntax of a zettel.

Zettelmarkup supports the longevity of stored notes by providing a syntax that any person can easily read, as well as a computer.
Zettelmarkup can be much easier parsed / consumed by a software compared to other markup languages.
Writing a parser for [[Markdown|https://daringfireball.net/projects/markdown/syntax]] is quite challenging.
[[CommonMark|00001008010500]] is an attempt to make it simpler by providing a comprehensive specification, combined with an extra chapter to give hints for the implementation.
Zettelmarkup follows some simple principles that anybody who knows to ho write software should be able understand to create an implementation.
Zettelmarkup follows some simple principles that anybody who knows how ho write software should be able understand to create an implementation.

Zettelmarkup is a markup language on its own.
This is in contrast to Markdown, which is basically a super-set of HTML: every HTML document is a valid Markdown document.[^To be precise: the content of the ``<body>`` of each HTML document is a valid Markdown document.]
While HTML is a markup language that will probably last for a long time, it cannot be easily translated to other formats, such as PDF, JSON, or LaTeX.
Additionally, it is allowed to embed other languages into HTML, such as CSS or even JavaScript.
This could create problems with longevity as well as security problems.

Changes to docs/manual/00001007010000.zettel.

1
2
3
4
5

6

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

7
8
9
10
11
12
13
14





+
-
+







id: 00001007010000
title: Zettelmarkup: General Principles
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124175047
modified: 20241213101524

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.
List blocks also begin at the first position of a line, but may need one or more identical character, plus a space character.
43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60
61







-
+











Many block and inline elements can be refined by additional [[attributes|00001007050000]].
Attributes resemble roughly HTML attributes and are put near the corresponding elements by using the syntax ``{...}``{=zmk}.
One example is to make space characters visible inside a inline literal element: ``1 + 2 = 3``{-} was specified by using the default attribute: ``\`\`1 + 2 = 3\`\`{-}``.

To summarize:

* With some exceptions, blocks-structural elements begins at the for position of a line with three identical characters.
* With some exceptions, block-structural elements begins at the for position of a line with three identical characters.
* The most important exception to this rule is the specification of lists.
* If no block element is found, a paragraph with inline elements is assumed.
* With some exceptions, inline-structural elements begins with two characters, quite often the same two characters.
* The most important exceptions are links.
* The backslash character can help to resolve possible ambiguities.
* Attributes refine some block and inline elements.
* Block elements have a higher priority than inline elements.

These principles makes automatic recognizing zettelmarkup an (relatively) easy task.
By looking at the reference implementation, a moderately skilled software developer should be able to create a appropriate software in a different programming language.

Changes to docs/manual/00001007020000.zettel.

1
2
3
4
5

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

7
8
9
10
11
12
13
14





+
-
+







id: 00001007030000
title: Zettelmarkup: Block-Structured Elements
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220311181036
modified: 20241212153023

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

61
62
63
64
65
66
67
68

69
70
71
72
73
74
62
63
64
65
66
67
68

69
70
71
72
73
74
75







-
+






:::example
= Heading
Some text follows.
:::
This is because headings need at least three equal sign character.

A paragraph is essentially a sequence of [[inline-structured elements|00001007040000]].
Inline-structured elements cam span more than one line.
Inline-structured elements can span more than one line.
Paragraphs are separated by empty lines.

If you want to specify a second paragraph inside a list item, or if you want to continue a paragraph on a second and more line within a list item, you must begin the paragraph with a certain number of space characters.
The number of space characters depends on the kind of a list and the relevant nesting level.

A line that begins with a space character and which is outside of a list or does not contain the right number of space characters is considered to be part of a paragraph.

Changes to docs/manual/00001007030100.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
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
13
14
15
16
17
18
19
20


21
22
23
24
25
26
27
1
2
3
4
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: 00001007030200
title: Zettelmarkup: Nested Lists
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218133902
modified: 20241213121000

There are thee kinds of lists that can be nested: ordered lists, unordered lists, and quotation lists.
There are three 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).
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 sign (""''>''"", U+003E).
Let's call these three characters __list characters__.

Any nested list item is specified by a non-empty sequence of list characters, followed by a space character and a sequence of [[inline elements|00001007040000]].
In case of a quotation list as the last list character, the space character followed by a sequence of inline elements is optional.
The number / count of list characters gives the nesting of the lists.
If the following lines should also be part of the list item, exactly the same number of spaces must be given at the beginning of each of the following lines as it is the lists are nested, plus one additional space character.
In other words: the inline elements must begin at the same column as it was on the previous line.

The resulting sequence on inline elements is merged into a paragraph.
Appropriately indented paragraphs can specified after the first one.
The resulting sequence of inline elements is merged into a paragraph.
Appropriately indented paragraphs can be specified after the first one.
Since each blocks-structured element has to be specified at the first position of a line, none of the nested list items may contain anything else than paragraphs.

Some examples:
```zmk
# One
# Two
# Three

Changes to docs/manual/00001007030300.zettel.

1
2
3
4
5

6
7
8
9
10
11
12
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
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
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
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
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
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: 20220218130330
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 JSON, the comment block will not be ignored but it will output some JSON text.
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
13
14
15
16
17
18
1
2
3
4
5
6

7
8
9

10
11

12
13
14
15
16
17
18
19





+
-
+


-
+

-
+







id: 00001007031000
title: Zettelmarkup: Tables
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218131107
modified: 20241212153641

Tables are used to show some data in a two-dimensional fashion.
In zettelmarkup, table are not specified explicitly, but by entering __table rows__.
In zettelmarkup, tables 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__.
A table row is nothing but 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.

The first cell of a row must begin with the vertical bar character (""''|''"", U+007C) at the first position of a line.
The other cells of a row begin with the same vertical bar character at later positions in that line.
A cell is delimited by the vertical bar character of the next cell or by the end of the current line.
A vertical bar character as the last character of a line will not result in a table cell.
It will be ignored.

Changes to docs/manual/00001007031140.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
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
modified: 20241213153229

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







-
-
-
-
-
-
-
-
-


+
+
+
-
+


+
+
+









: The resulting list will be a numbered list.
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''TITLE'' (parameter)
: All words following ''TITLE'' are joined together to form a title.
  It is used for the ''ATOM'' and ''RSS'' action.
; ''ATOM'' (aggregate)
: Transform the zettel list into an [[Atom 1.0|https://www.rfc-editor.org/rfc/rfc4287]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''RSS'' (aggregate)
: Transform the zettel list into a [[RSS 2.0|https://www.rssboard.org/rss-specification]]-conformant document / feed.
  The document is embedded into the referencing zettel.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''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]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
; 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
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]].
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
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]].
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
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: 20220218131003
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
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
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
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/00001007040322.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28



1
2
3
4
5
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: 00001007040322
title: Zettelmarkup: Image Embedding
role: manual
tags: #manual #zettelmarkup #zettelstore
syntax: zmk
created: 20210811154251
modified: 20221112111054
modified: 20241202101206

Image content is assumed, if an URL is used or if the referenced zettel contains an image.

Supported formats are:

* Portable Network Graphics (""PNG""), as defined by [[RFC\ 2083|https://tools.ietf.org/html/rfc2083]].
* Graphics Interchange Format (""GIF"), as defined by [[https://www.w3.org/Graphics/GIF/spec-gif89a.txt]].
* JPEG / JPG, defined by the __Joint Photographic Experts Group__.
* Scalable Vector Graphics (SVG), defined by [[https://www.w3.org/Graphics/SVG/]]
* WebP, defined by [[Google|https://developers.google.com/speed/webp]]

If the text is given, it will be interpreted as an alternative textual representation, to help persons with some visual disabilities.

[[Attributes|00001007050000]] are supported.
They must follow the last right curly bracket character immediately.
One prominent example is to specify an explicit title attribute that is shown on certain web browsers when the zettel is rendered in HTML:

Examples:
* [!spin|``{{Spinning Emoji|00000000040001}}{title=Emoji width=30}``] is rendered as ::{{Spinning Emoji|00000000040001}}{title=Emoji width=30}::{=example}.
* The above image is also the placeholder for a non-existent zettel:
** ``{{00000000009999}}`` will be rendered as ::{{00000000009999}}::{=example}.
* The above image is also the placeholder for a non-existing invalid zettel or for using an invalid zettel identifier:
** ``{{99999999999999}}`` will be rendered as ::{{99999999999999}}::{=example}.
** ``{{00000000000000}}`` will be rendered as ::{{00000000000000}}::{=example}.

Changes to docs/manual/00001007040324.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
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
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
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}
  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
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
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
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
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
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
1

2
3
4
5
6

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
title: Query Expression
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20220805150154
modified: 20220913135434
modified: 20230731161954

A query expression allows you to search for specific zettel and to perform some actions on them.
You may select zettel based on a full-text search, based on specific metadata values, or both.
You may select zettel based on a list of [[zettel identifier|00001006050000]], based on a query directive, based on a full-text search, based on specific metadata values, or some or all of them.

A query expression consists of a __search expression__ and of an optional __action list__.
Both are separated by a vertical bar character (""''|''"", U+007C).
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]].

=== Search expression

* [[List of zettel identifier|00001007710000]]
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"".

* [[Query directives|00001007720000]]
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"".

** [[Context directive|00001007720300]]
** [[Ident directive|00001007720600]]
** [[Items directive|00001007720900]]
** [[Unlinked directive|00001007721200]]
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]]
* [[Search expression|00001007701000]]
** [[Search term|00001007702000]]
** [[Search operator|00001007705000]]
** [[Search value|00001007706000]]
* [[Action list|00001007770000]]

Here are [[some examples|00001007790000]] of search expressions, which can be used to manage a Zettelstore:
Here are [[some examples|00001007790000]], 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]].

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
1
2
3
4
5
6

7
8
9
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: 20220821163727
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 REVERSED published'' will return a reversed result order.
  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


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 ''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"".
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
1
2
3
4
5
6

7
8
9
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: 20220819194709
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 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"")
* 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 10 possible combinations:
Since the exclamation mark character can be combined with the other, there are 18 possible combinations:
# ""''!''"": is an abbreviation of the ""''!~''"" operator.
# ""''~''"": is successful if the search value matched the value to be compared.
# ""''!~''"": is successful if the search value does not match the value to be compared.
# ""'':''"": is successful if the search value is equal to one word of the value to be compared.
# ""''!:''"": is successful if the search value is not equal to any word of the value to be compared.
# ""''>''"": is successful if the search value is a prefix of the value to be compared.
# ""''!>''"": is successful if the search value is not a prefix of the value to be compared.
# ""''<''"": is successful if the search value is a suffix of the value to be compared.
# ""''!<''"": is successful if the search value is not a suffix of the value to be compared.
# ""''=''"": is successful if the search value is 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
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: 200).

If no ''BACKWARD'' and ''FORWARD'' is specified, a search for context zettel will be done though backward and forward links.

The cost of a context zettel is calculated iteratively:
* Each of the specified zettel hast a cost of one.
* A zettel found as a single folge zettel or single precursor zettel has the cost of the originating zettel, plus 0.1.
* A zettel found as a single sequel zettel or single prequel zettel has the cost of the originating zettel, plus 1.0.
* A zettel found as a single successor zettel or single predecessor zettel has the cost of the originating zettel, plus seven.
* A zettel found via another link without being part of a [[set of zettel identifier|00001006032500]], has the cost of the originating zettel, plus two.
* A zettel which is part of a set of zettel identifier, has the cost of the originating zettel, plus one of the four choices above and multiplied with roughly a linear-logarithmic value based on the size of the set.
* A zettel with the same tag, has the cost of the originating zettel, plus a linear-logarithmic number based on the number of zettel with this tag.
  If a zettel belongs to more than one tag compared with the current zettel, there is a discount of 90% per additional tag.
  This only applies if the ''FULL'' directive was specified.

The maximum cost is only checked for all zettel that are not directly reachable from the initial, specified list of zettel.
This ensures that initial zettel that have only a highly used tag, will also produce some context zettel.

Despite its possibly complicated structure, this algorithm ensures in practice that the zettel context is a list of zettel, where the first elements are ""near"" to the specified zettel and the last elements are more ""distant"" to the specified zettel.
It also penalties zettel that acts as a ""hub"" to other zettel, to make it more likely that only relevant zettel appear on the context list.

This directive may be specified only once as a query directive.
A second occurence of ''CONTEXT'' is interpreted as a [[search expression|00001007701000]].
In most cases it is easier to adjust the maximum cost than to perform another context search, which is relatively expensive in terms of retrieving effort.

Added docs/manual/00001007720600.zettel.













1
2
3
4
5
6
7
8
9
10
11
12
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007720600
title: Query: Ident Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230724153018
modified: 20230724154015

An ident directive is needed if you want to specify just a list of zettel identifier.
It starts with / consists of the keyword ''IDENT''.

When not using the ident directive, zettel identifier are interpreted as a [[search expression|00001007701000]].

Added docs/manual/00001007720900.zettel.








































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007720900
title: Query: Items Directive
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230729102142
modified: 20230729120755

The items directive works on zettel that act as a ""table of contents"" for other zettel.
The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another.
Every zettel with a certain internal structure can act as the ""table of contents"" for others.

What is a ""table of contents""?
Basically, it is just a list of references to other zettel.

To retrieve the items of a zettel, the software looks at first level [[list items|00001007030200]].
If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the items list, in the ""table of contents"".

This applies only to first level list items (ordered or unordered list), but not to deeper levels.
Only the first reference to a valid zettel is collected for the table of contents.
Following references to zettel within such an list item are ignored.


````
# curl 'http://127.0.0.1:23123/z?q=00001000000000+ITEMS'
00001001000000 Introduction to the Zettelstore
00001002000000 Design goals for the Zettelstore
00001003000000 Installation of the Zettelstore software
00001004000000 Configuration of Zettelstore
00001005000000 Structure of Zettelstore
00001006000000 Layout of a Zettel
00001007000000 Zettelmarkup
00001008000000 Other Markup Languages
00001010000000 Security
00001012000000 API
00001014000000 Web user interface
00001017000000 Tips and Tricks
00001018000000 Troubleshooting
````

Added docs/manual/00001007721200.zettel.













































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007721200
title: Query: Unlinked Directive
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20230928190540

The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.

__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel.

To retrieve unlinked references to an existing zettel, use the query ''{ID} UNLINKED''.

````
# curl 'http://127.0.0.1:23123/z?q=00001012000000+UNLINKED'
00001012921200 API: Encoding of Zettel Access Rights
````

This returns all zettel (in this case: only one) that references the title of the given Zettel, but does not references it directly.

In addition you may add __phrases__ if you do not want to scan for the title of the given zettel.

```
# curl 'http://localhost:23123/z?q=00001012054400+UNLINKED+PHRASE+API'
00001012050600 API: Provide an access token
00001012921200 API: Encoding of Zettel Access Rights
00001012080200 API: Check for authentication
00001012080500 API: Refresh internal data
00001012050200 API: Authenticate a client
00001010040700 Access token
```

This finds all zettel that does contain the phrase ""API"" but does not directly reference the given zettel.

The directive searches within all zettel whether the title of the specified zettel occurs there.
The other zettel must not link to the specified zettel.
The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting.
The match must be exact, but is case-insensitive.
For example ""API"" does not match ""API:"".

Added docs/manual/00001007770000.zettel.






















1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001007770000
title: Query: Action List
role: manual
tags: #manual #search #zettelstore
syntax: zmk
created: 20230707205246
modified: 20240219161813

With a [[list of zettel identifier|00001007710000]], a [[query directives|00001007720000]], or a [[search expression|00001007701000]], a list of zettel is selected.
__Actions__ allow to modify this list to a certain degree.

Which actions are allowed depends on the context.
However, actions are further separated into __parameter action__ and __aggregate actions__.
A parameter action just sets a parameter for an aggregate action.
An aggregate action transforms the list of selected zettel into a different, aggregate form.
Only the first aggregate form is executed, following aggregate actions are ignored.

In most contexts, valid actions include the name of metadata keys, at least of type [[Word|00001006035500]] or [[TagSet|00001006034000]].

To allow some kind of backward compatibility, an action written in uppercase letters that leads to an empty result list, will be ignored.
In this case the list of selected zettel is returned.

Changes to docs/manual/00001007780000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29












































30
1
2
3
4
5
6

7
8
9




















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






-
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

id: 00001007780000
title: Formal syntax of query expressions
role: manual
tags: #manual #reference #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20220913134024
modified: 20241213153200

```
QueryExpression  := SearchExpression ActionExpression?
SearchExpression := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm       := SearchOperator? SearchValue
                  | SearchKey SearchOperator SearchValue?
                  | SearchKey ExistOperator
                  | "OR"
                  | "RANDOM"
                  | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey
                  | "OFFSET" SPACE+ PosInt
                  | "LIMIT" SPACE+ PosInt.
SearchValue      := Word.
SearchKey        := MetadataKey.
SearchOperator   := '!'
                  | ('!')? ('~' | ':' | '<' | '>').
ExistOperator    := '?'
                  | '!' '?'.
PosInt           := '0'
                  | ('1' .. '9') DIGIT*.
ActionExpression := '|' (Word (SPACE+ Word)*)?
Word             := NO-SPACE NO-SPACE*
QueryExpression   := ZettelList? QueryDirective* SearchExpression ActionExpression?
ZettelList        := (ZID (SPACE+ ZID)*).
ZID               := '0'+ ('1' .. '9'') DIGIT*
                   | ('1' .. '9') DIGIT*.
QueryDirective    := ContextDirective
                   | IdentDirective
                   | ItemsDirective
                   | UnlinkedDirective.
ContextDirective  := "CONTEXT" (SPACE+ ContextDetail)*.
ContextDetail     := "FULL"
                   | "BACKWARD"
                   | "FORWARD"
                   | "COST" SPACE+ PosInt
                   | "MAX" SPACE+ PosInt.
IdentDirective    := IDENT.
ItemsDirective    := ITEMS.
UnlinkedDirective := UNLINKED (SPACE+ PHRASE SPACE+ Word)*.
SearchExpression  := SearchTerm (SPACE+ SearchTerm)*.
SearchTerm        := SearchOperator? SearchValue
                   | SearchKey SearchOperator SearchValue?
                   | SearchKey ExistOperator
                   | "OR"
                   | "RANDOM"
                   | "PICK" SPACE+ PosInt
                   | "ORDER" SPACE+ ("REVERSE" SPACE+)? SearchKey
                   | "OFFSET" SPACE+ PosInt
                   | "LIMIT" SPACE+ PosInt.
SearchValue       := Word.
SearchKey         := MetadataKey.
SearchOperator    := '!'
                   | ('!')? ('~' | ':' | '[' | '}').
ExistOperator     := '?'
                   | '!' '?'.
PosInt            := '0'
                   | ('1' .. '9') DIGIT*.
ActionExpression  := '|' (Word (SPACE+ Word)*)?
Action            := Word
                   | 'KEYS'
                   | 'N' NO-SPACE*
                   | 'MAX' PosInt
                   | 'MIN' PosInt
                   | 'REDIRECT'
                   | 'REINDEX'.
Word              := NO-SPACE NO-SPACE*
```

Changes to docs/manual/00001007790000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13

14
15
16



1
2
3
4
5
6

7
8
9
10
11
12

13
14
15
16
17
18
19






-
+





-
+



+
+
+
id: 00001007790000
title: Useful query expressions
role: manual
tags: #example #manual #search #zettelstore
syntax: zmk
created: 20220810144539
modified: 20220917174956
modified: 20240216003702

|= Query Expression |= Meaning
| [[query:role:configuration]] | Zettel that contains some configuration data for the Zettelstore
| [[query:ORDER REVERSE created LIMIT 40]] | 40 recently created zettel
| [[query:ORDER REVERSE published LIMIT 40]] | 40 recently updated zettel
| [[query:RANDOM LIMIT 40]] | 40 random zettel
| [[query:PICK 40]] | 40 random zettel, ordered by zettel identifier
| [[query:dead?]] | Zettel with invalid / dead links
| [[query:backward!? precursor!?]] | Zettel that are not referenced by other zettel
| [[query:tags!?]] | Zettel without tags
| [[query:expire? ORDER expire]] | Zettel with an expire date, ordered from the nearest to the latest
| [[query:00001007700000 CONTEXT]] | Zettel within the context of the [[given zettel|00001007700000]]
| [[query:PICK 1 | REDIRECT]] | Redirect to a random zettel

Changes to docs/manual/00001007800000.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
1
2
3
4
5
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: 00001007800000
title: Zettelmarkup: Summary of Formatting Characters
role: manual
tags: #manual #reference #zettelmarkup #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220810095559
modified: 20241125182149

The following table gives an overview about the use of all characters that begin a markup element.

|= Character :|= [[Blocks|00001007030000]] <|= [[Inlines|00001007040000]] <
| ''!''  | (free) | (free)
| ''"''  | [[Verse block|00001007030700]] | [[Short inline quote|00001007040100]]
| ''#''  | [[Ordered list|00001007030200]] | [[Tag|00001007040000]]
| ''#''  | [[Ordered list|00001007030200]] | [[marked / highlighted text|00001007040100]]
| ''$''  | (reserved) | (reserved)
| ''%''  | [[Comment block|00001007030900]] | [[Comment|00001007040000]]
| ''&''  | (free) | [[Entity|00001007040000]]
| ''\'''  | (free)  | [[Computer input|00001007040200]]
| ''(''  | (free) | (free)
| '')''  | (free) | (free)
| ''*''  | [[Unordered list|00001007030200]] | [[strongly emphasized text|00001007040100]]
| ''+''  | (free) | (free)
| ''+''  | (reserved) | (reserved)
| '',''  | (free) | [[Sub-scripted text|00001007040100]]
| ''-''  | [[Horizontal rule|00001007030400]] | ""[[en-dash|00001007040000]]""
| ''.''  | (free) | (free)
| ''/''  | (free) | (free)
| '':''  | [[Region block|00001007030800]] / [[description text|00001007030100]] | [[Inline region|00001007040100]]
| '';''  | [[Description term|00001007030100]] | (free)
| ''<''  | [[Quotation block|00001007030600]] | (free)

Changes to docs/manual/00001007903000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001007903000
title: Zettelmarkup: First Steps
role: manual
tags: #manual #tutorial #zettelmarkup #zettelstore
syntax: zmk
created: 20220810182917
modified: 20220926183359
modified: 20231201135849

[[Zettelmarkup|00001007000000]] allows you to leave your text as it is, at least in many situations.
Some characters have a special meaning, but you have to enter them is a defined way to see a visible change.
Zettelmarkup is designed to be used for zettel, which are relatively short.
It allows to produce longer texts, but you should probably use a different tool, if you want to produce an scientific paper, to name an example.

=== Paragraphs
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41







-
+







| ''An __emphasized__ word'' | An __emphasized__ word | Put two underscore characters before and after the text you want to emphasize
| ''Someone uses **bold** text'' | Someone uses **bold** text | Put two asterisks before and after the text you want to see bold
| ''He says: ""I love you!""'' | Her says: ""I love you!"" | Put two quotation mark characters before and after the text you want to quote.

You probably see a principle.

One nice thing about the quotation mark characters: they are rendered according to the current language.
Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}, ""finnish""{lang=fi}.
Examples: ""english""{lang=en}, ""french""{lang=fr}, ""german""{lang=de}.
You will see later, how to change the current language.

=== Lists
Quite often, text consists of lists.
Zettelmarkup supports different types of lists.
The most important lists are:
* Unnumbered lists,

Changes to docs/manual/00001007990000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
1
2
3
4
5
6

7
8
9
10
11
12
13
14

15
16
17
18
19
20
21
22






-
+







-
+







id: 00001007990000
title: Zettelmarkup: Cheat Sheet
role: manual
tags: #manual #reference #zettelmarkup
syntax: zmk
created: 20221209191905
modified: 20221209193310
modified: 20231201140000

=== Overview
This Zettelmarkup cheat sheet provides a quick overview of many Zettelmarkup elements.
It can not cover any special case.
If you need more information about any of these elements, please refer to the detailed description.

=== Basic Syntax
|[[Text formatting|00001007040100]]|''__italic text__'' &rarr; __italic text__, ''**bold text**'' &rarr; **bold text**, ''""quoted text""'' &rarr; ""quoted text""
|[[Text formatting|00001007040100]]|''__italic text__'' &rarr; __italic text__, ''**bold text**'' &rarr; **bold text**, ''""quoted text""'' &rarr; ""quoted text"", ''##marked text##'' &rarr; ##marked text##
|[[Text editing|00001007040100]]|''>>inserted text>>'' &rarr; >>inserted text>>, ''~~deleted text~~'' &rarr; ~~deleted text~~
|[[Text literal formatting|00001007040200]]|''\'\'entered text\'\''' &rarr; ''entered text'', ''``source code``'' &rarr; ``source code``, ''==text output=='' &rarr; ==text output==
|[[Superscript, subscript|00001007040100]]|''m^^2^^'' &rarr; m^^2^^, ''H,,2,,O'' &rarr; H,,2,,O
|[[Links to other zettel|00001007040310]]|''[[Link text|00001007990000]]'' &rarr; [[Link text|00001007990000]]
|[[Links to external resources|00001007040310]]|''[[Zettelstore|https://zettelstore.de]]'' &rarr; [[Zettelstore|https://zettelstore.de]]
|[[Embed an image|00001007040322]]|''{{Image text|00000000040001}}'' &rarr; {{Image text|00000000040001}}
|[[Embed content of first paragraph|00001007040324]]|''{{00001007990000}}'' &rarr; {{00001007990000}}

Changes to docs/manual/00001008000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001008000000
title: Other Markup Languages
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20210126175300
modified: 20221112111148
modified: 20240413160242

[[Zettelmarkup|00001007000000]] is not the only markup language you can use to define your content.
Zettelstore is quite agnostic with respect to markup languages.
Of course, Zettelmarkup plays an important role.
However, with the exception of zettel titles, you can use any (markup) language that is supported:

* CSS
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52



53
54
55
56
57
58
59
60
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







-
-







+
+
+








  For security reasons, equivocal elements will not be encoded in the HTML format / web user interface.
  The ``< script ...>`` tag is an example.
  See [[security aspects of Markdown|00001008010000#security-aspects]] for some details.
; [!markdown|''markdown''], [!md|''md'']
: For those who desperately need [[Markdown|https://daringfireball.net/projects/markdown/]].
  Since the world of Markdown is so diverse, a [[CommonMark|00001008010500]] parser is used.
  See [[Use Markdown within Zettelstore|00001008010000]].
; [!mustache|''mustache'']
: A [[Mustache template|https://mustache.github.io/]], used when rendering a zettel as HTML for the [[web user interface|00001014000000]].
; [!none|''none'']
: Only the metadata of a zettel is ""parsed"".
  Useful for displaying the full metadata.
  The [[runtime configuration zettel|00000000000100]] uses this syntax.
  The zettel content is ignored.
; [!svg|''svg'']
: [[Scalable Vector Graphics|https://www.w3.org/TR/SVG2/]].
; [!sxn|''sxn'']
: S-Expressions, as implemented by [[Sx|https://t73f.de/r/sx]].
  Often used to specify templates when rendering a zettel as HTML for the [[web user interface|00001014000000]] (with the help of sxhtml]).
; [!text|''text''], [!plain|''plain''], [!txt|''txt'']
: Plain text that must not be interpreted further.
; [!zmk|''zmk'']
: [[Zettelmarkup|00001007000000]].

The actual values are also listed in a zettel named [[Zettelstore Supported Parser|00000000000092]].

If you specify something else, your content will be interpreted as plain text.

Changes to docs/manual/00001008050000.zettel.

1
2

3
4
5

6

7
8
9
10
11
12
13
1

2
3
4
5
6

7
8
9
10
11
12
13
14

-
+



+
-
+







id: 00001008050000
title: The ""draw"" language
title: The "draw" language
role: manual
tags: #graphic #manual #zettelstore
syntax: zmk
created: 20220131142036
modified: 20220311120439
modified: 20230403123738

Sometimes, ""a picture is worth a thousand words"".
To create some graphical representations, Zettelmarkup provides a simple mechanism.
Characters like ""''|''"" or ""''-''"" already provide some visual feedback.
For example, to create a picture containing two boxes that are connected via an arrow, the following representation is possible:
```
~~~draw

Changes to docs/manual/00001010000000.zettel.

1
2
3
4
5
6
7

8
9
10


11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29

30
31
32
33
34
35
36
1
2
3
4
5
6

7
8


9
10
11
12
13
14
15
16
17
18
19
20
21
22

23
24
25
26
27
28

29
30
31
32
33
34
35
36






-
+

-
-
+
+












-
+





-
+







id: 00001010000000
title: Security
role: manual
tags: #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221018123622
modified: 20241213102811

Your zettel could contain sensitive content.
You probably want to ensure that only authorized person can read and/or modify them.
Your zettel may contain sensitive content.
You probably want to ensure that only authorized persons can read and/or modify them.
Zettelstore ensures this in various ways.

=== Local first
The Zettelstore is designed to run on your local computer.
If you do not configure it in other ways, no person from another computer can connect to your Zettelstore.
You must explicitly configure it to allow access from other computers.

In the case that you own multiple computers, you do not have to access the Zettelstore remotely.
You could install Zettelstore on each computer and set-up some software to synchronize your zettel.
Since zettel are stored as ordinary files, this task could be done in various ways.

=== Read-only
You can start the Zettelstore in an read-only mode.
You can start the Zettelstore in a read-only mode.
Nobody, not even you as the owner of the Zettelstore, can change something via its interfaces[^However, as an owner, you have access to the files that store the zettel. If you modify the files, these changes will be reflected via its interfaces.].

You enable read-only mode through the key ''readonly'' in the [[startup configuration zettel|00001004010000#readonly]] or with the ''-r'' option of the ``zettelstore run`` sub-command.

=== Authentication
The Zettelstore can be configured that a user must authenticate itself to gain access to the content.
The Zettelstore can be configured that users must authenticate themselves to gain access to the content.

* [[How to enable authentication|00001010040100]]
* [[How to add a new user|00001010040200]]
* [[How users are authenticated|00001010040400]] (some technical background)
* [[Authenticated sessions|00001010040700]]

=== Authorization

Changes to docs/manual/00001010040100.zettel.

1
2
3
4
5

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





+







id: 00001010040100
title: Enable authentication
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220419192817

To enable authentication, you must create a zettel that stores [[authentication data|00001010040200]] for the owner.
Then you must reference this zettel within the [[startup configuration|00001004010000#owner]] under the key ''owner''.
Once the startup configuration contains a valid [[zettel identifier|00001006050000]] under that key, authentication is enabled.

Please note that you must also set key ''secret'' of the [[startup configuration|00001004010000#secret]] to some random string data (minimum length is 16 bytes) to secure the data exchanged with a client system.

Changes to docs/manual/00001010040400.zettel.

1
2
3
4
5

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





+







id: 00001010040400
title: Authentication process
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211127174943

When someone tries to authenticate itself with an user identifier / ""user name"" and a password, the following process is executed:

# If meta key ''owner'' of the configuration zettel does not have a valid [[zettel identifier|00001006050000]] as value, authentication fails.
# Retrieve all zettel, where the meta key ''user-id'' has the same value as the given user identification. If the list is empty, authentication fails.
# From above list, the zettel with the numerically smallest identifier is selected.

Changes to docs/manual/00001010040700.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
1
2
3
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: 00001010040700
title: Access token
role: manual
tags: #authentication #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211202120950
modified: 20241213101607

If an user is authenticated, an ""access token"" is created that must be sent with every request to prove the identity of the caller.
Otherwise the user will not be recognized by Zettelstore.

If the user was authenticated via the [[web user interface|00001014000000]], the access token is stored in a [[""session cookie""|https://en.wikipedia.org/wiki/HTTP_cookie#Session_cookie]].
When the web browser is closed, theses cookies are not saved.
When the web browser is closed, these cookies are not saved.
If you want web browser to store the cookie as long as lifetime of that token, the owner must set ''persistent-cookie'' of the [[startup configuration|00001004010000]] to ''true''.

If the web browser remains inactive for a period, the user will be automatically logged off, because each access token has a limited lifetime.
The maximum length of this period is specified by the ''token-lifetime-html'' value of the startup configuration.
Every time a web page is displayed, a fresh token is created and stored inside the cookie.

If the user was authenticated via the API, the access token will be returned as the content of the response.
Typically, the lifetime of this token is more short term, e.g. 10 minutes.
It is specified by the ''token-lifetime-api'' value of the startup configuration.
If you need more time, you can either [[re-authenticate|00001012050200]] the user or use an API call to [[renew the access token|00001012050400]].

If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), your must set the ''insecure-cookie'' value of the startup configuration to ''true''.
If you remotely access your Zettelstore via HTTP (not via HTTPS, which allows encrypted communication), you must set the ''insecure-cookie'' value of the startup configuration to ''true''.
In most cases, such a scenario is not recommended, because user name and password will be transferred as plain text.
You could make use of such scenario if you know all parties that access the local network where you access the Zettelstore.

Changes to docs/manual/00001010070300.zettel.

1
2
3
4
5

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





+







id: 00001010070300
title: User roles
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220214175212

Every user is associated with some basic privileges.
These are specified in the [[user zettel|00001010040200]] with the key ''user-role''.
The following values are supported:

; [!reader|""reader""]

Changes to docs/manual/00001010070400.zettel.

1
2
3
4
5

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





+







id: 00001010070400
title: Authorization and read-only mode
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211103164251

It is possible to enable both the read-only mode of the Zettelstore __and__ authentication/authorization.
Both modes are independent from each other.
This gives four use cases:

; Not read-only, no authorization

Changes to docs/manual/00001010070600.zettel.

1
2
3
4
5

6

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

7
8
9
10
11
12
13
14





+
-
+







id: 00001010070600
title: Access rules
role: manual
tags: #authorization #configuration #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124142456
modified: 20240711183714

Whether an operation of the Zettelstore is allowed or rejected, depends on various factors.

The following rules are checked first, in this order:

# In read-only mode, every operation except the ""Read"" operation is rejected.
# If there is no owner, authentication is disabled and every operation is allowed for everybody.
38
39
40
41
42
43
44
45
46
47
48
49
50
51
39
40
41
42
43
44
45



46
47
48
49







-
-
-




** If the zettel is the [[user zettel|00001010040200]] of the authenticated user, proceed as follows:
*** If some sensitive meta values are changed (e.g. user identifier, zettel role, user role, but not hashed password), reject the access
*** Since the user just updates some uncritical values, grant the access
   In other words: a user is allowed to change its user zettel, even if s/he has no writer privilege and if only uncritical data is changed.
** If the ''user-role'' of the user is ""reader"", reject the access.
** If the user is not allowed to create a new zettel, reject the access.
** Otherwise grant the access.
* Rename a zettel
** Reject the access.
   Only the owner of the Zettelstore is currently allowed to give a new identifier for a zettel.
* Delete a zettel
** Reject the access.
   Only the owner of the Zettelstore is allowed to delete a zettel.
   This may change in the future.

Changes to docs/manual/00001010090100.zettel.

1
2
3
4
5

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





+







id: 00001010090100
title: External server to encrypt message transport
role: manual
tags: #configuration #encryption #manual #security #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220217180826

Since Zettelstore does not encrypt the messages it exchanges with its clients, you may need some additional software to enable encryption.

=== Public-key encryption
To enable encryption, you probably use some kind of encryption keys.
In most cases, you need to deploy a ""public-key encryption"" process, where your side publish a public encryption key that only works with a corresponding private decryption key.

Changes to docs/manual/00001012000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14

15
16
17
18
19
20
21
22
23
24
25
26


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1
2
3
4
5
6

7
8
9
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: 00001012000000
title: API
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221219150243
modified: 20240711183736

The API (short for ""**A**pplication **P**rogramming **I**nterface"") is the primary way to communicate with a running Zettelstore.
Most integration with other systems and services is done through the API.
The [[web user interface|00001014000000]] is just an alternative, secondary way of interacting with a Zettelstore.

=== Background
The API is HTTP-based and uses plain text and JSON as its main encoding format for exchanging messages between a Zettelstore and its client software.
The API is HTTP-based and uses plain text and [[symbolic expressions|00001012930000]] as its main encoding formats for exchanging messages between a Zettelstore and its client software.

There is an [[overview zettel|00001012920000]] that shows the structure of the endpoints used by the API and gives an indication about its use.

=== Authentication
If [[authentication is enabled|00001010040100]], most API calls must include an [[access token|00001010040700]] that proves the identity of the caller.
* [[Authenticate an user|00001012050200]] to obtain an access token
* [[Renew an access token|00001012050400]] without costly re-authentication
* [[Provide an access token|00001012050600]] when doing an API call

=== Zettel lists
* [[List all zettel|00001012051200]]
* [[Query the list of all zettel|00001012051400]]
* [[Determine a tag zettel|00001012051600]]
* [[Determine a role zettel|00001012051800]]

=== Working with zettel
* [[Create a new zettel|00001012053200]]
* [[Retrieve metadata and content of an existing zettel|00001012053300]]
* [[Retrieve metadata of an existing zettel|00001012053400]]
* [[Retrieve evaluated metadata and content of an existing zettel in various encodings|00001012053500]]
* [[Retrieve parsed metadata and content of an existing zettel in various encodings|00001012053600]]
* [[Retrieve context of an existing zettel|00001012053800]]
* [[Retrieve unlinked references to an existing zettel|00001012053900]]
* [[Retrieve zettel order within an existing zettel|00001012054000]]
* [[Update metadata and content of a zettel|00001012054200]]
* [[Rename a zettel|00001012054400]]
* [[Delete a zettel|00001012054600]]

=== Various helper methods
* [[Retrieve administrative data|00001012070500]]
* [[Execute some commands|00001012080100]]
** [[Check for authentication|00001012080200]]
** [[Refresh internal data|00001012080500]]

Changes to docs/manual/00001012050200.zettel.

1
2
3
4
5

6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21

22
23
24
25
26
27

28
29
30

31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46

47
48
49
50
51
52
53
1
2
3
4
5
6

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





+
-
+








-
+





-
+





-
+


-
+









-
+





-
+







id: 00001012050200
title: API: Authenticate a client
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220107215844
modified: 20230412150544

Authentication for future API calls is done by sending a [[user identification|00001010040200]] and a password to the Zettelstore to obtain an [[access token|00001010040700]].
This token has to be used for other API calls.
It is valid for a relatively short amount of time, as configured with the key ''token-lifetime-api'' of the [[startup configuration|00001004010000#token-lifetime-api]] (typically 10 minutes).

The simplest way is to send user identification (''IDENT'') and password (''PASSWORD'') via [[HTTP Basic Authentication|https://tools.ietf.org/html/rfc7617]] and send them to the [[endpoint|00001012920000]] ''/a'' with a POST request:
```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA2MiwiaWF0IjoxNjgxMzA0MDAyLCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kdF8PdiL50gIPkRD3ovgR6nUXR0-80EKAXcY2zVYgYvryF09iXnNR3zrvYnGzdrArMcnvAYqVvuXtqhQj2jG9g" 600)
```

Some tools, like [[curl|https://curl.haxx.se/]], also allow to specify user identification and password as part of the URL:
```sh
# curl -X POST http://IDENT:PASSWORD@127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 600)
```

If you do not want to use Basic Authentication, you can also send user identification and password as HTML form data:
```sh
# curl -X POST -d 'username=IDENT&password=PASSWORD' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":600}
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4OCwiaWF0IjoxNjgxMzA0MDI4LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.qIEyOMFXykCApWtBaqbSESwTL96stWl2LRICiRNAXUjcY-mwx_SSl9L5Fj2FvmrI1K1RBvWehjoq8KZUNjhJ9Q" 600)
```

In all cases, you will receive an JSON object will all [[relevant data|00001012921000]] to be used for further API calls.
In all cases, you will receive a list with three elements that will contain all [[relevant data|00001012921000]] to be used for further API calls.

**Important:** obtaining a token is a time-intensive process.
Zettelstore will delay every request to obtain a token for a certain amount of time.
Please take into account that this request will take approximately 500 milliseconds, under certain circumstances more.

However, if [[authentication is not enabled|00001010040100]] and you send an authentication request, no user identification/password checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X POST -u IDENT:PASSWORD http://127.0.0.1:23123/a
{"token":"freeaccess","token_type":"Bearer","expires_in":316224000}
("Bearer" "freeaccess" 316224000)
```

In this case, it is even possible to omit the user identification/password.

=== HTTP Status codes
In all cases of successful authentication, a JSON object is returned, which contains the token under the key ''"token"''.
In all cases of successful authentication, a list is returned, which contains the token as the second element.
A successful authentication is signaled with the HTTP status code 200, as usual.

Other status codes possibly send by the Zettelstore:
; ''400''
: Unable to process the request.
  In most cases the form data was invalid.
; ''401''

Changes to docs/manual/00001012050400.zettel.

1
2
3
4
5

6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31

32
33
34
35
36
37
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15

16
17
18
19
20
21
22
23
24

25
26
27
28
29
30
31

32
33
34
35
36
37
38





+
-
+








-
+








-
+






-
+






id: 00001012050400
title: API: Renew an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220107215751
modified: 20230412160219

An access token is only valid for a certain duration.
Since the [[authentication process|00001012050200]] will need some processing time, there is a way to renew the token without providing full authentication data.

Send a HTTP PUT request to the [[endpoint|00001012920000]] ''/a'' and include the current access token in the ''Authorization'' header:

```sh
# curl -X PUT -H 'Authorization: Bearer TOKEN' http://127.0.0.1:23123/a
{"access_token":"eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTYwMTczMTI3NSwiaWF0IjoxNjAxNzMwNjc1LCJzdWIiOiJhYmMiLCJ6aWQiOiIyMDIwMTAwMzE1MDEwMCJ9.ekhXkvn146P2bMKFQcU-bNlvgbeO6sS39hs6U5EKfjIqnSInkuHYjYAIfUqf_clYRfr6YBlX5izii8XfxV8jhg","token_type":"Bearer","expires_in":456}
("Bearer" "eyJhbGciOiJIUzUxMiJ9.eyJfdGsiOjEsImV4cCI6MTY4MTMwNDA4NiwiaWF0IjoxNjgxMzA0MDI2LCJzdWIiOiJvd25lciIsInppZCI6IjIwMjEwNjI5MTYzMzAwIn0.kZd3prYc79dt9efDsrYVHtKrjWyOWvfByjeeUB3hf_vs43V3SNJqmb8k-zTHVNWOK0-5orVPrg2tIAqbXqmkhg" 456)
```
You may receive a new access token, or the current one if it was obtained not a long time ago.
However, the lifetime of the returned [[access token|00001012921000]] is accurate.

If [[authentication is not enabled|00001010040100]] and you send a renew request, no checking is done and you receive an artificial token immediate, without any delay:

```sh
# curl -X PUT -H 'Authorization: Bearer freeaccess' http://127.0.0.1:23123/a
{"token":"freeaccess","token_type":"Bearer","expires_in":316224000}
("Bearer" "freeaccess" 316224000)
```

In this case, it is even possible to omit the access token.

=== HTTP Status codes
; ''200''
: Renew process was successful, the body contains an [[appropriate JSON object|00001012921000]].
: Renew process was successful, the body contains a [[list|00001012921000]] with the relevant data.
; ''400''
: The renew process was not successful.
  There are several reasons for this.
  Maybe access bearer token was not valid.

  Probably you should [[authenticate|00001012050200]] again with user identification and password.

Changes to docs/manual/00001012050600.zettel.

1
2
3
4
5

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





+







id: 00001012050600
title: API: Provide an access token
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220218130020

The [[authentication process|00001012050200]] provides you with an [[access token|00001012921000]].
Most API calls need such an access token, so that they know the identity of the caller.

You send the access token in the ""Authorization"" request header field, as described in [[RFC 6750, section 2.1|https://tools.ietf.org/html/rfc6750#section-2.1]].
You need to use the ""Bearer"" authentication scheme to transmit the access token.

Changes to docs/manual/00001012051200.zettel.

1
2
3
4
5
6
7

8
9

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



26




27
28
29
30


31
32
33

34
35
36
37


38
39
40
41


42
43
44
45
46
47
48
49

50
51

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








66
67
68
69
70
71

72
73



74
75
76



77
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112

113
114
115
116

117

118
119
120
121
122
123
124
125
126
127
128

129
130
131
132
1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

29
30
31
32
33
34


35
36
37
38

39
40



41
42




43
44








45


46














47
48
49
50
51
52
53
54






55


56
57
58



59
60
61









62



















63








64




65

66
67
68
69
70
71
72
73
74
75
76

77
78
79
80
81






-
+

-
+
















+
+
+
-
+
+
+
+


-
-
+
+


-
+

-
-
-
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
+
-
+










-
+




id: 00001012051200
title: API: List all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221219154400
modified: 20241216104355

To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
To list all zettel just send a HTTP GET request to the [[endpoint|00001012920000]] ''/z''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].
Always use the endpoint ''/z'' to work with a list of zettel.

Without further specifications, a plain text document is returned, with one line per zettel.
Each line contains in the first 14 characters the [[zettel identifier|00001006050000]].
Separated by a space character, the title of the zettel follows:

```sh
# curl http://127.0.0.1:23123/z
...
00001012051200 API: List all zettel
00001012050600 API: Provide an access token
00001012050400 API: Renew an access token
00001012050200 API: Authenticate a client
...
```

The list is **not** sorted, even in the these examples where it appears to be sorted.
If you want to have it ordered, you must specify it with the help of a [[query expression|00001007700000]] / [[search term|00001007702000]].
See [[Query the list of all zettel|00001012051400]] how to do it.
Alternatively, you may retrieve the list of all zettel as a JSON object by specifying the encoding with the query parameter ''enc=json'':

=== Data output

Alternatively, you may retrieve the zettel list as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':

```sh
# curl 'http://127.0.0.1:23123/z?enc=json'
{"query":"","human":"","list":[{"id":"00001012051200","meta":{"back":"00001012000000","backward":"00001012000000 00001012920000","box-number":"1","created":"20210126175322","forward":"00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000","modified":"20221219150626","published":"20221219150626","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: List all zettel"},"rights":62},{"id":"00001012050600","meta":{"back":"00001012000000 00001012080500","backward":"00001012000000 00001012080500","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012921000","modified":"20220218130020","published":"20220218130020","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Provide an access token"},"rights":62},{"id":"00001012050400","meta":{"back":"00001010040700 00001012000000","backward":"00001010040700 00001012000000 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001010040100 00001012050200 00001012920000 00001012921000","modified":"20220107215751","published":"20220107215751","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Renew an access token"},"rights":62},{"id":"00001012050200","meta":{"back":"00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200","backward":"00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012080200 00001012920000 00001012921000","box-number":"1","created":"00010101000000","forward":"00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000","modified":"20220107215844","published":"20220107215844","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Authenticate a client"},"rights":62}, ...]}
# curl 'http://127.0.0.1:23123/z?enc=data'
(meta-list (query "") (human "") (list (zettel (id "00001012921200") (meta (title "API: Encoding of Zettel Access Rights") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000") (box-number "1") (created "00010101000000") (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300") (modified "20220201171959") (published "20220201171959")) (rights 62)) (zettel (id "00001007030100") ...
```

If you reformat the JSON output, you will see its structure better:
Pretty-printed, this results in:
```
{
  "query": "",
  "human": "",
(meta-list (query "")
           (human "")
  "list": [
    {
      "id": "00001012051200",
      "meta": {
           (list (zettel (id "00001012921200")
                         (meta (title "API: Encoding of Zettel Access Rights")
        "back": "00001012000000",
        "backward": "00001012000000 00001012920000",
        "box-number": "1",
        "created": "20210126175322",
        "forward": "00001006020000 00001006050000 00001007700000 00001010040100 00001012050200 00001012051400 00001012920000 00001012921000 00001014000000",
        "modified": "20221219151200",
        "published": "20221219151200",
        "role": "manual",
                               (role "manual")
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
                               (tags "#api #manual #reference #zettelstore")
        "title": "API: List all zettel"
      },
      "rights": 62
    },
    {
      "id": "00001012050600",
      "meta": {
        "back": "00001012000000 00001012080500",
        "backward": "00001012000000 00001012080500",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001012050200 00001012921000",
        "modified": "20220218130020",
        "published": "20220218130020",
                               (syntax "zmk")
                               (back "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
                               (backward "00001012051200 00001012051400 00001012053300 00001012053400 00001012053900 00001012054000")
                               (box-number "1")
                               (created "00010101000000")
                               (forward "00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300")
                               (modified "20220201171959")
                               (published "20220201171959"))
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
        "title": "API: Provide an access token"
      },
      "rights": 62
                         (rights 62))
    },
    {
                 (zettel (id "00001007030100")
```

      "id": "00001012050400",
      "meta": {
        "back": "00001010040700 00001012000000",
* The result is a list, starting with the symbol ''meta-list''.
* Then, some key/value pairs are following, also nested.
* Keys ''query'' and ''human'' will be explained [[later in this manual|00001012051400]].
        "backward": "00001010040700 00001012000000 00001012920000 00001012921000",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001010040100 00001012050200 00001012920000 00001012921000",
        "modified": "20220107215751",
        "published": "20220107215751",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
* ''list'' starts a list of zettel.
        "title": "API: Renew an access token"
      },
      "rights": 62
    },
    {
      "id": "00001012050200",
      "meta": {
        "back": "00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 00001012053600 00001012
080200",
        "backward": "00001010040700 00001012000000 00001012050400 00001012050600 00001012051200 00001012051400 00001012053300 00001012053400 00001012053500 0000
1012053600 00001012080200 00001012920000 00001012921000",
        "box-number": "1",
        "created": "00010101000000",
        "forward": "00001004010000 00001010040100 00001010040200 00001010040700 00001012920000 00001012921000",
        "modified": "20220107215844",
        "published": "20220107215844",
        "role": "manual",
        "syntax": "zmk",
        "tags": "#api #manual #zettelstore",
* ''zettel'' itself start, well, a zettel.
        "title": "API: Authenticate a client"
      },
      "rights": 62
    }
  ]
}
```

* ''id'' denotes the zettel identifier, encoded as a string.
The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

* Nested in ''meta'' are the metadata, each as a key/value pair.
The JSON objects keys ''"query"'' and ''"human"'' will be explained later in this manual.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

=== Note
This request (and similar others) will always return a list of metadata, provided the request was syntactically correct.
There will never be a HTTP status code 403 (Forbidden), even if [[authentication was enabled|00001010040100]] and you did not provide a valid access token.
In this case, the resulting list might be quite short (some zettel will have [[public visibility|00001010070200]]) or the list might be empty.

With this call, you cannot differentiate between an empty result list (e.g because your search did not found a zettel with the specified term) and an empty list because of missing authorization (e.g. an invalid access token).

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an [[appropriate JSON object|00001012921000]].
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid.

Changes to docs/manual/00001012051400.zettel.

1
2
3
4
5
6
7


8
9

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


27
28
29
30
31
32

33
34
35
36


37
38
39
40
41
42
43


44
45
46
47






48


49
50

51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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
1
2
3
4
5
6

7
8
9

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






-
+
+

-
+















-
-
+
+





-
+


-
-
+
+


-
-
-
-
-
+
+




+
+
+
+
+
+

+
+

-
+


















+
+
+
-
+


-
+
-
+


-
-
+
+
+
+
+


-
+


-
+
-
+


-
-
-


+
+









+
+
+
+
+
+
+
+
+
-
+



+
-
+

+
+







+
+




id: 00001012051400
title: API: Query the list of all zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220912111111
modified: 20230116183844
modified: 20241216104329
precursor: 00001012051200

The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.
The [[endpoint|00001012920000]] ''/z'' also allows you to filter the list of all zettel[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] and optionally to provide some actions.

A [[query|00001007700000]] is an optional [[search expression|00001007700000#search-expression]], together with an optional [[list of actions|00001007700000#action-list]] (described below).
An empty search expression will select all zettel.
An empty list of action, or no valid action, returns the list of all selected zettel metadata.

Search expression and action list are separated by a vertical bar character (""''|''"", U+007C), and must be given with the query parameter ''q''.

The query parameter ""''q''"" allows you to specify [[query expressions|00001007700000]] for a full-text search of all zettel content and/or restricting the search according to specific metadata.

It is allowed to specify this query parameter more than once.

This parameter loosely resembles the search form of the [[web user interface|00001014000000]] or those of [[Zettelmarkup's Query Transclusion|00001007031140]].

For example, if you want to retrieve all zettel that contain the string ""API"" in its title, your request will be:
```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI'
00001012921000 API: JSON structure of an access token
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1'
00001012921000 API: Structure of an access token
00001012920500 Formats available by the API
00001012920000 Endpoints used by the API
...
```

If you want to retrieve a JSON document:
If you want to retrieve a data document, as a [[symbolic expression|00001012930500]]:

```sh
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI&enc=json'
{"query":"title:API","human":"title HAS API","list":[{"id":"00001012921200","meta":{"back":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","backward":"00001012051200 00001012051400 00001012053300 00001012053400 00001012053800 00001012053900 00001012054000","box-number":"1","created":"00010101000000","forward":"00001003000000 00001006020400 00001010000000 00001010040100 00001010040200 00001010070200 00001010070300","modified":"20220201171959","published":"20220201171959","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: Encoding of Zettel Access Rights"},"rights":62},{"id":"00001012921000","meta":{"back":"00001012050600 00001012051200","backward":"00001012050200 00001012050400 00001012050600 00001012051200","box-number":"1","created":"00010101000000","forward":"00001012050200 00001012050400","published":"00010101000000","role":"manual","syntax":"zmk","tags":"#api #manual #reference #zettelstore","title":"API: JSON structure of an access token"},"rights":62}, ...]
# curl 'http://127.0.0.1:23123/z?q=title%3AAPI+ORDER+REVERSE+id+OFFSET+1&enc=data'
(meta-list (query "title:API ORDER REVERSE id OFFSET 1") (human "title HAS API ORDER REVERSE id OFFSET 1") (list (zettel (id 1012921000) (meta (title "API: Structure of an access token") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001012050600 00001012051200") (backward "00001012050200 00001012050400 00001012050600 00001012051200") (box-number "1") (created "20210126175322") (forward "00001012050200 00001012050400 00001012930000") (modified "20230412155303") (published "20230412155303")) (rights 62)) (zettel (id 1012920500) (meta (title "Encodings available via the API") (role "manual") (tags "#api #manual #reference #zettelstore") (syntax "zmk") (back "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (backward "00001006000000 00001008010000 00001008010500 00001012053500 00001012053600") (box-number "1") (created "20210126175322") (forward "00001012000000 00001012920510 00001012920513 00001012920516 00001012920519 00001012920522 00001012920525") (modified "20230403123653") (published "20230403123653")) (rights 62)) (zettel (id 1012920000) (meta (title "Endpoints used by the API") ...
```

The JSON object contains a key ''"list"'' where its value is a list of zettel JSON objects.
These zettel JSON objects themselves contains the keys ''"id"'' (value is a string containing the [[zettel identifier|00001006050000]]), ''"meta"'' (value as a JSON object), and ''"rights"'' (encodes the [[access rights|00001012921200]] for the given zettel).
The value of key ''"meta"'' effectively contains all metadata of the identified zettel, where metadata keys are encoded as JSON object keys and metadata values encoded as JSON strings.

Additionally, the JSON object contains the keys ''"query"'' and ''"human"'' with a string value.
The data object contains a key ''"meta-list"'' to signal that it contains a list of metadata values (and some more).
It contains the keys ''"query"'' and ''"human"'' with a string value.
Both will contain a textual description of the underlying query if you select only some zettel with a [[query expression|00001007700000]].
Without a selection, the values are the empty string.
''"query"'' returns the normalized query expression itself, while ''"human"'' is the normalized query expression to be read by humans.

The symbol ''list'' starts the list of zettel data.
Data of a zettel is indicated by the symbol ''zettel'', followed by ''(id ID)'' that describes the zettel identifier as a numeric value.
Leading zeroes are removed.
Metadata starts with the symbol ''meta'', and each metadatum itself is a list of metadata key / metadata value.
Metadata keys are encoded as a symbol, metadata values as a string.
''"rights"'' encodes the [[access rights|00001012921200]] for the given zettel.

=== Aggregates

An implicit precondition is that the zettel must contain the given metadata key.
For a metadata key like [[''title''|00001006020000#title]], which has a default value, this precondition should always be true.
For a metadata key like [[''title''|00001006020000#title]], which have a default value, this precondition should always be true.
But the situation is different for a key like [[''url''|00001006020000#url]].
Both ``curl 'http://localhost:23123/z?q=url%3A'`` and ``curl 'http://localhost:23123/z?q=url%3A!'`` may result in an empty list.


As an example for a query action, to list all roles used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|role''.

```sh
# curl 'http://127.0.0.1:23123/z?q=|role'
configuration	00001000000100 00000000090002 00000000090000 00000000040001 00000000025001 00000000020001 00000000000100 00000000000092 00000000000090 00000000000006 00000000000005 00000000000004 00000000000001
manual	00001018000000 00001017000000 00001014000000 00001012921200 00001012921000 00001012920800 00001012920588 00001012920584 00001012920582 00001012920522 00001012920519 00001012920516 00001012920513 00001012920510 00001012920503 00001012920500 00001012920000 00001012080500 00001012080200 00001012080100 00001012070500 00001012054600 00001012054400 00001012054200 00001012054000 00001012053900 00001012053800 00001012053600 00001012053500 00001012053400 00001012053300 00001012053200 00001012051400 00001012051200 00001012050600 00001012050400 00001012050200 00001012000000 00001010090100 00001010070600 00001010070400 00001010070300 00001010070200 00001010040700 00001010040400 00001010040200 00001010040100 00001010000000 00001008050000 00001008010500 00001008010000 00001008000000 00001007990000 00001007906000 00001007903000 00001007900000 00001007800000 00001007790000 00001007780000 00001007706000 00001007705000 00001007702000 00001007700000 00001007050200 00001007050100 00001007050000 00001007040350 00001007040340 00001007040330 00001007040324 00001007040322 00001007040320 00001007040310 00001007040300 00001007040200 00001007040100 00001007040000 00001007031400 00001007031300 00001007031200 00001007031140 00001007031110 00001007031100 00001007031000 00001007030900 00001007030800 00001007030700 00001007030600 00001007030500 00001007030400 00001007030300 00001007030200 00001007030100 00001007030000 00001007020000 00001007010000 00001007000000 00001006055000 00001006050000 00001006036500 00001006036000 00001006035500 00001006035000 00001006034500 00001006034000 00001006033500 00001006033000 00001006032500 00001006032000 00001006031500 00001006031000 00001006030500 00001006030000 00001006020400 00001006020100 00001006020000 00001006010000 00001006000000 00001005090000 00001005000000 00001004101000 00001004100000 00001004059900 00001004059700 00001004051400 00001004051200 00001004051100 00001004051000 00001004050400 00001004050200 00001004050000 00001004020200 00001004020000 00001004011600 00001004011400 00001004011200 00001004010000 00001004000000 00001003600000 00001003315000 00001003310000 00001003305000 00001003300000 00001003000000 00001002000000 00001001000000 00001000000000
zettel	00010000000000 00000000090001
```

The result is a text file.
The first word, separated by a horizontal tab (U+0009) contains the role name.
The rest of the line consists of zettel identifier, where the corresponding zettel have this role.
Zettel identifier are separated by a space character (U+0020).

Please note that the list is **not** sorted by the role name, so the same request might result in a different order.
If you want a sorted list, you could sort it on the command line (``curl 'http://127.0.0.1:23123/z?q=|role' | sort``) or within the software that made the call to the Zettelstore.

Of course, this list can be returned as a JSON object:
Of course, this list can also be returned as a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|role?enc=json'
# curl 'http://127.0.0.1:23123/z?q=|role&enc=data'
{"map":{"configuration":["00000000090002","00000000090000", ... ,"00000000000001"],"manual":["00001014000000", ... ,"00001000000000"],"zettel":["00010000000000", ... ,"00001012070500","00000000090001"]}}
(aggregate "role" (query "| role") (human "| role") (list ("zettel" 10000000000 90001) ("configuration" 6 100 1000000100 20001 90 25001 92 4 40001 1 90000 5 90002) ("manual" 1008050000 1007031110 1008000000 1012920513 1005000000 1012931800 1010040700 1012931000 1012053600 1006050000 1012050200 1012000000 1012070500 1012920522 1006032500 1006020100 1007906000 1007030300 1012051400 1007040350 1007040324 1007706000 1012931900 1006030500 1004050200 1012054400 1007700000 1004050000 1006020000 1007030400 1012080100 1012920510 1007790000 1010070400 1005090000 1004011400 1006033000 1012930500 1001000000 1007010000 1006020400 1007040300 1010070300 1008010000 1003305000 1006030000 1006034000 1012054200 1012080200 1004010000 1003300000 1006032000 1003310000 1004059700 1007031000 1003600000 1004000000 1007030700 1007000000 1006055000 1007050200 1006036000 1012050600 1006000000 1012053900 1012920500 1004050400 1007031100 1007040340 1007020000 1017000000 1012053200 1007030600 1007040320 1003315000 1012054000 1014000000 1007030800 1010000000 1007903000 1010070200 1004051200 1007040330 1004051100 1004051000 1007050100 1012080500 1012053400 1006035500 1012054600 1004100000 1010040200 1012920000 1012920525 1004051400 1006031500 1012921200 1008010500 1012921000 1018000000 1012051200 1010040100 1012931200 1012920516 1007040310 1007780000 1007030200 1004101000 1012920800 1007030100 1007040200 1012053500 1007040000 1007040322 1007031300 1007031140 1012931600 1012931400 1004059900 1003000000 1006036500 1004020200 1010040400 1006033500 1000000000 1012053300 1007990000 1010090100 1007900000 1007030500 1004011600 1012930000 1007030900 1004020000 1007030000 1010070600 1007040100 1007800000 1012050400 1006010000 1007705000 1007702000 1007050000 1002000000 1007031200 1006035000 1006031000 1006034500 1004011200 1007031400 1012920519)))
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all role names as keys and the list of identifier of those zettel with this specific role as a value.
The data object starts with the symbol ''aggregate'' to signal a different format compared to ''meta-list'' above.
Then a string follows, which specifies the key on which the aggregate was performed.
''query'' and ''human'' have the same meaning as above.
The ''symbol'' list starts the result list of aggregates.
Each aggregate starts with a string of the aggregate value, in this case the role value, followed by a list of zettel identifier, denoting zettel which have the given role value.

Similar, to list all tags used in the Zettelstore, send a HTTP GET request to the endpoint ''/z?q=|tags''.
If successful, the output is a JSON object:
If successful, the output is a data object:

```sh
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=json'
# curl 'http://127.0.0.1:23123/z?q=|tags&enc=data'
{"map":{"#api":[:["00001012921000","00001012920800","00001012920522",...],"#authorization":["00001010040700","00001010040400",...],...,"#zettelstore":["00010000000000","00001014000000",...,"00001001000000"]}}
(aggregate "tags" (query "| tags") (human "| tags") (list ("#zettel" 1006034500 1006034000 1006031000 1006020400 1006033500 1006036500 1006032500 1006020100 1006031500 1006030500 1006035500 1006033000 1006020000 1006036000 1006030000 1006032000 1006035000) ("#reference" 1006034500 1006034000 1007800000 1012920500 1006031000 1012931000 1006020400 1012930000 1006033500 1012920513 1007050100 1012920800 1007780000 1012921000 1012920510 1007990000 1006036500 1006032500 1006020100 1012931400 1012931800 1012920516 1012931600 1012920525 1012931200 1006031500 1012931900 1012920000 1005090000 1012920522 1006030500 1007050200 1012921200 1006035500 1012920519 1006033000 1006020000 1006036000 1006030000 1006032000 1012930500 1006035000) ("#graphic" 1008050000) ("#search" 1007700000 1007705000 1007790000 1007780000 1007702000 1007706000 1007031140) ("#installation" 1003315000 1003310000 1003000000 1003305000 1003300000 1003600000) ("#zettelmarkup" 1007900000 1007030700 1007031300 1007030600 1007800000 1007000000 1007031400 1007040100 1007030300 1007031200 1007040350 1007030400 1007030900 1007050100 1007040000 1007030500 1007903000 1007040200 1007040330 1007990000 1007040320 1007050000 1007040310 1007031100 1007040340 1007020000 1007031110 1007031140 1007040324 1007030800 1007031000 1007030000 1007010000 1007906000 1007050200 1007030100 1007030200 1007040300 1007040322) ("#design" 1005000000 1006000000 1002000000 1006050000 1006055000) ("#markdown" 1008010000 1008010500) ("#goal" 1002000000) ("#syntax" 1006010000) ...
```

The JSON object only contains the key ''"map"'' with the value of another object.
This second object contains all tags as keys and the list of identifier of those zettel with this tag as a value.

If you want only those tags that occur at least 100 times, use the endpoint ''/z?q=|MIN100+tags''.
You see from this that actions are separated by space characters.

=== Actions

There are two types of actions: parameters and aggregates.
The following actions are supported:
; ''MINn'' (parameter)
: Emit only those values with at least __n__ aggregated values.
  __n__ must be a positive integer, ''MIN'' must be given in upper-case letters.
; ''MAXn'' (parameter)
: Emit only those values with at most __n__ aggregated values.
  __n__ must be a positive integer, ''MAX'' must be given in upper-case letters.
; ''KEYS'' (aggregate)
: Emit a list of all metadata keys, together with the number of zettel having the key.
; ''REDIRECT'' (aggregate)
: Performs a HTTP redirect to the first selected zettel, using HTTP status code 302.
  The zettel identifier is in the body.
; ''REINDEX'' (aggregate)
: Updates the internal search index for the selected zettel, roughly similar to the [[refresh|00001012080500]] API call.
  It is not really an aggregate, since it is used only for its side effect.
  It is allowed to specify another aggregate.
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]], [[WordSet|00001006036000]], or [[TagSet|00001006034000]] (aggregates)
; Any [[metadata key|00001006020000]] of type [[Word|00001006035500]] or [[TagSet|00001006034000]] (aggregates)
: Emit an aggregate of the given metadata key.
  The key can be given in any letter case.

First, ''REINDEX'' actions are executed, then ''REDIRECT''.
Only the first aggregate action will be executed.
If no ''REDIRECT'' was found the first other aggregate action will be executed.

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.

=== HTTP Status codes
; ''200''
: Query was successful.
; ''204''
: Query was successful, but results in no content.
  Most likely, you specified no appropriate aggregator.
; ''302''
: Query was successful, redirect to first zettel in list.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the access bearer token was not valid, or you forgot to specify a valid query.

Added docs/manual/00001012051600.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012051600
title: API: Determine a tag zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20230928183339
modified: 20230929114937

The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""tag zettel"", i.e. a zettel that documents a given tag.

The query parameter ""''tag''"" allows you to specify a value that is interpreted as the name of a tag.
Zettelstore tries to determine the corresponding tag zettel.

A tag zettel is a zettel with the [[''role''|00001006020100]] value ""tag"" and a title that names the tag.
If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used.

For example, if you want to determine the tag zettel for the tag ""#api"", your request will be:
```sh
# curl -i 'http://127.0.0.1:23123/z?tag=%23api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14

00001019990010
```

Alternatively, you can omit the ''#'' character at the beginning of the tag:
```sh
# curl -i 'http://127.0.0.1:23123/z?tag=api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14

00001019990010
```

If there is a corresponding tag zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the tag zettel.
Its zettel identifier will be returned in the HTTP response body.

If you specified some more query parameter, these will be part of the URL in the response header ''Location'':

```sh
# curl -i 'http://127.0.0.1:23123/z?tag=%23api&part=zettel'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010?part=zettel
Content-Length: 14

00001019990010
```

Otherwise, if no tag zettel was found, the response will use the HTTP status code 404 (""Not found"").

```sh
# curl -i 'http://127.0.0.1:23123/z?tag=notag'
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 29

Tag zettel not found: #notag
```

To fulfill this service, Zettelstore will evaluate internally the query ''role:tag title=TAG'', there ''TAG'' is the actual tag.

Of course, if you are interested in the URL of the tag zettel, you can make use of the HTTP ''HEAD'' method:

```sh
# curl -I 'http://127.0.0.1:23123/z?tag=%23api'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/00001019990010
Content-Length: 14
```

=== HTTP Status codes
; ''302''
: Tag zettel was found.
  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
; ''404''
: No zettel for the given tag was found.

Added docs/manual/00001012051800.zettel.








































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012051800
title: API: Determine a role zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20231128183917
modified: 20231128184701

The [[endpoint|00001012920000]] ''/z'' also allows you to determine a ""role zettel"", i.e. a zettel that documents a given role.

The query parameter ""''role''"" allows you to specify a value that is interpreted as the name of a role.
Zettelstore tries to determine the corresponding role zettel.

A role zettel is a zettel with the [[''role''|00001006020100]] value ""role"" and a title that names the role.
If there is more than one zettel that qualifies, the zettel with the highest zettel identifier is used.

For example, if you want to determine the role zettel for the role ""manual"", your request will be:
```sh
# curl -i 'http://127.0.0.1:23123/z?role=manual'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200
Content-Length: 14

20231128184200
```

If there is a corresponding role zettel, the response will use the HTTP status code 302 (""Found""), the HTTP response header ''Location'' will contain the URL of the role zettel.
Its zettel identifier will be returned in the HTTP response body.

If you specified some more query parameter, these will be part of the URL in the response header ''Location'':

```sh
# curl -i 'http://127.0.0.1:23123/z?role=manual&part=zettel'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200?part=zettel
Content-Length: 14

20231128184200
```

Otherwise, if no role zettel was found, the response will use the HTTP status code 404 (""Not found"").

```sh
# curl -i 'http://127.0.0.1:23123/z?role=norole'
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Content-Length: 30

Role zettel not found: norole
```

To fulfill this service, Zettelstore will evaluate internally the query ''role:role title=ROLE'', there ''ROLE'' is the actual role.

Of course, if you are only interested in the URL of the role zettel, you can make use of the HTTP ''HEAD'' method:

```sh
# curl -I 'http://127.0.0.1:23123/z?role=manual'
HTTP/1.1 302 Found
Content-Type: text/plain; charset=utf-8
Location: /z/20231128184200
Content-Length: 14
```

=== HTTP Status codes
; ''302''
: Role zettel was found.
  The HTTP header ''Location'' contains its URL, the body of the response contains its zettel identifier.
; ''404''
: No zettel for the given role was found.

Changes to docs/manual/00001012053200.zettel.

1
2
3
4
5
6
7

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

26
27

28
29
30
31
32
33
34
35
36
37
38
39
40

41
42
43
44
45
46
47

48
49
50
51
52
53
54
55

56
57
58
59
60
61
62
63
64

65
66
67
68

69
70
1
2
3
4
5
6

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

25


26













27



28



29








30





31
32
33

34
35
36
37

38
39
40






-
+

















-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-

-
-
-
+
-
-
-
-
-
-
-
-
+
-
-
-
-
-



-
+



-
+


id: 00001012053200
title: API: Create a new zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20221219162225
modified: 20230807170416

A zettel is created by adding it to the [[list of zettel|00001012000000]].
Therefore, the [[endpoint|00001012920000]] to create a new zettel is also ''/z'', but you must send the data of the new zettel via a HTTP POST request.


The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data $'title: Note\n\nImportant content.' http://127.0.0.1:23123/z
20210903211500
```

The zettel identifier of the created zettel is returned.
In addition, the HTTP response header contains a key ''Location'' with a relative URL for the new zettel.
A client must prepend the HTTP protocol scheme, the host name, and (optional, but often needed) the post number to make it an absolute URL.


=== Data input
Alternatively, the body of the POST request may contain a JSON object that specifies metadata and content of the zettel to be created.
To do this, you must add the query parameter ''enc=json''.
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The following keys of the JSON object are used:
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"encoding"''
: States how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
  Other values will result in a HTTP response status code ''400''.
; ''"content"''
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.

The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].
Other keys will be ignored.
Even these three keys are just optional.
The body of the HTTP POST request must not be empty and it must contain a JSON object.

Therefore, a body containing just ''{}'' is perfectly valid.
The new zettel will have no content, and only an identifier as [[metadata|00001006020000]]:

The encoding for [[access rights|00001012921200]] must be given, but is ignored.
```
# curl -X POST --data '{}' 'http://127.0.0.1:23123/z?enc=json'
{"id":"20210713161000"}
```
If creating the zettel was successful, the HTTP response will contain a JSON object with one key:
; ''"id"''
: Contains the [[zettel identifier|00001006050000]] of the created zettel for further usage.

You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.
As an example, a zettel with title ""Note"" and content ""Important content."" can be created by issuing:
```
# curl -X POST --data '{"meta":{"title":"Note"},"content":"Important content."}' 'http://127.0.0.1:23123/z?enc=json'
{"id":"20210713163100"}
```

=== HTTP Status codes
; ''201''
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (JSON object or plain text).
: Zettel creation was successful, the body contains its [[zettel identifier|00001006050000]] (data value or plain text).
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Most likely, the JSON was not formed according to above rules.
  Most likely, the symbolic expression was not formed according to above rules.
; ''403''
: You are not allowed to create a new zettel.

Changes to docs/manual/00001012053300.zettel.

1
2
3
4
5
6
7

8
9
10
11

12
13
14
15
16
17

18
19
20
21
22
23
24
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: 00001012053300
title: API: Retrieve metadata and content of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211004093206
modified: 20221219160613
modified: 20241216104429

The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

````sh
# curl 'http://127.0.0.1:23123/z/00001012053300'
The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053300''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

```sh
...
````

Optionally, you may provide which parts of the zettel you are requesting.
In this case, add an additional query parameter ''part=PART''.
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
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







-
+

-
+
+

-
+
-
+


-
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-


-
-
-
+
-
-
-
-
+
+
-
-
+
+

-
-
+
-

-
-



-
+













The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint
...
````

=== JSON output
=== Data output

Alternatively, you may retrieve the zettel as a JSON object by providinv the query parameter ''enc=json'':
Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':

```sh
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=json&part=zettel'
# curl 'http://127.0.0.1:23123/z/00001012053300?enc=data&part=zettel'
{"id":"00001012053300","meta":{"back":"00001012000000 00001012054400","backward":"00001012000000 00001012054400 00001012920000","box-number":"1","created":"20211004093206","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200","modified":"20221219160211","published":"20221219160211","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata and content of an existing zettel"},"encoding":"","content":"The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400") (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000") (box-number "1") (created "20211004093206") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500") (modified "20230703174152") (published "20230703174152") (role "manual") (syntax "zmk") (tags "#api #manual #zettelstore") (title "API: Retrieve metadata and content of an existing zettel")) (rights 62) (encoding "") (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
```

Pretty-printed, this results in:
If you print the result a little bit nicer, you will see its structure:
```
{
  "id": "00001012053300",
  "meta": {
    "back": "00001012000000 00001012054400",
    "backward": "00001012000000 00001012054400 00001012920000",
    "box-number": "1",
    "created": "20211004093206",
    "forward": "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200",
    "modified": "20221219160211",
    "published": "20221219160211",
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata and content of an existing zettel"
(zettel (meta (back "00001006000000 00001012000000 00001012053200 00001012054400")
              (backward "00001006000000 00001012000000 00001012053200 00001012054400 00001012920000")
              (box-number "1")
              (created "20211004093206")
              (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012053400 00001012920000 00001012920800 00001012921200 00001012930500")
              (modified "20230703174152")
              (published "20230703174152")
              (role "manual")
              (syntax "zmk")
              (tags "#api #manual #zettelstore")
              (title "API: Retrieve metadata and content of an existing zettel"))
  },
  "encoding": "",
  "content": "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' (...)
        (rights 62)
        (encoding "")
        (content "The [[endpoint|00001012920000]] to work with metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].\n\nFor example, ...
  "rights": 62
}
```

The following keys of the JSON object are used:
; ''"id"''
: The zettel identifier of the zettel you requested.
* The result is a list, starting with the symbol ''zettel''.
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
; ''"encoding"''
: States how the content is encoded.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.
* ''"encoding"'' states how the content is encoded.
  Currently, only two values are allowed: the empty string (''""'') that specifies an empty encoding, and the string ''"base64"'' that specifies the [[standard Base64 encoding|https://www.rfc-editor.org/rfc/rfc4648.txt]].
  Other values will result in a HTTP response status code ''400''.
; ''"content"''
* The zettel contents is stored as a value of the key ''content''.
: Is a string value that contains the content of the zettel to be created.
  Typically, text content is not encoded, and binary content is encoded via Base64.
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object / plain zettel data.
: Retrieval was successful, the body contains an appropriate data value.
; ''204''
: Request was valid, but there is no data to be returned.
  Most likely, you specified the query parameter ''part=content'', but the zettel does not contain any content.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the [[zettel identifier|00001006050000]] did not consists of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053400.zettel.

1
2
3
4
5
6
7

8
9

10
11
12
13
14
15
16
17
18
19
20

21


22
23
24
25


26
27
28
29
30
31
32
33
34
35
36
37
38











39
40
41
42
43
44

45
46
47
48
49
50
51
52
53
54




55
56
57
58

59
60
61
62
63
64
65
1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
17
18
19
20
21

22
23
24
25


26
27
28
29
30
31









32
33
34
35
36
37
38
39
40
41
42






43

44







45
46
47
48
49
50
51
52

53
54
55
56
57
58
59
60






-
+

-
+











+
-
+
+


-
-
+
+




-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-

-
-
-
-
-
-
-

+
+
+
+



-
+







id: 00001012053400
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20221219161030
modified: 20241216104120

The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header].
The [[endpoint|00001012920000]] to work with metadata of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]][^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header].

To retrieve the plain metadata of a zettel, use the query parameter ''part=meta''

````sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta'
title: API: Retrieve metadata of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
````

=== Data output
To return a JSON object, use also the query parameter ''enc=json''.

Alternatively, you may retrieve the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data'':

```sh
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=json'
{"meta":{"back":"00001012000000 00001012053300","backward":"00001012000000 00001012053300 00001012920000","box-number":"1","created":"20210726174524","forward":"00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200","modified":"20220917175233","published":"20220917175233","role":"manual","syntax":"zmk","tags":"#api #manual #zettelstore","title":"API: Retrieve metadata of an existing zettel"},"rights":62}
# curl 'http://127.0.0.1:23123/z/00001012053400?part=meta&enc=data'
(list (meta (title "API: Retrieve metadata of an existing zettel") (role "manual") (tags "#api #manual #zettelstore") (syntax "zmk") (back "00001012000000 00001012053300") (backward "00001012000000 00001012053300") (box-number "1") (created "20210726174524") (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200") (modified "20230703174515") (published "20230703174515")) (rights 62))
```

Pretty-printed, this results in:
```
{
  "meta": {
    "back": "00001012000000 00001012053300",
    "backward": "00001012000000 00001012053300 00001012920000",
    "box-number": "1",
    "created": "20210726174524",
    "forward": "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200",
    "modified": "20220917175233",
    "published": "20220917175233",
(list (meta (title "API: Retrieve metadata of an existing zettel")
            (role "manual")
            (tags "#api #manual #zettelstore")
            (syntax "zmk")
            (back "00001012000000 00001012053300")
            (backward "00001012000000 00001012053300")
            (box-number "1")
            (created "20210726174524")
            (forward "00001006020000 00001006050000 00001010040100 00001012050200 00001012920000 00001012921200")
            (modified "20230703174515")
            (published "20230703174515"))
    "role": "manual",
    "syntax": "zmk",
    "tags": "#api #manual #zettelstore",
    "title": "API: Retrieve metadata of an existing zettel"
  },
  "rights": 62
      (rights 62))
}
```
The following keys of the JSON object are used:
; ''"meta"''
: References an embedded JSON object with only string values.
  The name/value pairs of this objects are interpreted as the metadata of the new zettel.
  Please consider the [[list of supported metadata keys|00001006020000]] (and their value types).
; ''"rights"''
: An integer number that describes the [[access rights|00001012921200]] for the zettel.

* The result is a list, starting with the symbol ''list''.
* Then, some key/value pairs are following, also nested.
* Nested in ''meta'' are the metadata, each as a key/value pair.
* ''rights'' specifies the [[access rights|00001012921200]] the user has for this zettel.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''

Changes to docs/manual/00001012053500.zettel.

1
2
3
4
5
6
7

8
9
10
11
12


13
14
15


16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

52
53
54
55
56
57
58
59
60
1
2
3
4
5
6

7
8
9
10


11
12
13


14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

51
52
53
54
55
56
57
58
59
60






-
+



-
-
+
+

-
-
+
+


















-
+
















-
+









id: 00001012053500
title: API: Retrieve evaluated metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210726174524
modified: 20230109105303
modified: 20241216104145

The [[endpoint|00001012920000]] to work with evaluated metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sexpr encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sexpr''.
If successful, the output is a JSON object:
For example, to retrieve some evaluated data about this zettel you are currently viewing in [[Sz encoding|00001012920516]], just send a HTTP GET request to the endpoint ''/z/00001012053500''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''enc=sz''.
If successful, the output is a symbolic expression value:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sexpr'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) (TEXT "evaluated") (SPACE) (TEXT "metadata") (SPACE) (TEXT "and") (SPACE) (TEXT "content") (SPACE) (TEXT "of") (SPACE) (TEXT "a") (SPACE) (TEXT "specific") (SPACE) (TEXT "zettel") (SPACE) (TEXT "is") (SPACE) (LITERAL-INPUT () "/z/{ID}") (TEXT ",") (SPACE) (TEXT "where") (SPACE) (LITERAL-INPUT () "{ID}") ...
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=sz'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") (LITERAL-INPUT () "{ID}") (TEXT " is a placeholder for the ") ...
```

To select another encoding, you must provide the query parameter ''enc=ENCODING''.
Others are ""[[html|00001012920510]]"", ""[[text|00001012920519]]"", and some [[more|00001012920500]].
In addition, you may provide a query parameter ''part=PART'' to select the relevant [[part|00001012920800]] of a zettel.
```sh
# curl 'http://127.0.0.1:23123/z/00001012053500?enc=html&part=zettel'
<html>
<head>
<meta charset="utf-8">
<title>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</title>
<meta name="zs-title" content="API: Retrieve evaluated metadata and content of an existing zettel in various encodings">
<meta name="zs-role" content="manual">
<meta name="zs-tags" content="#api #manual #zettelstore">
<meta name="zs-syntax" content="zmk">
<meta name="zs-back" content="00001006000000 00001012000000 00001012053600">
<meta name="zs-backward" content="00001006000000 00001012000000 00001012053600 00001012920000">
<meta name="zs-box-number" content="1">
<meta name="zs-copyright" content="(c) 2020-2022 by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-copyright" content="(c) 2020-present by Detlef Stern <ds@zettelstore.de>">
<meta name="zs-created" content="20210726174524">
<meta name="zs-forward" content="00001006050000 00001010040100 00001012050200 00001012920000 00001012920500 00001012920503 00001012920510 00001012920519 00001012920800">
<meta name="zs-lang" content="en">
<meta name="zs-license" content="EUPL-1.2-or-later">
<meta name="zs-modified" content="20221219161621">
<meta name="zs-published" content="20221219161621">
<meta name="zs-visibility" content="public">
</head>
<body>
<h1>API: Retrieve evaluated metadata and content of an existing zettel in various encodings</h1>
<p>The <a href="00001012920000">endpoint</a> to work with evaluated metadata and content of a specific zettel is <kbd>/z/{ID}</kbd>,
...
```

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012053600.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14

15
16
17
18


19
20
21
22
23
24
25
26

27
28
29
30
31
32
33
1
2
3
4
5
6

7
8
9
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: 00001012053600
title: API: Retrieve parsed metadata and content of an existing zettel in various encodings
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109105034
modified: 20241216104306

The [[endpoint|00001012920000]] to work with parsed metadata and content of a specific zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].

A __parsed__ zettel is basically an [[unevaluated|00001012053500]] zettel: the zettel is read and analyzed, but its content is not __evaluated__.
By using this endpoint, you are able to retrieve the structure of a zettel before it is evaluated.

For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include the a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example, to retrieve some data about this zettel you are currently viewing, just send a HTTP GET request to the endpoint ''/z/00001012053600''[^If [[authentication is enabled|00001010040100]], you must include a valid [[access token|00001012050200]] in the ''Authorization'' header] with the query parameter ''parseonly'' (and other appropriate query parameter).
For example:
```sh
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sexpr&parseonly'
((PARA (TEXT "The") (SPACE) (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (SPACE) (TEXT "to") (SPACE) (TEXT "work") (SPACE) (TEXT "with") (SPACE) ...
# curl 'http://127.0.0.1:23123/z/00001012053600?enc=sz&parseonly'
(BLOCK (PARA (TEXT "The ") (LINK-ZETTEL () "00001012920000" (TEXT "endpoint")) (TEXT " to work with parsed metadata and content of a specific zettel is ") (LITERAL-INPUT () "/z/{ID}") (TEXT ", where ") ...
```

Similar to [[retrieving an encoded zettel|00001012053500]], you can specify an [[encoding|00001012920500]] and state which [[part|00001012920800]] of a zettel you are interested in.
The same default values applies to this endpoint.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
: Retrieval was successful, the body contains an appropriate data value.
; ''400''
: Request was not valid. 
  There are several reasons for this.
  Maybe the zettel identifier did not consist of exactly 14 digits or ''enc'' / ''part'' contained illegal values.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''

Deleted docs/manual/00001012053800.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012053800
title: API: Retrieve context of an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210222151131
modified: 20221208165112

The context of an origin zettel consists of those zettel that are somehow connected to the origin zettel.
Direct connections of an origin zettel to other zettel are visible via [[metadata values|00001006020000]], such as ''backward'', ''forward'' or other values with type [[identifier|00001006032000]] or [[set of identifier|00001006032500]].

The context is defined by a __direction__, a __cost__, and a __limit__:
* Direction: connections are directed.
  For example, the metadata value of ''backward'' lists all zettel that link to the current zettel, while ''forward'' list all zettel to which the current zettel links to.
  When you are only interested in one direction, set the parameter ''dir'' either to the value ""backward"" or ""forward"".
  All other values, including a missing value, is interpreted as ""both"".
* Cost: a normal direct connection has cost 5, an indirect connection is the length of the shortest path between two zettel.
  The cost of a folgezettel or a precursor zettel is just 1, the cost of successor zettel and predecessor zettel are 2.
  You should limit the cost by using the parameter ''cost''.
  Its default value is ""17"".
  A value of ""0"" does disable any cost check.
* Limit: to set an upper bound for the returned context, you should use the parameter ''limit''.
  Its default value is ""200"".
  A value of ""0"" disables does not limit the number of elements returned.

The search for the context of a zettel stops at the [[home zettel|00001004020000#home-zettel]].
This zettel is connected to all other zettel.
If it is included, the context would become too big and therefore unusable.

To retrieve the context of an existing zettel, use the [[endpoint|00001012920000]] ''/x/{ID}''[^Mnemonic: conte**X**t].

````
# curl 'http://127.0.0.1:23123/x/00001012053800?limit=3&dir=forward&cost=5'
{"id": "00001012053800","meta": {...},"rights":62,"list":[{"id": "00001012921000","meta": {...},"rights":62},{"id": "00001012920800","meta": {...},"rights":62},{"id": "00010000000000","meta": {...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001012053800",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001012921000",
      "meta": {...},
      "rights":62
    },
    {
      "id": "00001012920800",
      "meta": {...},
      "rights":62
    },
    {
      "id": "00010000000000",
      "meta": {...},
      "rights":62
    }
  ]
}
````
=== Keys
The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the context was requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that contains the zettel of the context.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Deleted docs/manual/00001012053900.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012053900
title: API: Retrieve unlinked references to an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211119133357
modified: 20220913152019

The value of a personal Zettelstore is determined in part by explicit connections between related zettel.
If the number of zettel grow, some of these connections are missing.
There are various reasons for this.
Maybe, you forgot that a zettel exists.
Or you add a zettel later, but forgot that previous zettel already mention its title.

__Unlinked references__ are phrases in a zettel that mention the title of another, currently unlinked zettel.

To retrieve unlinked references to an existing zettel, use the [[endpoint|00001012920000]] ''/u/{ID}''.

````
# curl 'http://127.0.0.1:23123/u/00001007000000'
{"id": "00001007000000","meta": {...},"rights":62,"list": [{"id": "00001012070500","meta": {...},"rights":62},...{"id": "00001006020000","meta": {...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001007000000",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001012070500",
      "meta": {...},
      "rights": 62
    },
    ...
    {
      "id": "00001006020000",
      "meta": {...},
      "rights": 62
    }
  ]
}
````

This call searches within all zettel whether the title of the specified zettel occurs there.
The other zettel must not link to the specified zettel.
The title must not occur within a link (e.g. to another zettel), in a [[heading|00001007030300]], in a [[citation|00001007040340]], and must have a uniform formatting.
The match must be exact, but is case-insensitive.

If the title of the specified zettel contains some extra character that probably reduce the number of found unlinked references,
you can specify the title phase to be searched for as a query parameter ''phrase'':

````
# curl 'http://127.0.0.1:23123/u/00001007000000?phrase=markdown'
{"id": "00001007000000","meta": {...},"list": [{"id": "00001008010000","meta": {...},"rights":62},{"id": "00001004020000","meta": {...},"rights":62}]}
````

%%TODO: In addition, you are allowed to limit the search by a [[query expression|00001012051840]], which may search for zettel content.

=== Keys
The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the unlinked references were requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe zettel with unlinked references.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Deleted docs/manual/00001012054000.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84




















































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012054000
title: API: Retrieve zettel order within an existing zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
modified: 20220202112451

Some zettel act as a ""table of contents"" for other zettel.
The [[initial zettel|00001000000000]] of this manual is one example, the [[general API description|00001012000000]] is another.
Every zettel with a certain internal structure can act as the ""table of contents"" for others.

What is a ""table of contents""?
Basically, it is just a list of references to other zettel.

To retrieve the ""table of contents"", the software looks at first level [[list items|00001007030200]].
If an item contains a valid reference to a zettel, this reference will be interpreted as an item in the table of contents.

This applies only to first level list items (ordered or unordered list), but not to deeper levels.
Only the first reference to a valid zettel is collected for the table of contents.
Following references to zettel within such an list item are ignored.

To retrieve the zettel order of an existing zettel, use the [[endpoint|00001012920000]] ''/o/{ID}''.

````
# curl http://127.0.0.1:23123/o/00001000000000
{"id":"00001000000000","meta":{...},"rights":62,"list":[{"id":"00001001000000","meta":{...},"rights":62},{"id":"00001002000000","meta":{...},"rights":62},{"id":"00001003000000","meta":{...},"rights":62},{"id":"00001004000000","meta":{...},"rights":62},...,{"id":"00001014000000","meta":{...},"rights":62}]}
````
Formatted, this translates into:[^Metadata (key ''meta'') are hidden to make the overall structure easier to read.]
````json
{
  "id": "00001000000000",
  "meta": {...},
  "rights": 62,
  "list": [
    {
      "id": "00001001000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001002000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001003000000",
      "meta": {...},
      "rights": 62
    },
    {
      "id": "00001004000000",
      "meta": {...},
      "rights": 62
    },
    ...
    {
      "id": "00001014000000",
      "meta": {...},
      "rights": 62
    }
  ]
}
````

The following top-level JSON keys are returned:
; ''id''
: The [[zettel identifier|00001006050000]] for which the references were requested.
; ''meta'':
: The metadata of the zettel, encoded as a JSON object.
; ''rights''
: An integer number that describes the [[access rights|00001012921200]] for the given zettel.
; ''list''
: A list of JSON objects with keys ''id'', ''meta'', and ''rights'' that describe other zettel in the defined order.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
; ''400''
: Request was not valid.
; ''403''
: You are not allowed to retrieve data of the given zettel.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

Changes to docs/manual/00001012054200.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
15
16
17
18
19
20

21
22

23


24
25
26

27
28
29
30

31
32
33
34
35
36
37
38
1
2
3
4
5
6

7
8
9
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: 00001012054200
title: API: Update a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20221219155029
modified: 20231116110417

Updating metadata and content of a zettel is technically quite similar to [[creating a new zettel|00001012053200]].
In both cases you must provide the data for the new or updated zettel in the body of the HTTP request.

One difference is the endpoint.
The [[endpoint|00001012920000]] to update a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP PUT request to that endpoint.

The zettel must be encoded in a [[plain|00001006000000]] format: first comes the [[metadata|00001006010000]] and the following content is separated by an empty line.
This is the same format as used by storing zettel within a [[directory box|00001006010000]].

```
# curl -X POST --data 'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
# curl -X PUT --data $'title: Updated Note\n\nUpdated content.' http://127.0.0.1:23123/z/00001012054200
```

=== Data input
Alternatively, you can use the JSON encoding by using the query parameter ''enc=json''.
Alternatively, you may encode the zettel as a parseable object / a [[symbolic expression|00001012930500]] by providing the query parameter ''enc=data''.
The encoding is the same as the data output encoding when you [[retrieve a zettel|00001012053300#data-output]].

```
# curl -X PUT --data '{}' 'http://127.0.0.1:23123/z/00001012054200?enc=json'
The encoding for [[access rights|00001012921200]] must be given, but is ignored.
```
This will put some empty content and metadata to the zettel you are currently reading.
As usual, some metadata will be calculated if it is empty.

You may encode computed or property [[metadata keys|00001006020000]], but these are also ignored.
The body of the HTTP response is empty, if the request was successful.

=== HTTP Status codes
; ''204''
: Update was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the request body was not valid.

Deleted docs/manual/00001012054400.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: 00001012054400
title: API: Rename a zettel
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20210713150005
modified: 20221219154659

Renaming a zettel is effectively just specifying a new identifier for the zettel.
Since more than one [[box|00001004011200]] might contain a zettel with the old identifier, the rename operation must success in every relevant box to be overall successful.
If the rename operation fails in one box, Zettelstore tries to rollback previous successful operations.

As a consequence, you cannot rename a zettel when its identifier is used in a read-only box.
This applies to all [[predefined zettel|00001005090000]], for example.

The [[endpoint|00001012920000]] to rename a zettel is ''/z/{ID}'', where ''{ID}'' is a placeholder for the [[zettel identifier|00001006050000]].
You must send a HTTP MOVE request to this endpoint, and you must specify the new zettel identifier as an URL, placed under the HTTP request header key ''Destination''.
```
# curl -X MOVE -H "Destination: 10000000000001" http://127.0.0.1:23123/z/00001000000000
```

Only the last 14 characters of the value of ''Destination'' are taken into account and those must form an unused [[zettel identifier|00001006050000]].
If the value contains less than 14 characters that do not form an unused zettel identifier, the response will contain a HTTP status code ''400''.
All other characters, besides those 14 digits, are effectively ignored.
However, the value should form a valid URL that could be used later to [[read the content|00001012053300]] of the freshly renamed zettel.

=== HTTP Status codes
; ''204''
: Rename was successful, there is no body in the response.
; ''400''
: Request was not valid.
  For example, the HTTP header did not contain a valid ''Destination'' key, or the new identifier is already in use.
; ''403''
: You are not allowed to delete the given zettel.
  In most cases you have either not enough [[access rights|00001010070600]] or at least one box containing the given identifier operates in read-only mode.
; ''404''
: Zettel not found.
  You probably used a zettel identifier that is not used in the Zettelstore.

=== Rationale for the MOVE method
HTTP [[standardizes|https://www.rfc-editor.org/rfc/rfc7231.txt]] eight methods.
None of them is conceptually close to a rename operation.

Everyone is free to ""invent"" some new method to be used in HTTP.
To avoid a divergency, there is a [[methods registry|https://www.iana.org/assignments/http-methods/]] that tracks those extensions.
The [[HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)|https://www.rfc-editor.org/rfc/rfc4918.txt]] defines the method MOVE that is quite close to the desired rename operation.
In fact, some command line tools use a ""move"" method for renaming files.

Therefore, Zettelstore adopts somehow WebDAV's MOVE method and its use of the ''Destination'' HTTP header key.

Changes to docs/manual/00001012070500.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

1

2
3
4
5
6

7
8
9
10
11
12
13
14

15
16
17

18
19



20
21
22
23

24
25
26
27
28

29

-
+



+
-
+







-
+


-
+

-
-
-
+
+
+

-
+
+



-
+
id: 00001012070500
title: Retrieve administrative data
title: API: Retrieve administrative data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220304164242
modified: 20220805174216
modified: 20230928190516

The [[endpoint|00001012920000]] ''/x'' allows you to retrieve some (administrative) data.

Currently, you can only request Zettelstore version data.

````
# curl 'http://127.0.0.1:23123/x'
{"major":0,"minor":4,"patch":0,"info":"dev","hash":"cb121cc980-dirty"}
(0 13 0 "dev" "f781dc384b-dirty")
````

Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]].
* Zettelstore conforms somehow to the Standard [[Semantic Versioning|https://semver.org/]].

The names ""major"", ""minor"", and ""patch"" are described in this standard.

The name ""info"" contains sometimes some additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version.
  The first three digits contain the major, minor, and patch version as described in this standard.
* The first string contains additional information, e.g. ""dev"" for a development version, or ""preview"" for a preview version.
* The second string contains data to identify the version from a developers perspective.

The name ""hash"" contains some data to identify the version from a developers perspective.
If any of the three digits has the value -1, its semantic value is unknown.
Similar, the two string might be empty.

=== HTTP Status codes
; ''200''
: Retrieval was successful, the body contains an appropriate JSON object.
: Retrieval was successful, the body contains an appropriate object.

Changes to docs/manual/00001012080200.zettel.

1
2
3
4
5
6
7

8
9

10
11
12
13
14
15
16
1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16






-
+

-
+







id: 00001012080200
title: API: Check for authentication
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20220103224858
modified: 20220908163156
modified: 20241216104549

API clients typically wants to know, whether [[authentication is enabled|00001010040100]] or not.
API clients typically want to know, whether [[authentication is enabled|00001010040100]] or not.
If authentication is enabled, they present some form of user interface to get user name and password for the actual authentication.
Then they try to [[obtain an access token|00001012050200]].
If authentication is disabled, these steps are not needed.

To check for enabled authentication, you must send a HTTP POST request to the [[endpoint|00001012920000]] ''/x'' and you must specify the query parameter ''cmd=authenticated''.

```sh

Changes to docs/manual/00001012080500.zettel.

1
2
3
4
5
6
7

8
9
10
11
12

13
14
15
16
17
18
19
1
2
3
4
5
6

7
8
9
10
11

12
13
14
15
16
17
18
19






-
+




-
+







id: 00001012080500
title: API: Refresh internal data
role: manual
tags: #api #manual #zettelstore
syntax: zmk
created: 20211230230441
modified: 20220923104836
modified: 20241216104624

Zettelstore maintains some internal data to allow faster operations.

One example is the [[content search|00001012051400]] for a term: Zettelstore does not need to scan all zettel to find all occurrences for the term.
Instead, all word are stored internally, with a list of zettel where they occur.
Instead, all words are stored internally, with a list of zettel where they occur.

Another example is the way to determine which zettel are stored in a [[ZIP file|00001004011200]].
Scanning a ZIP file is a lengthy operation, therefore Zettelstore maintains a directory of zettel for each ZIP file.

All these internal data may become stale.
This should not happen, but when it comes e.g. to file handling, every operating systems behaves differently in very subtle ways.

Changes to docs/manual/00001012920000.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
1
2
3
4
5
6

7
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: 00001012920000
title: Endpoints used by the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20221219162335
modified: 20240711183819

All API endpoints conform to the pattern ''[PREFIX]LETTER[/ZETTEL-ID]'', where:
; ''PREFIX''
: is the URL prefix (default: ""/""), configured via the ''url-prefix'' [[startup configuration|00001004010000]],
; ''LETTER''
: is a single letter that specifies the resource type,
; ''ZETTEL-ID''
: is an optional 14 digits string that uniquely [[identify a zettel|00001006050000]].

The following letters are currently in use:

|= Letter:| Without zettel identifier | With [[zettel identifier|00001006050000]] | Mnemonic
| ''a'' | POST: [[client authentication|00001012050200]] | | **A**uthenticate
|       | PUT: [[renew access token|00001012050400]] |
| ''o'' |  | GET: [[list zettel order|00001012054000]] | **O**rder
| ''u'' |  | GET [[unlinked references|00001012053900]] | **U**nlinked
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | GET: [[list zettel context|00001012053800]] | Conte**x**t
| ''x'' | GET: [[retrieve administrative data|00001012070500]] | | E**x**ecute
|       | POST: [[execute command|00001012080100]]
| ''z'' | GET: [[list zettel|00001012051200]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update a zettel|00001012054200]]
| ''z'' | GET: [[list zettel|00001012051200]]/[[query zettel|00001012051400]] | GET: [[retrieve zettel|00001012053300]] | **Z**ettel
|       | POST: [[create new zettel|00001012053200]] | PUT: [[update zettel|00001012054200]]
|       |  | DELETE: [[delete zettel|00001012054600]]
|       |  | MOVE: [[rename zettel|00001012054400]]

The full URL will contain either the ""http"" oder ""https"" scheme, a host name, and an optional port number.

The API examples will assume the ""http"" schema, the local host ""127.0.0.1"", the default port ""23123"", and the default empty ''PREFIX'' ""/"".
Therefore, all URLs in the API documentation will begin with ""http://127.0.0.1:23123/"".

Changes to docs/manual/00001012920500.zettel.

1
2

3
4
5
6
7

8
9

10
11
12

13

14
15
16
1

2
3
4
5
6

7
8
9
10
11
12
13
14

15
16

17

-
+




-
+


+



+
-
+

-

id: 00001012920500
title: Encodings available via the [[API|00001012000000]]
title: Encodings available via the API
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109104839
modified: 20230403123653

A zettel representation can be encoded in various formats for further processing.
These will be retrieved via the [[API|00001012000000]].

* [[html|00001012920510]]
* [[md|00001012920513]]
* [[shtml|00001012920525]]
* [[sexpr|00001012920516]]
* [[sz|00001012920516]]
* [[text|00001012920519]]
* [[zjson|00001012920503]] (will be deprecated in v0.11)
* [[zmk|00001012920522]]

Deleted docs/manual/00001012920503.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






































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920503
title: ZJSON Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20230109104722

**Note**: ZJSON encoding will be deprecated in v0.11
---
A zettel representation that allows to process the syntactic structure of a zettel.
It is a JSON-based encoding format, but different to the structures returned by using the plain [[endpoint|00001012920000]] ''/z/{ID}''.

For an example, take a look at the ZJSON encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920503?enc=zjson&part=zettel]],
* [[//z/00001012920503?enc=zjson&part=meta]],
* [[//z/00001012920503?enc=zjson&part=content]].

If transferred via HTTP, the content type will be ''application/json''.

A full zettel encoding results in a JSON object with two keys: ''"meta"'' and ''"content"''.
Both values are the same as if you have requested just the appropriate [[part|00001012920800]].

=== Encoding of metadata
Metadata encoding results in a JSON object, where each metadata key is mapped to the same JSON object name.
The associated value is itself a JSON object with two names.
The first name ``""`` references the [[metadata key type|00001006030000]].
Depending on the key type, the other name denotes the value of the metadata element.
The meaning of these names is [[well defined|00001012920582]], as well as the [[mapping of key types to used object names|00001012920584]].

=== Encoding of zettel content
The content encoding results in a JSON array of objects, where each objects represents a Zettelmarkup element.

Every [!zettelmarkup|Zettelmarkup] element is encoded as a JSON object.
These objects always contain the empty name ''""'' with a string value describing the type of Zettelmarkup element.
Depending on the type, other one letter names denotes the details of the element.
The meaning of these names is [[well defined|00001012920588]].

Changes to docs/manual/00001012920510.zettel.

1
2
3
4
5

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





+







id: 00001012920510
title: HTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210726193034

A zettel representation in HTML.
This representation is different form the [[web user interface|00001014000000]] as it contains the zettel representation only and no additional data such as the menu bar.

It is intended to be used by external clients.

Changes to docs/manual/00001012920516.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
1

2
3
4
5
6

7
8

9
10


11


12
13



14
15
16
17


18


19



20



















-
+




-
+

-
+

-
-
+
-
-
+

-
-
-
+
+
+

-
-
+
-
-

-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920516
title: Sexpr Encoding
title: Sz Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220422181104
modified: 20230109104812
modified: 20230403161458

A zettel representation that is a [[s-expression|https://en.wikipedia.org/wiki/S-expression]] (also known as symbolic expression).
A zettel representation that is a [[s-expression|00001012930000]] (also known as symbolic expression).

It is an alternative to the [[ZJSON encoding|00001012920503]][^ZJSON will be deprecated in v0.11].
Both encodings are (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.
It is (relatively) easy to parse and contain all relevant information of a zettel, metadata and content.

For example, take a look at the Sexpr encoding of this page, which is available via the ""Info"" sub-page of this zettel: 
For example, take a look at the Sz encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920516?enc=sexpr&part=zettel]],
* [[//z/00001012920516?enc=sexpr&part=meta]],
* [[//z/00001012920516?enc=sexpr&part=content]].
* [[//z/00001012920516?enc=sz&part=zettel]],
* [[//z/00001012920516?enc=sz&part=meta]],
* [[//z/00001012920516?enc=sz&part=content]].

If transferred via HTTP, the content type will be ''text/plain''.

Some zettel describe the [[Sz encoding|00001012931000]] in a more detailed way.
=== Syntax of s-expressions
There are only two types of elements: atoms and lists.

A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.

If transferred via HTTP, the content type will be ''text/plain''.
There are three syntactic forms for an atom: numbers, symbols and strings.

A number is a non-empty sequence of digits (""0"" ... ""9"").
The smallest number is ``0``, there are no negative numbers.

A symbol is a non-empty sequence of printable characters, except left or right parenthesis.
Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S).
Symbols are case-insensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote the same symbol.

A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark.
To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C).
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

Atoms are separated by Unicode characters of category separator (Z).

Changes to docs/manual/00001012920519.zettel.

1
2
3
4
5

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





+







id: 00001012920519
title: Text Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20210726193119

A zettel representation contains just all textual data of a zettel.
Could be used for creating a search index.

Every line may contain zero, one, or more words, separated by space character.

Changes to docs/manual/00001012920522.zettel.

1
2
3
4
5

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





+






id: 00001012920522
title: Zmk Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20211124140857

A zettel representation that tries to recreate a [[Zettelmarkup|00001007000000]] representation of the zettel.
Useful if you want to convert [[other markup languages|00001008000000]] to Zettelmarkup (e.g. [[Markdown|00001008010000]]).

If transferred via HTTP, the content type will be ''text/plain''.

Added docs/manual/00001012920525.zettel.





































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012920525
title: SHTML Encoding
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230316181044
modified: 20230403150657

A zettel representation that is a [[s-expression|00001012930000]], syntactically similar to the [[Sz encoding|00001012920516]], but denotes [[HTML|00001012920510]] semantics.
It is derived from a XML encoding in s-expressions, called [[SXML|https://en.wikipedia.org/wiki/SXML]].

It is (relatively) easy to parse and contains everything to transform it into real HTML.
In contrast to HTML, SHTML is easier to parse and to manipulate.
For example, take a look at the SHTML encoding of this page, which is available via the ""Info"" sub-page of this zettel: 

* [[//z/00001012920525?enc=shtml&part=zettel]],
* [[//z/00001012920525?enc=shtml&part=meta]],
* [[//z/00001012920525?enc=shtml&part=content]].

If transferred via HTTP, the content type will be ''text/plain''.

Internally, if a zettel should be transformed into HTML, the zettel is translated into the [[Sz encoding|00001012920516]], which is transformed into this SHTML encoding to produce the [[HTML encoding|00001012920510]].

=== Syntax of SHTML
There are only two types of elements: atoms and lists, similar to the Sz encoding.

A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.
Before the last element of a list of at least to elements, a full stop character (""''.''"", U+002E) signal a pair as the last two elements.
This allows a more space economic storage of data.

An HTML tag like ``< a href="link">Text</a>`` is encoded in SHTML with a list, where the first element is a symbol named a the tag.
The second element is an optional encoding of the tag's attributes.
Further elements are either other tag encodings or a string.
The above tag is encoded as ``(a (@ (href . "link")) "Text")``.
Also possible is to encode the attribute without pairs: ``(a (@ (href "link")) "Text")`` (note the missing full stop character).

Deleted docs/manual/00001012920582.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920582
title: ZJSON Encoding: List of Valid Metadata Value Objects Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104713

**Note**: ZJSON encoding will be deprecated in v0.11
---
Every Metadata value element is mapped to a JSON object with some well defined names / keys.

|=Name | JSON Value | Meaning
| ''"\"'' | string | The type of the Zettelmarkup element.
| ''"i"'' | array  | A sequence of [[inline-structured|00001007040000]] elements.
| ''"s"'' | string | The first / major string value of an element.
| ''"y"'' | array  | A set of string values.

Deleted docs/manual/00001012920584.zettel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30






























-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
id: 00001012920584
title: ZJSON Encoding: Mapping of Metadata Key Types to Object Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104703

**Note**: ZJSON encoding will be deprecated in v0.11
---
Every [[Metadata key|00001006030000]] is mapped to an [[object name|00001012920582]] where its value is encoded.

|=Type | JSON Object Name | Remark
| [[Credential|00001006031000]] | ''"s"'' | A string with the decrypted credential.
| [[EString|00001006031500]] | ''"s"'' | A possibly empty string.
| [[Identifier|00001006032000]] | ''"s"'' | A string containing a [[zettel identifier|00001006050000]].
| [[IdentifierSet|00001006032500]] | ''"y"'' | An array of strings containing [[zettel identifier|00001006050000]].
| [[Number|00001006033000]] | ''"s"'' | A string containing a numeric value.
| [[String|00001006033500]] | ''"s"'' | A non-empty string.
| [[TagSet|00001006034000]] | ''"y"'' | An array of string containing zettel tags.
| [[Timestamp|00001006034500]] | ''"s"''  | A string containing a timestamp in the format YYYYMMDDHHmmSS.
| [[URL|00001006035000]] | ''"s"'' | A string containing an URL.
| [[Word|00001006035500]] | ''"s"'' | A string containing a word (no space characters)
| [[WordSet|00001006036000]] | ''"y"'' | An array of strings containing words.
| [[Zettelmarkup|00001006036500]] | ''"i"'' | A sequence of [[inline-structured|00001007040000]] elements.

Please note, that metadata is weakly typed.
Every metadata key expects a certain type.
But the user is free to enter something different.
For example, even if the metadata type is ""number"", its value could still be ""abc"".
However, the mapping itself is always valid.

Deleted docs/manual/00001012920588.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: 00001012920588
title: ZJSON Encoding: List of Valid Zettelmarkup Element Objects Names
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
modified: 20230109104648

**Note**: ZJSON encoding will be deprecated in v0.11
---

Every [[Zettelmarkup|00001007000000]] element is mapped to a JSON object with some well defined names / keys.

|=Name | JSON Value | Meaning
| ''"\"'' | string | The type of the Zettelmarkup element.
| ''"a"'' | object | Additional attributes of the element.
| ''"b"'' | array  | A sequence of [[block-structured|00001007030000]] elements.
| ''"c"'' | array  | A sequence of a sequence of (sub-) list elements or [[inline-structured|00001007040000]] elements. Used for nested lists.
| ''"d"'' | array  | A sequence of description list elements, where each element is an object of a definition term and a list of descriptions.
| ''"e"'' | array  | A sequence of descriptions: a JSON array of simple description, which is itself a JSON array of block structured elements.
| ''"i"'' | array  | A sequence of [[inline-structured|00001007040000]] elements.
| ''"j"'' | object | An objects describing a BLOB element.
| ''"n"'' | number | A numeric value, e.g. for specifying the [[heading|00001007030300]] level.
| ''"o"'' | string | A base64 encoded binary value. Used in some BLOB elements.
| ''"p"'' | array  | A sequence of two elements: a sequence of [[table|00001007031000]] header value, followed by a sequence of sequence of table row values.
| ''"q"'' | string | A second string value, if ''""s""'' is already used.
| ''"s"'' | string | The first / major string value of an element.
| ''"v"'' | string | A third string value, if ''""q""'' is already used.

Changes to docs/manual/00001012920800.zettel.

1
2
3
4
5

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





+







id: 00001012920800
title: Values to specify zettel parts
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20210126175322
modified: 20220214175335

When working with [[zettel|00001006000000]], you could work with the whole zettel, with its metadata, or with its content:
; [!zettel|''zettel'']
: Specifies that you work with a zettel as a whole.
  Contains identifier, metadata, and content of a zettel.
; [!meta|''meta'']

Changes to docs/manual/00001012921000.zettel.

1
2


3
4
5


6
7
8
9

10
11
12
13
14











1

2
3
4
5

6
7
8
9
10

11
12




13
14
15
16
17
18
19
20
21
22
23

-
+
+


-
+
+



-
+

-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
id: 00001012921000
title: API: JSON structure of an access token
title: API: Structure of an access token
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
role: manual
created: 20210126175322
modified: 20230807165915

If the [[authentication process|00001012050200]] was successful, an access token with some additional data is returned.
The same is true, if the access token was [[renewed|00001012050400]].
The response is structured as an JSON object, with the following named values:
The response is structured as a [[symbolic expression|00001012930000]] list, with the following elements:

|=Name|Description
|''access_token''|The access token itself, as string value, which is a [[JSON Web Token|https://tools.ietf.org/html/rfc7519]] (JWT, RFC 7915)
|''token_type''|The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
|''expires_in''|An integer that gives a hint about the lifetime / endurance of the token, measured in seconds
# The type of the token, always set to ''"Bearer"'', as described in [[RFC 6750|https://tools.ietf.org/html/rfc6750]]
# The token itself, which is technically the string representation of a [[symbolic expression|00001012930500]] containing relevant data, plus a check sum.
#* The symbolic expression has the form ''(KIND USERNAME NOW EXPIRE Z-ID)''
#* ''KIND'' is ''0'' for an API access, ''1'' if it created for the Web user interface.
#* ''USERNAME'' is the user name of the user.
#* ''NOW'' is a timestamp of the current time.
#* ''EXPIRE'' is the timestamp when the access token expires.
#* ''Z-ID'' is the zettel identifier of the user zettel.
  The symbolic expression is encoded via ""base64"".
  Based on this encoding, a checksum is calculated, also encoded via ""base64"".
  Both encoded values are concatenated, with a period (''"."'') as a delimiter.

Changes to docs/manual/00001012921200.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
1
2
3
4
5
6

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: 00001012921200
title: API: Encoding of Zettel Access Rights
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20220201173115
modified: 20220201171959
modified: 20240711183931

Various API calls return a JSON key ''"rights"'' that encodes the access rights the user currently has.
It is an integer number between 0 and 62.[^Not all values in this range are used.]
Various API calls return a symbolic expression list ''(rights N)'', with ''N'' as a number, that encodes the access rights the user currently has.
''N'' is an integer number between 0 and 62.[^Not all values in this range are used.]

The value ""0"" signals that something went wrong internally while determining the access rights.

A value of ""1"" says, that the current user has no access right for the given zettel.
In most cases, this value will not occur, because only zettel are presented, which are at least readable by the current user.

Values ""2"" to ""62"" are binary encoded values, where each bit signals a special right.

|=Bit number:|Bit value:|Meaning
| 1 |  2 | User is allowed to create a new zettel
| 2 |  4 | User is allowed to read the zettel
| 3 |  8 | User is allowed to update the zettel
| 4 | 16 | User is allowed to rename the zettel
| 4 | 16 | (not in use; was assigned to an operation)
| 5 | 32 | User is allowed to delete the zettel

The algorithm to calculate the actual access rights from the value is relatively simple:
# Search for the biggest bit value that is less than the rights value.
  This is an access right for the current user.
# Subtract the bit value from the rights value.
  Remember the difference as the new rights value.

Added docs/manual/00001012930000.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: 00001012930000
title: Symbolic Expression
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403145644
modified: 20230403154010

A symbolic expression (also called __s-expression__) is a notation of a list-based tree.
Inner nodes are lists of arbitrary length, outer nodes are primitive values (also called __atoms__) or the empty list.

A symbolic expression is either
* a primitive value (__atom__), or
* a list of the form __(E,,1,, E,,2,, &hellip; E,,n,,)__, where __E,,i,,__ is itself a symbolic expression, separated by space characters.

An atom is a number, a string, or a symbol.

Symbolic expressions are used in programming languages like LISP or Scheme, where they denote both data structures and the program itself.
This allows a LISP or Scheme program to process LISP or Scheme programs.
That property is also used within Zettelstore.

Symbolic expressions are relatively easy to read, to parse, and to process.

=== See also
* [[Syntax|00001012930500]] of symbolic expressions in the Zettelstore
* [[S-expression @ Wikipedia|https://en.wikipedia.org/wiki/S-expression]]

Added docs/manual/00001012930500.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012930500
title: Syntax of Symbolic Expressions
role: manual
tags: #manual #reference #zettelstore
syntax: zmk
created: 20230403151127
modified: 20240413160345

=== Syntax of lists
A list always starts with the left parenthesis (""''(''"", U+0028) and ends with a right parenthesis (""'')''"", U+0029).
A list may contain a possibly empty sequence of elements, i.e. lists and / or atoms.

Internally, lists are composed of __cells__.
A cell allows to store two values.
The first value references the first element of a list.
The second value references the rest of the list, or is the special value __nil__ if there is no rest of the list.

However, it is possible to store an atom as the second value of the last cell.
In this case, before the last element of a list of at least two elements, a full stop character (""''.''"", U+002E) signals such a cell as the last two elements.
This allows a more space economic storage of data.

A __proper__ list, which contains __nil__ as the second value of the last element might be pictured as follows:

~~~draw
+---+---+   +---+---+         +---+---+
| V | N +-->| V | N +-->   -->| V |   |
+-+-+---+   +-+-+---+         +-+-+---+
  |           |                 |
  v           v                 v
+-------+   +-------+         +-------+
| Elem1 |   | Elem2 |         | ElemN |
+-------+   +-------+         +-------+
~~~

''V'' is a placeholder for a value, ''N'' is the reference to the next cell (also known as the rest / tail of the list).
Above list will be represented as an symbolic expression as ''(Elem1 Elem2 ... ElemN)''

An improper list will have a non-__nil__ reference to an atom as the very last element

~~~draw
+---+---+   +---+---+         +---+---+
| V | N +-->| V | N +-->   -->| V | V |
+-+-+---+   +-+-+---+         +-+-+-+-+
  |           |                 |   |
  v           v                 v   v
+-------+   +-------+    +-------+ +------+
| Elem1 |   | Elem2 |    | ElemN | | Atom |
+-------+   +-------+    +-------+ +------+
~~~

Above improper list will be represented as an symbolic expression as ''(Elem1 Elem2 ... ElemN . Atom)''


=== Syntax of numbers (atom)
A number is a non-empty sequence of digits (""0"" ... ""9"").
The smallest number is ``0``, there are no negative numbers.

=== Syntax of symbols (atom)
A symbol is a non-empty sequence of printable characters, except left or right parenthesis.
Unicode characters of the following categories contains printable characters in the above sense: letter (L), number (N), punctuation (P), symbol (S).
Symbols are case-sensitive, i.e. ""''ZETTEL''"" and ""''zettel''"" denote different symbols.

=== Syntax of string (atom)

A string starts with a quotation mark (""''"''"", U+0022), contains a possibly empty sequence of Unicode characters, and ends with a quotation mark.
To allow a string to contain a quotations mark, it must be prefixed by one backslash (""''\\''"", U+005C).
To allow a string to contain a backslash, it also must be prefixed by one backslash.
Unicode characters with a code less than U+FF are encoded by by the sequence ""''\\xNM''"", where ''NM'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFF are encoded by by the sequence ""''\\uNMOP''"", where ''NMOP'' is the hex encoding of the character.
Unicode characters with a code less than U+FFFFFF are encoded by by the sequence ""''\\UNMOPQR''"", where ''NMOPQR'' is the hex encoding of the character.
In addition, the sequence ""''\\t''"" encodes a horizontal tab (U+0009), the sequence ""''\\n''"" encodes a line feed (U+000A).

=== See also
* Currently, Zettelstore uses [[Sx|https://t73f.de/r/sx]] (""Symbolic eXpression framework"") to implement symbolic expressions.
  The project page might contain additional information about the full syntax.

  Zettelstore only uses lists, numbers, string, and symbols to represent zettel.

Added docs/manual/00001012931000.zettel.

















































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931000
title: Encoding of Sz
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403153903
modified: 20240123120319

Zettel in a [[Sz encoding|00001012920516]] are represented as a [[symbolic expression|00001012930000]].
To process these symbolic expressions, you need to know, how a specific part of a zettel is represented by a symbolic expression.

Basically, each part of a zettel is represented as a list, often a nested list.
The first element of that list is always an unique symbol, which denotes that part.
The meaning / semantic of all other elements depend on that symbol.

=== Zettel
A full zettel is represented by a list of two elements.
The first elements represents the metadata, the second element represents the zettel content.

:::syntax
__Zettel__ **=** ''('' [[__Metadata__|#metadata]] [[__Content__|#content]] '')''.
:::

=== Metadata

Metadata is represented by a list, where the first element is the symbol ''META''.
Following elements represent each metadatum[^""Metadatum"" is used as the singular form of metadata.] of a zettel in standard order.

Standard order is: [[Title|00001006020000#title]], [[Role|00001006020000#role]], [[Tags|00001006020000#tags]], [[Syntax|00001006020000#syntax]], all other [[keys|00001006020000]] in alphabetic order.

:::syntax
__Metadata__ **=** ''(META'' [[__Metadatum__|00001012931200]] &hellip; '')''.
:::
=== Content

Zettel content is represented by a block.
:::syntax
__Content__ **=** [[__Block__|#block]].
:::

==== Block
A block is represented by a list with the symbol ''BLOCK'' as the first element.
All following elements represent a nested [[block-structured element|00001007030000]].

:::syntax
[!block|__Block__] **=** ''(BLOCK'' [[__BlockElement__|00001012931400]] &hellip; '')''.
:::

==== Inline
Both block-structured elements and some metadata values may contain [[inline-structured elements|00001007040000]].
Similar, inline-structured elements are represented as follows:

:::syntax
__Inline__ **=** ''(INLINE'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::

==== Attribute
[[Attributes|00001007050000]] may be specified for both block- and inline- structured elements.
Attributes are represented by the following schema.

:::syntax
__Attribute__ **=** ''('' **[** [[__AttributeKeyValue__|00001012931800]] &hellip; **]** ')'.
:::

Either, there are no attributes.
These are specified by the empty list ''()''.
Or there are attributes.
In this case, the first element of the list must be the symbol ''quote'': ''(quote'' ''('' A,,1,, A,,2,, &hellip; A,,n,, '')'''')''.

=== Other
A list with ''UNKNOWN'' as its first element signals an internal error during transforming a zettel into the Sz encoding.
It may be ignored, or it may produce an error.

:::syntax
__Unknown__ **=** ''(UNKNOWN'' Object &hellip; '')''.
:::

The list may only contain the symbol ''UNKNOWN'', or additionally an unlimited amount of other objects.

Similar, any symbol with the pattern ''**xyz:NOT-FOUND**'', where ''xyz'' is any string, signals an internal error.

Added docs/manual/00001012931200.zettel.




































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931200
title: Encoding of Sz Metadata
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161618
modified: 20240219161848

A single metadata (""metadatum"") is represented by a triple: a symbol representing the type, a symbol representing the key, and either a string or a list that represent the value.

The symbol depends on the [[metadata key type|00001006030000]].
The value also depends somehow on the key type: a set of values is represented as a list, all other values are represented by a string, even if it is a number.

The following table maps key types to symbols and to the type of the value representation.

|=Key Type<| Symbol<| Value<
| [[Credential|00001006031000]] | ''CREDENTIAL'' | string
| [[EString|00001006031500]] | ''EMPTY-STRING'' | string
| [[Identifier|00001006032000]] | ''ZID'' | string
| [[IdentifierSet|00001006032500]] | ''ZID-SET'' | ListValue
| [[Number|00001006033000]] | ''NUMBER'' | string
| [[String|00001006033500]] | ''STRING'' | string
| [[TagSet|00001006034000]] | ''TAG-SET'' | ListValue
| [[Timestamp|00001006034500]] | ''TIMESTAMP'' | string
| [[URL|00001006035000]] | ''URL'' | string
| [[Word|00001006035500]] | ''WORD'' | string
| [[Zettelmarkup|00001006036500]] | ''ZETTELMARKUP'' | string

:::syntax
__ListValue__ **=** ''('' String,,1,, String,,2,, &hellip; String,,n,, '')''.
:::

Examples:
* The title of this zettel is represented as: ''(EMPTY-STRING title "Encoding of Sz Metadata")''
* The tags of this zettel are represented as: ''(TAG-SET tags ("#api" "#manual" "#reference" "#zettelstore"))''

Added docs/manual/00001012931400.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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931400
title: Encoding of Sz Block Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161803
modified: 20241216104739

=== ''PARA''
:::syntax
__Paragraph__ **=** ''(PARA'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A paragraph is just a list of inline elements.

=== ''HEADING''
:::syntax
__Heading__ **=** ''(HEADING'' Number [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A heading consists of a number, which specifies its level (1 -- 5), its optional attributes.
The first string is a ""slug"" of the heading text, i.e. transformed it to lower case, replaced space character with minus sign, and some more.
The second string is the slug, but made unique for the whole zettel.
Then the heading text follows as a sequence of inline elements.

=== ''THEMATIC''
:::syntax
__Thematic__ **=** ''(THEMATIC'' [[__Attributes__|00001012931000#attribute]] '')''.
:::

=== ''ORDERED'', ''UNORDERED'', ''QUOTATION''
These three symbols are specifying different kinds of lists / enumerations: an ordered list, an unordered list, and a quotation list.
Their structure is the same.

:::syntax
__OrderedList__ **=** ''(ORDERED'' __ListElement__ &hellip; '')''.

__UnorderedList__ **=** ''(UNORDERED'' __ListElement__ &hellip; '')''.

__QuotationList__ **=** ''(QUOTATION'' __ListElement__ &hellip; '')''.
:::

:::syntax
__ListElement__ **=** [[__Block__|00001012931000#block]] **|** [[__Inline__|00001012931000#inline]].
:::
A list element is either a block or an inline.
If it is a block, it may contain a nested list.
=== ''DESCRIPTION''
:::syntax
__Description__ **=** ''(DESCRIPTION'' __DescriptionTerm__ __DescriptionValues__ __DescriptionTerm__ __DescriptionValues__ &hellip; '')''.
:::
A description is a sequence of one or more terms and values.

:::syntax
__DescriptionTerm__ **=** ''('' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A description term is just an inline-structured value.

:::syntax
__DescriptionValues__ **=** ''(BLOCK'' [[__Block__|00001012931000#block]] &hellip; '')''.
:::
Description values are sequences of blocks.

=== ''TABLE''
:::syntax
__Table__ **=** ''(TABLE'' __TableHeader__ __TableRow__ &hellip; '')''.
:::
A table is a table header and a sequence of table rows.

:::syntax
__TableHeader__ **=** ''()'' **|** ''('' __TableCell__ &hellip; '')''.
:::
A table header is either the empty list or a list of table cells.

:::syntax
__TableRow__ **=** ''('' __TableCell__ &hellip; '')''.
:::
A table row is a list of table cells.

=== ''CELL'', ''CELL-*''
There are four kinds of table cells, one for each possible cell alignment.
The structure is the same for all kind.

:::syntax
__TableCell__ **=** __DefaultCell__ **|** __CenterCell__ **|** __LeftCell__ **|** __RightCell__.
:::

:::syntax
__DefaultCell__ **=** ''(CELL'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The cell content, specified by the sequence of inline elements, used the default alignment.

:::syntax
__CenterCell__ **=** ''(CELL-CENTER'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The cell content, specified by the sequence of inline elements, is centered aligned.

:::syntax
__LeftCell__ **=** ''(CELL-LEFT'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The cell content, specified by the sequence of inline elements, is left aligned.

:::syntax
__RightCell__ **=** ''(CELL-RIGHT'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The cell content, specified by the sequence of inline elements, is right aligned.

=== ''REGION-*''
The following lists specifies different kinds of regions.
A region treat a sequence of block elements to be belonging together in certain ways.
The have a similar structure.

:::syntax
__BlockRegion__ **=** ''(REGION-BLOCK'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to belong in an unspecified way.
Typically, the reason is given in the attributes.
The inline describes the block.

:::syntax
__QuoteRegion__ **=** ''(REGION-QUOTE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to contain a longer quotation.
Attributes may further specify the quotation.
The inline typically describes author / source of the quotation.

:::syntax
__VerseRegion__ **=** ''(REGION-VERSE'' [[__Attributes__|00001012931000#attribute]] ''('' [[__BlockElement__|00001012931400]] &hellip; '')'' [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
A block region just treats the block to contain a verse.
Soft line breaks are transformed into hard line breaks to save the structure of the verse / poem.
Attributes may further specify something.
The inline typically describes author / source of the verse.

=== ''VERBATIM-*''
The following lists specifies some literal text of more than one line.
The structure is always the same, the initial symbol denotes the actual usage.
The content is encoded as a string, most likely to contain control characters that signals the end of a line.

:::syntax
__CommentVerbatim__ **=** ''(VERBATIM-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as an internal comment not to be interpreted further.

:::syntax
__EvalVerbatim__ **=** ''(VERBATIM-EVAL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be evaluated by an (external) software to produce some derived content.

:::syntax
__HTMLVerbatim__ **=** ''(VERBATIM-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains HTML code.

:::syntax
__MathVerbatim__ **=** ''(VERBATIM-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as special code to be interpreted as mathematical formulas.

:::syntax
__CodeVerbatim__ **=** ''(VERBATIM-CODE'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (executable) code.

:::syntax
__ZettelVerbatim__ **=** ''(VERBATIM-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.

=== ''BLOB''
:::syntax
__BLOB__ **=** ''(BLOB'' ''('' [[__InlineElement__|00001012931600]] &hellip; '')'' String,,1,, String,,2,, '')''.
:::
A BLOB contains an image in block mode.
The inline elements states some description.
The first string contains the syntax of the image.
The second string contains the actual image.
If the syntax is ""SVG"", then the second string contains the SVG code.
Otherwise the (binary) image data is encoded with base64.

=== ''TRANSCLUDE''
:::syntax
__Transclude__ **=** ''(TRANSCLUDE'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] '')''.
:::
A transclude list only occurs for a parsed zettel, but not for a evaluated zettel.
Evaluating a zettel also means that all transclusions are resolved.

__Reference__ denotes the zettel to be transcluded.

Added docs/manual/00001012931600.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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931600
title: Encoding of Sz Inline Elements
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161845
modified: 20241216104906

=== ''TEXT''
:::syntax
__Text__ **=** ''(TEXT'' String '')''.
:::
Specifies the string as some text content, including white space characters.

=== ''SOFT''
:::syntax
__Soft__ **=** ''(SOFT)''.
:::
Denotes a soft line break.
It will be often encoded as a space character, but signals the point in the textual content, where a line break occurred.

=== ''HARD''
:::syntax
__Hard__ **=** ''(HARD)''.
:::
Specifies a hard line break, i.e. the user wants to have a line break here.

=== ''LINK-*''
The following lists specify various links, based on the full reference.
They all have the same structure, with a trailing sequence of __InlineElements__ that contain the linked text.

:::syntax
__InvalidLink__ **=** ''(LINK-INVALID'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the invalid link specification.

:::syntax
__ZettelLink__ **=** ''(LINK-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the zettel identifier, a zettel reference.

:::syntax
__SelfLink__ **=** ''(LINK-SELF'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the number sign character and the name of a zettel mark.
It reference the same zettel where it occurs.

:::syntax
__FoundLink__ **=** ''(LINK-FOUND'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a zettel identifier, a zettel reference, of a zettel known to be included in the Zettelstore.

:::syntax
__BrokenLink__ **=** ''(LINK-BROKEN'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a zettel identifier, a zettel reference, of a zettel known to be __not__ included in the Zettelstore.

:::syntax
__HostedLink__ **=** ''(LINK-HOSTED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a link starting with one slash character, denoting an absolute local reference.

:::syntax
__BasedLink__ **=** ''(LINK-BASED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a link starting with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL.

:::syntax
__QueryLink__ **=** ''(LINK-BASED'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a [[query expression|00001007700000]].

:::syntax
__ExternalLink__ **=** ''(LINK-EXTERNAL'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains a full URL, referencing a resource outside of the Zettelstore server.

=== ''EMBED''
:::syntax
__Embed__ **=** ''(EMBED'' [[__Attributes__|00001012931000#attribute]] [[__Reference__|00001012931900]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
Specifies an embedded element, in most cases some image content or an inline transclusion.
A transclusion will only be used, if the zettel content was not evaluated.

__Reference__ denoted the referenced zettel.

If the string value is empty, it is an inline transclusion.
Otherwise it contains the syntax of an image.

The __InlineElement__ at the end of the list is interpreted as describing text.

=== ''EMBED-BLOB''
:::syntax
__EmbedBLOB__ **=** ''(EMBED-BLOB'' [[__Attributes__|00001012931000#attribute]] String,,1,, String,,2,, '')''.
:::
If used if some processed image has to be embedded inside some inline material.
The first string specifies the syntax of the image content.
The second string contains the image content.
If the syntax is ""SVG"", the image content is not encoded further.
Otherwise a base64 encoding is used.

=== ''CITE''
:::syntax
__CiteBLOB__ **=** ''(CITE'' [[__Attributes__|00001012931000#attribute]] String [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The string contains the citation key.

=== ''MARK''
:::syntax
__Mark__ **=** ''(MARK'' String,,1,, String,,2,, String,,3,, [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The first string is the mark string used in the content.
The second string is the mark string transformed to a slug, i.e. transformed to lower case, remove non-ASCII characters.
The third string is the slug string, but made unique for the whole zettel.
Then follows the marked text as a sequence of __InlineElement__s.

=== ''ENDNOTE''
:::syntax
__Endnote__ **=** ''(ENDNOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
Specifies endnote / footnote text.

=== ''FORMAT-*''
The following lists specifies some inline text formatting.
The structure is always the same, the initial symbol denotes the actual formatting.

:::syntax
__DeleteFormat__ **=** ''(FORMAT-DELETE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as deleted.

:::syntax
__EmphasizeFormat__ **=** ''(FORMAT-EMPH'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as emphasized.

:::syntax
__InsertFormat__ **=** ''(FORMAT-INSERT'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as inserted.

:::syntax
__MarkFormat__ **=** ''(FORMAT-MARK'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as highlighted for the reader (but was not important to the original author).

:::syntax
__QuoteFormat__ **=** ''(FORMAT-QUOTE'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as quoted text.

:::syntax
__SpanFormat__ **=** ''(FORMAT-SPAN'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as belonging together in a certain, yet unspecified way.

:::syntax
__SubScriptFormat__ **=** ''(FORMAT-SUB'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as sub-scripted text.

:::syntax
__SuperScriptFormat__ **=** ''(FORMAT-SUPER'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as super-scripted text.

:::syntax
__StrongFormat__ **=** ''(FORMAT-STRONG'' [[__Attributes__|00001012931000#attribute]] [[__InlineElement__|00001012931600]] &hellip; '')''.
:::
The inline text should be treated as strongly emphasized.

=== ''LITERAL-*''
The following lists specifies some literal text.
The structure is always the same, the initial symbol denotes the actual usage.

:::syntax
__CodeLiteral__ **=** ''(LITERAL-CODE'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as executable code.

:::syntax
__CommentLiteral__ **=** ''(LITERAL-COMMENT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as an internal comment not to be interpreted further.

:::syntax
__HTMLLiteral__ **=** ''(LITERAL-HTML'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as HTML code.

:::syntax
__InputLiteral__ **=** ''(LITERAL-INPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as input entered by an user.

:::syntax
__MathLiteral__ **=** ''(LITERAL-MATH'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as special code to be interpreted as mathematical formulas.

:::syntax
__OutputLiteral__ **=** ''(LITERAL-OUTPUT'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as computer output to be read by an user.

:::syntax
__ZettelLiteral__ **=** ''(LITERAL-ZETTEL'' [[__Attributes__|00001012931000#attribute]] String '')''.
:::
The string contains text that should be treated as (nested) zettel content.

Added docs/manual/00001012931800.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: 00001012931800
title: Encoding of Sz Attribute Values
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230403161923
modified: 20240122115245

An attribute is represented by a single cell.
The first element of the cell references the attribute key, the second value the corresponding value.

:::syntax
__AttributeKeyValue__ **=** ''('' __AttributeKey__ ''.'' __AttributeValue__ '')''.
:::

__AttributeKey__ and __AttributeValue__ are [[string values|00001012930500]].

An empty key denotes the generic attribute.

A key with the value ''"-"'' specifies the default attribute.
In this case, the attribute value is not interpreted.

Some examples:
* ''()'' represents the absence of attributes,
* ''(("-" . ""))'' represent the default attribute,
* ''(("-" . "") ("" . "syntax"))'' adds the generic attribute with the value ""syntax"",
* ''(("lang" . "en"))'' denotes the attribute key ""lang"" with a value ""en"".

Added docs/manual/00001012931900.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
id: 00001012931900
title: Encoding of Sz Reference Values
role: manual
tags: #api #manual #reference #zettelstore
syntax: zmk
created: 20230405123046
modified: 20240122094720

A reference is encoded as the actual reference value, and a symbol describing the state of that actual reference value.

:::syntax
__Reference__ **=** ''('' __ReferenceState__ String '')''.
:::
The string contains the actual reference value.

:::syntax
__ReferenceState__ **=** ''INVALID'' **|** ''ZETTEL'' **|** ''SELF'' **|** ''FOUND'' **|** ''BROKEN'' **|** ''HOSTED'' **|** ''BASED'' **|** ''QUERY'' **|** ''EXTERNAL''.
:::

The meaning of the state symbols corresponds to that of the symbols used for the description of [[link references|00001012931600#link]].

; ''INVALID''
: The reference value is invalid.
; ''ZETTEL''
: The reference value is a reference to a zettel.
  This value is only possible before evaluating the zettel.
; ''SELF''
: The reference value is a reference to the same zettel, to a specific mark.
; ''FOUND''
: The reference value is a valid reference to an existing zettel.
  This value is only possible after evaluating the zettel.
; ''BROKEN''
: The reference value is a valid reference to an missing zettel.
  This value is only possible after evaluating the zettel.
; ''HOSTED''
: The reference value starts with one slash character, denoting an absolute local reference.
; ''BASED''
: The reference value starts with two slash characters, denoting a local reference interpreted relative to the Zettelstore base URL.
; ''QUERY''
: The reference value contains a query expression.
; ''EXTERNAL''
: The reference value contains a full URL, referencing a resource outside of the Zettelstore server.

Changes to docs/manual/00001014000000.zettel.

1
2
3
4
5

6
7
8
1
2
3
4
5
6
7
8
9





+



id: 00001014000000
title: Web user interface
tags: #manual #webui #zettelstore
syntax: zmk
role: manual
created: 20210126175322

The Web user interface is just a secondary way to interact with a Zettelstore.
Using external software that interacts via the [[API|00001012000000]] is the recommended way.

Changes to docs/manual/00001017000000.zettel.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
1
2
3
4
5
6

7
8
9
10
11
12
13
14






-
+







id: 00001017000000
title: Tips and Tricks
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 20220803170112
modified: 20221205154406
modified: 20241216105111

=== Welcome Zettel
* **Problem:** You want to put your Zettelstore into the public and need a starting zettel for your users.
  In addition, you still want a ""home zettel"", with all your references to internal, non-public zettel.
  Zettelstore only allows to specify one [[''home-zettel''|00001004020000#home-zettel]].
* **Solution 1:**
*# Create a new zettel with all your references to internal, non-public zettel.
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
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







-
-
+
+

-
-
+
+




-
-
-
+
+
+

-
-
+
+


-







  If multiple user should use the same home zettel, its zettel identifier must be set in all relevant user zettel.

=== Role-specific Layout of Zettel in Web User Interface (WebUI)
[!role-css]
* **Problem:** You want to add some CSS when displaying zettel of a specific [[role|00001006020000#role]].
  For example, you might want to add a yellow background color for all [[configuration|00001006020100#configuration]] zettel.
  Or you want a multi-column layout.
* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Role to CSS Map|00000000029000]]"" (its identifier is ''00000000029000'').
  This zettel maps a role name to a zettel that must contain the role-specific CSS code.
* **Solution:** If you enable [[''expert-mode''|00001004020000#expert-mode]], you will have access to a zettel called ""[[Zettelstore Sxn Start Code|00000000019000]]"" (its identifier is ''00000000019000'').
  This zettel is the starting point for Sxn code, where you can place a definition for a variable named ""CSS-ROLE-map"".

  First, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must).
  Set its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"".
  But first, create a zettel containing the needed CSS: give it any title, its role is preferably ""configuration"" (but this is not a must).
  Its [[''syntax''|00001006020000#syntax]] must be set to ""[[css|00001008000000#css]]"".
  The content must contain the role-specific CSS code, for example ``body {background-color: #FFFFD0}``for a background in a light yellow color.

  Let's assume, the newly created CSS zettel got the identifier ''20220825200100''.

  Now, you have to connect this zettel to the zettel called ""Zettelstore Role CSS Map"".
  Since you have enabled ''expert-mode'', you are allowed to modify it.
  Add the following metadata ''css-configuration-zid: 20220825200100'' to assign the role-specific CSS code for the role ""configuration"" to the CSS zettel containing that CSS.
  Now, you have to map this freshly created zettel to a role, for example ""zettel"".
  Since you have enabled ''expert-mode'', you are allowed to modify the zettel ""[[Zettelstore Sxn Start Code|00000000019000]]"".
  Add the following code to the Sxn Start Code zettel: ``(set! CSS-ROLE-map '(("zettel" . "20220825200100")))``.

  In general, its role-assigning metadata must be like this pattern: ''css-ROLE-zid: ID'', where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code.
  It is allowed to assign more than one role to a specific CSS zettel.
  In general, the mapping must follow the pattern: ``(ROLE . ID)``, where ''ROLE'' is the placeholder for the role, and ''ID'' for the zettel identifier containing CSS code.
  For example, if you also want the role ""configuration"" to be rendered using that CSS, the code should be something like ``(set! CSS-ROLE-map '(("zettel" . "20220825200100") ("configuration" . "20220825200100")))``.
* **Discussion:** you have to ensure that the CSS zettel is allowed to be read by the intended audience of the zettel with that given role.
  For example, if you made zettel with a specific role public visible, the CSS zettel must also have a [[''visibility: public''|00001010070200]] metadata.
* **Extension:** if you have already established a role-specific layout for zettel, but you additionally want just one another zettel with another role to be rendered with the same CSS, you have to add metadata to the one zettel: ''css-role: ROLE'', where ''ROLE'' is the placeholder for the role that already is assigned to a specific CSS-based layout.

=== Zettel synchronization with iCloud (Apple)
* **Problem:** You use Zettelstore on various macOS computers and you want to use the sameset of zettel across all computers.
* **Solution:** Place your zettel in an iCloud folder.

  To configure Zettelstore to use the folder, you must specify its location within you directory structure as [[''box-uri-X''|00001004010000#box-uri-x]] (replace ''X'' with an appropriate number).
  Your iCloud folder is typically placed in the folder ''~/Library/Mobile Documents/com~apple~CloudDocs''.
66
67
68
69
70
71
72
73

74
75
76
77

78
79
80
81
65
66
67
68
69
70
71

72
73
74
75

76
77
78
79
80







-
+



-
+




* **Solution 2:** If you typically start your Zettelstore on the command line, you could use the ''-d DIR'' option for the [[''run''|00001004051000#d]] sub-command.
  In this case you are allowed to use the character ""''~''"".

  ''zettelstore run -d ~/Library/Mobile\\ Documents/com\\~apple\\~CloudDocs/zettel''

  (The ""''\\''"" is needed by the command line processor to mask the following character to be processed in unintended ways.)
* **Discussion:** Zettel files are synchronized between your computers via iCloud.
  Is does not matter, if one of your computer is offline / switched off.
  Is does not matter, if one of your computers is offline or switched off.
  iCloud will synchronize the zettel files if it later comes online.

  However, if you use more than one computer simultaneously, you must be aware that synchronization takes some time.
  It might take several seconds, maybe longer, that new new version of a zettel appears on the other computer.
  It might take several seconds, maybe longer, that the new version of a zettel appears on the other computer.
  If you update the same zettel on multiple computers at nearly the same time, iCloud will not be able to synchronize the different versions in a safe manner.
  Zettelstore is intentionally not aware of any synchronization within its zettel boxes.

  If Zettelstore behaves strangely after a synchronization took place, the page about [[Troubleshooting|00001018000000#working-with-files]] might contain some useful information.

Changes to docs/manual/00001018000000.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






1
2
3
4
5


6
7
8
9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41
42


43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62





-
-
+
+










-
+












-
+











-
-
+
+












+
+
+
+
+
+
id: 00001018000000
title: Troubleshooting
role: manual
tags: #manual #zettelstore
syntax: zmk
created: 00010101000000
modified: 20221020132617
created: 20211027105921
modified: 20241212153148

This page lists some problems and their solutions that may occur when using your Zettelstore.

=== Installation
* **Problem:** When you double-click on the Zettelstore executable icon, macOS complains that Zettelstore is an application from an unknown developer.
  Therefore, it will not start Zettelstore.
** **Solution:** Press the ''Ctrl'' key while opening the context menu of the Zettelstore executable with a right-click.
   A dialog is then opened where you can acknowledge that you understand the possible risks when you start Zettelstore.
   This dialog is only resented once for a given Zettelstore executable.
* **Problem:** When you double-click on the Zettelstore executable icon, Windows complains that Zettelstore is an application from an unknown developer.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allows to start Zettelstore.
** **Solution:** Windows displays a dialog where you can acknowledge possible risks and allow to start Zettelstore.

=== Authentication
* **Problem:** [[Authentication is enabled|00001010040100]] for a local running Zettelstore and there is a valid [[user zettel|00001010040200]] for the owner.
  But entering user name and password at the [[web user interface|00001014000000]] seems to be ignored, while entering a wrong password will result in an error message.
** **Explanation:** A local running Zettelstore typically means, that you are accessing the Zettelstore using an URL with schema ''http://'', and not ''https://'', for example ''http://localhost:23123''.
   The difference between these two is the missing encryption of user name / password and for the answer of the Zettelstore if you use the ''http://'' schema.
   To be secure by default, the Zettelstore will not work in an insecure environment.
** **Solution 1:** If you are sure that your communication medium is safe, even if you use the ''http:/\/'' schema (for example, you are running the Zettelstore on the same computer you are working on, or if the Zettelstore is running on a computer in your protected local network), then you could add the entry ''insecure-cookie: true'' in you [[startup configuration|00001004010000#insecure-cookie]] file.
** **Solution 2:** If you are not sure about the security of your communication medium (for example, if unknown persons might use your local network), then you should run an [[external server|00001010090100]] in front of your Zettelstore to enable the use of the ''https://'' schema.

=== Working with Zettel Files
* **Problem:** When you delete a zettel file by removing it from the ""disk"", e.g. by dropping it into the trash folder, by dragging into another folder, or by removing it from the command line, Zettelstore sometimes did not detect that change.
  If you access the zettel via Zettelstore, a fatal error is reported.
  If you access the zettel via Zettelstore, an error is reported.
** **Explanation:** Sometimes, the operating system does not tell Zettelstore about the removed zettel.
   This occurs mostly under MacOS.
** **Solution 1:** If you are running Zettelstore in [[""simple-mode""|00001004051100]] or if you have enabled [[''expert-mode''|00001004020000#expert-mode]], you are allowed to refresh the internal data by selecting ""Refresh"" in the Web User Interface (you find it in the menu ""Lists"").
** **Solution 2:** There is an [[API|00001012080500]] call to make Zettelstore aware of this change.
** **Solution 3:** If you have an enabled [[Administrator Console|00001004100000]] you can use the command [[''refresh''|00001004101000#refresh]] to make your changes visible.
** **Solution 4:** You configure the zettel box as [[""simple""|00001004011400]].

=== HTML content is not shown
* **Problem:** You have entered some HTML code as content for your Zettelstore, but this content is not shown on the Web User Interface.

  You may have entered a Zettel with syntax [[""html""|00001008000000#html]], or you have used an [[inline-zettel block|00001007031200]] with syntax ""html"", or you entered a Zettel with syntax [[""markdown""|00001008000000#markdown]] (or ""md"") and used some HTML code fragments.
** **Explanation:** Working with HTML code from unknown sources may lead so severe security problems.
   The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contains malicious software.
** **Explanation:** Working with HTML code from unknown sources may lead to severe security problems.
   The HTML code may force web browsers to load more content from external server, it may contain malicious JavaScript code, it may reference to CSS artifacts that itself load from external servers and may contain malicious software.
   Zettelstore tries to do its best to ignore problematic HTML code, but it may fail.
   Either because of unknown bugs or because of yet unknown changes in the future.

   Zettelstore sets a restrictive [[Content Security Policy|https://www.w3.org/TR/CSP/]], but this depends on web browsers to implement them correctly and on users to not disable it.
   Zettelstore will not display any HTML code, which contains a ``<script>>`` or an ``<iframe>`` tag.
   But attackers may find other ways to deploy their malicious code.

   Therefore, Zettelstore disallows any HTML content as a default.
   If you know what you are doing, e.g. because you will never copy HTML code you do not understand, you can relax this default.
** **Solution 1:** If you want zettel with syntax ""html"" not to be ignored, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""html"".
** **Solution 2:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""markdown"".
** **Solution 3:** If you want zettel with syntax ""html"" not to be ignored, **and** want to allow HTML in Markdown, **and** want to use HTML code within Zettelmarkup, you set the startup configuration key [[''insecure-html''|00001004010000#insecure-html]] to the value ""zettelmarkup"".

=== Search for specific content
* **Problem:** If you are searching for zettel with zettel content ""EUPL"", the zettel with Zettelstore's [[License|00000000000004]] is not shown, but it does contain the character sequence ""EUPL"".
** **Solution:** The content of zettel with a zettel identifier less or equal ''00009999999999'' is not searched.
   These zettel are predefined zettel, sometimes computed zettel, with some content not related to your research.
   For these zettel, only the metadata can be searched.

Added docs/manual/00001019990010.zettel.









1
2
3
4
5
6
7
8
+
+
+
+
+
+
+
+
id: 00001019990010
title: #api
role: tag
syntax: zmk
created: 20230928185004
modified: 20230928185204

Zettel with the tag ''#api'' contain a description of the [[API|00001012000000]].

Changes to docs/readmezip.txt.

13
14
15
16
17
18
19
20

21
13
14
15
16
17
18
19

20








-
+
-
https://zettelstore.de/manual/. It is a live example of the zettelstore
software, running in read-only mode. You can download it separately and it is
possible to make it directly available for your local Zettelstore.

The software, including the manual, is licensed under the European Union Public
License 1.2 (or later). See the separate file LICENSE.txt.

To get in contact with the developer, send an email to ds@zettelstore.de or
To get in contact with the developer, send an email to ds@zettelstore.de.
follow Zettelstore on Twitter: https://twitter.com/zettelstore.

Deleted domain/content.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

































































































































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

package domain

import (
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/z/input"
)

// Content is just the content of a zettel.
type Content struct {
	data     []byte
	isBinary bool
}

// NewContent creates a new content from a string.
func NewContent(data []byte) Content {
	return Content{data: data, isBinary: IsBinary(data)}
}

// Length returns the number of bytes stored.
func (zc *Content) Length() int { return len(zc.data) }

// Equal compares two content values.
func (zc *Content) Equal(o *Content) bool {
	if zc == nil {
		return o == nil
	}
	if zc.isBinary != o.isBinary {
		return false
	}
	return bytes.Equal(zc.data, o.data)
}

// Set content to new string value.
func (zc *Content) Set(data []byte) {
	zc.data = data
	zc.isBinary = IsBinary(data)
}

// Write it to a Writer
func (zc *Content) Write(w io.Writer) (int, error) {
	return w.Write(zc.data)
}

// AsString returns the content itself is a string.
func (zc *Content) AsString() string { return string(zc.data) }

// AsBytes returns the content itself is a byte slice.
func (zc *Content) AsBytes() []byte { return zc.data }

// IsBinary returns true if the content contains non-unicode values or is,
// interpreted a text, with a high probability binary content.
func (zc *Content) IsBinary() bool { return zc.isBinary }

// TrimSpace remove some space character in content, if it is not binary content.
func (zc *Content) TrimSpace() {
	if zc.isBinary {
		return
	}
	inp := input.NewInput(zc.data)
	pos := inp.Pos
	for inp.Ch != input.EOS {
		if input.IsEOLEOS(inp.Ch) {
			inp.Next()
			pos = inp.Pos
			continue
		}
		if !input.IsSpace(inp.Ch) {
			break
		}
		inp.Next()
	}
	zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace)
}

// Encode content for future transmission.
func (zc *Content) Encode() (data, encoding string) {
	if !zc.isBinary {
		return zc.AsString(), ""
	}
	return base64.StdEncoding.EncodeToString(zc.data), "base64"
}

// SetDecoded content to the decoded value of the given string.
func (zc *Content) SetDecoded(data, encoding string) error {
	switch encoding {
	case "":
		zc.data = []byte(data)
	case "base64":
		decoded, err := base64.StdEncoding.DecodeString(data)
		if err != nil {
			return err
		}
		zc.data = decoded
	default:
		return errors.New("unknown encoding " + encoding)
	}
	zc.isBinary = IsBinary(zc.data)
	return nil
}

// IsBinary returns true if the given data appears to be non-text data.
func IsBinary(data []byte) bool {
	if !utf8.Valid(data) {
		return true
	}
	l := len(data)
	for i := 0; i < l; i++ {
		if data[i] == 0 {
			return true
		}
	}
	return false
}

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


































































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

package domain_test

import (
	"testing"

	"zettelstore.de/z/domain"
)

func TestContentIsBinary(t *testing.T) {
	t.Parallel()
	td := []struct {
		s   string
		exp bool
	}{
		{"abc", false},
		{"äöü", false},
		{"", false},
		{string([]byte{0}), true},
	}
	for i, tc := range td {
		content := domain.NewContent([]byte(tc.s))
		got := content.IsBinary()
		if got != tc.exp {
			t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got)
		}
	}
}

func TestTrimSpace(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in, exp string
	}{
		{"", ""},
		{" ", ""},
		{"abc", "abc"},
		{" abc", " abc"},
		{"abc ", "abc"},
		{"abc \n", "abc"},
		{"abc\n ", "abc"},
		{"\nabc", "abc"},
		{" \nabc", "abc"},
		{" \n abc", " abc"},
		{" \n\n abc", " abc"},
		{" \n \n abc", " abc"},
		{" \n \n abc \n \n ", " abc"},
	}
	for _, tc := range testcases {
		c := domain.NewContent([]byte(tc.in))
		c.TrimSpace()
		got := c.AsString()
		if got != tc.exp {
			t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got)
		}
	}
}

Deleted domain/id/id.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



































































































































































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

// Package id provides domain specific types, constants, and functions about
// zettel identifier.
package id

import (
	"strconv"
	"time"

	"zettelstore.de/c/api"
)

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64

// Some important ZettelIDs.
const (
	Invalid = Zid(0) // Invalid is a Zid that will never be valid
)

// ZettelIDs that are used as Zid more than once.
//
// Note: if you change some values, ensure that you also change them in the
// Constant box. They are mentioned there literally, because these
// constants are not available there.
var (
	ConfigurationZid   = MustParse(api.ZidConfiguration)
	BaseTemplateZid    = MustParse(api.ZidBaseTemplate)
	LoginTemplateZid   = MustParse(api.ZidLoginTemplate)
	ListTemplateZid    = MustParse(api.ZidListTemplate)
	ZettelTemplateZid  = MustParse(api.ZidZettelTemplate)
	InfoTemplateZid    = MustParse(api.ZidInfoTemplate)
	FormTemplateZid    = MustParse(api.ZidFormTemplate)
	RenameTemplateZid  = MustParse(api.ZidRenameTemplate)
	DeleteTemplateZid  = MustParse(api.ZidDeleteTemplate)
	ContextTemplateZid = MustParse(api.ZidContextTemplate)
	ErrorTemplateZid   = MustParse(api.ZidErrorTemplate)
	RoleCSSMapZid      = MustParse(api.ZidRoleCSSMap)
	EmojiZid           = MustParse(api.ZidEmoji)
	TOCNewTemplateZid  = MustParse(api.ZidTOCNewTemplate)
	DefaultHomeZid     = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

// ParseUint interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUint(s string) (uint64, error) {
	res, err := strconv.ParseUint(s, 10, 47)
	if err != nil {
		return 0, err
	}
	if res == 0 || res > maxZid {
		return res, strconv.ErrRange
	}
	return res, nil
}

// Parse interprets a string as a zettel identification and
// returns its value.
func Parse(s string) (Zid, error) {
	if len(s) != 14 {
		return Invalid, strconv.ErrSyntax
	}
	res, err := ParseUint(s)
	if err != nil {
		return Invalid, err
	}
	return Zid(res), nil
}

// MustParse tries to interpret a string as a zettel identifier and returns
// its value or panics otherwise.
func MustParse(s api.ZettelID) Zid {
	zid, err := Parse(string(s))
	if err == nil {
		return zid
	}
	panic(err)
}

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid Zid) String() string {
	var result [14]byte
	zid.toByteArray(&result)
	return string(result[:])
}

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid Zid) Bytes() []byte {
	var result [14]byte
	zid.toByteArray(&result)
	return result[:]
}

// toByteArray converts the Zid into a fixed byte array, usable for printing.
//
// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
func (zid Zid) toByteArray(result *[14]byte) {
	date := uint64(zid) / 1000000
	fullyear := date / 10000
	century := fullyear / 100
	year := fullyear % 100
	monthday := date % 10000
	month := monthday / 100
	day := monthday % 100
	time := uint64(zid) % 1000000
	hmtime := time / 100
	second := time % 100
	hour := hmtime / 100
	minute := hmtime % 100
	result[0] = byte(century/10) + '0'
	result[1] = byte(century%10) + '0'
	result[2] = byte(year/10) + '0'
	result[3] = byte(year%10) + '0'
	result[4] = byte(month/10) + '0'
	result[5] = byte(month%10) + '0'
	result[6] = byte(day/10) + '0'
	result[7] = byte(day%10) + '0'
	result[8] = byte(hour/10) + '0'
	result[9] = byte(hour%10) + '0'
	result[10] = byte(minute/10) + '0'
	result[11] = byte(minute%10) + '0'
	result[12] = byte(second/10) + '0'
	result[13] = byte(second%10) + '0'
}

// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits.
func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid }

// ZidLayout to transform a date into a Zid and into other internal dates.
const ZidLayout = "20060102150405"

// New returns a new zettel id based on the current time.
func New(withSeconds bool) Zid {
	now := time.Now().Local()
	var s string
	if withSeconds {
		s = now.Format(ZidLayout)
	} else {
		s = now.Format("20060102150400")
	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}

Deleted domain/id/id_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























































































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

// Package id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

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

func TestIsValid(t *testing.T) {
	t.Parallel()
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",
		"00000000600000",
		"00000007000000",
		"00000080000000",
		"00000900000000",
		"00001000000000",
		"00020000000000",
		"00300000000000",
		"04000000000000",
		"50000000000000",
		"99999999999999",
		"00001007030200",
		"20200310195100",
	}

	for i, sid := range validIDs {
		zid, err := id.Parse(sid)
		if err != nil {
			t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err)
		}
		s := zid.String()
		if s != sid {
			t.Errorf(
				"i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s)
		}
	}

	invalidIDs := []string{
		"", "0", "a",
		"00000000000000",
		"0000000000000a",
		"000000000000000",
		"20200310T195100",
	}

	for i, sid := range invalidIDs {
		if zid, err := id.Parse(sid); err == nil {
			t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
		}
	}
}

var sResult string // to disable compiler optimization in loop below

func BenchmarkString(b *testing.B) {
	var s string
	for n := 0; n < b.N; n++ {
		s = id.Zid(12345678901200).String()
	}
	sResult = s
}

var bResult []byte // to disable compiler optimization in loop below

func BenchmarkBytes(b *testing.B) {
	var bs []byte
	for n := 0; n < b.N; n++ {
		bs = id.Zid(12345678901200).Bytes()
	}
	bResult = bs
}

Deleted domain/id/set.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























































































































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

package id

// Set is a set of zettel identifier
type Set map[Zid]struct{}

// NewSet returns a new set of identifier with the given initial values.
func NewSet(zids ...Zid) Set {
	l := len(zids)
	if l < 8 {
		l = 8
	}
	result := make(Set, l)
	result.AddSlice(zids)
	return result
}

// NewSetCap returns a new set of identifier with the given capacity and initial values.
func NewSetCap(c int, zids ...Zid) Set {
	l := len(zids)
	if c < l {
		c = l
	}
	if c < 8 {
		c = 8
	}
	result := make(Set, c)
	result.AddSlice(zids)
	return result
}

// Zid adds a Zid to the set.
func (s Set) Zid(zid Zid) Set {
	if s == nil {
		return NewSet(zid)
	}
	s[zid] = struct{}{}
	return s
}

// Contains return true if the set is nil or if the set contains the given Zettel identifier.
func (s Set) Contains(zid Zid) bool {
	if s != nil {
		_, found := s[zid]
		return found
	}
	return true
}

// Add all member from the other set.
func (s Set) Add(other Set) Set {
	if s == nil {
		return other
	}
	for zid := range other {
		s[zid] = struct{}{}
	}
	return s
}

// AddSlice adds all identifier of the given slice to the set.
func (s Set) AddSlice(sl Slice) {
	for _, zid := range sl {
		s[zid] = struct{}{}
	}
}

// Sorted returns the set as a sorted slice of zettel identifier.
func (s Set) Sorted() Slice {
	if l := len(s); l > 0 {
		result := make(Slice, 0, l)
		for zid := range s {
			result = append(result, zid)
		}
		result.Sort()
		return result
	}
	return nil
}

// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s Set) IntersectOrSet(other Set) Set {
	if s == nil {
		return other
	}
	if len(s) > len(other) {
		s, other = other, s
	}
	for zid := range s {
		_, otherOk := other[zid]
		if !otherOk {
			delete(s, zid)
		}
	}
	return s
}

// Remove all zettel identifier from 's' that are in the set 'other'.
func (s Set) Remove(other Set) {
	if s == nil || other == nil {
		return
	}
	for zid := range other {
		delete(s, zid)
	}
}

Deleted domain/id/set_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
138
139
140
141
142
143
144
145
146
147
148
149





















































































































































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

package id_test

import (
	"testing"

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

func TestSetContains(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   id.Set
		zid id.Zid
		exp bool
	}{
		{nil, id.Invalid, true},
		{nil, 14, true},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(), 1, false},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(1), 1, true},
	}
	for i, tc := range testcases {
		got := tc.s.Contains(tc.zid)
		if got != tc.exp {
			t.Errorf("%d: %v.Contains(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
		}
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1, 2}},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		got := tc.s1.Add(tc.s2).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetSorted(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		set id.Set
		exp id.Slice
	}{
		{nil, nil},
		{id.NewSet(), nil},
		{id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}},
	}
	for i, tc := range testcases {
		got := tc.set.Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Sorted() should be %v, but got %v", i, tc.set, tc.exp, got)
		}
	}
}

func TestSetIntersectOrSet(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), id.NewSet(), nil},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), nil},
		{id.NewSet(2), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		got := tc.s1.IntersectOrSet(tc.s2).Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetRemove(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(1), id.NewSet(1), id.Slice{}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.Sorted()
		sl2 := tc.s2.Sorted()
		newS1 := id.NewSet(sl1...)
		newS1.Remove(tc.s2)
		got := newS1.Sorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

// func BenchmarkSet(b *testing.B) {
// 	s := id.Set{}
// 	for i := 0; i < b.N; i++ {
// 		s[id.Zid(i)] = true
// 	}
// }
func BenchmarkSet(b *testing.B) {
	s := id.Set{}
	for i := 0; i < b.N; i++ {
		s[id.Zid(i)] = struct{}{}
	}
}

Deleted domain/id/slice.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



































































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

package id

import (
	"bytes"
	"sort"
)

// Slice is a sequence of zettel identifier. A special case is a sorted slice.
type Slice []Zid

func (zs Slice) Len() int           { return len(zs) }
func (zs Slice) Less(i, j int) bool { return zs[i] < zs[j] }
func (zs Slice) Swap(i, j int)      { zs[i], zs[j] = zs[j], zs[i] }

// Sort a slice of Zids.
func (zs Slice) Sort() { sort.Sort(zs) }

// Copy a zettel identifier slice
func (zs Slice) Copy() Slice {
	if zs == nil {
		return nil
	}
	result := make(Slice, len(zs))
	copy(result, zs)
	return result
}

// Equal reports whether zs and other are the same length and contain the samle zettel
// identifier. A nil argument is equivalent to an empty slice.
func (zs Slice) Equal(other Slice) bool {
	if len(zs) != len(other) {
		return false
	}
	if len(zs) == 0 {
		return true
	}
	for i, e := range zs {
		if e != other[i] {
			return false
		}
	}
	return true
}

func (zs Slice) String() string {
	if len(zs) == 0 {
		return ""
	}
	var buf bytes.Buffer
	for i, zid := range zs {
		if i > 0 {
			buf.WriteByte(' ')
		}
		buf.WriteString(zid.String())
	}
	return buf.String()
}

Deleted domain/id/slice_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






















































































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

package id_test

import (
	"testing"

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

func TestSliceSort(t *testing.T) {
	t.Parallel()
	zs := id.Slice{9, 4, 6, 1, 7}
	zs.Sort()
	exp := id.Slice{1, 4, 6, 7, 9}
	if !zs.Equal(exp) {
		t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs)
	}
}

func TestCopy(t *testing.T) {
	t.Parallel()
	var orig id.Slice
	got := orig.Copy()
	if got != nil {
		t.Errorf("Nil copy resulted in %v", got)
	}
	orig = id.Slice{9, 4, 6, 1, 7}
	got = orig.Copy()
	if !orig.Equal(got) {
		t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got)
	}
}

func TestSliceEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Slice
		exp    bool
	}{
		{nil, nil, true},
		{nil, id.Slice{}, true},
		{nil, id.Slice{1}, false},
		{id.Slice{1}, id.Slice{1}, true},
		{id.Slice{1}, id.Slice{2}, false},
		{id.Slice{1, 2}, id.Slice{2, 1}, false},
		{id.Slice{1, 2}, id.Slice{1, 2}, true},
	}
	for i, tc := range testcases {
		got := tc.s1.Equal(tc.s2)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got)
		}
		got = tc.s2.Equal(tc.s1)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got)
		}
	}
}

func TestSliceString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  id.Slice
		exp string
	}{
		{nil, ""},
		{id.Slice{}, ""},
		{id.Slice{1}, "00000000000001"},
		{id.Slice{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := tc.in.String()
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}

Deleted domain/meta/collection.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














































































































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

package meta

import "sort"

// Arrangement stores metadata within its categories.
// Typecally a category might be a tag name, a role name, a syntax value.
type Arrangement map[string][]*Meta

// CreateArrangement by inspecting a given key and use the found
// value as a category.
func CreateArrangement(metaList []*Meta, key string) Arrangement {
	if len(metaList) == 0 {
		return nil
	}
	descr := Type(key)
	if descr == nil {
		return nil
	}
	if descr.IsSet {
		return createSetArrangement(metaList, key)
	}
	return createSimplearrangement(metaList, key)
}

func createSetArrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		if vals, ok := m.GetList(key); ok {
			for _, val := range vals {
				a[val] = append(a[val], m)
			}
		}
	}
	return a
}

func createSimplearrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		if val, ok := m.Get(key); ok && val != "" {
			a[val] = append(a[val], m)
		}
	}
	return a
}

// Counted returns the list of categories, together with the number of
// metadata for each category.
func (a Arrangement) Counted() CountedCategories {
	if len(a) == 0 {
		return nil
	}
	result := make(CountedCategories, 0, len(a))
	for cat, metas := range a {
		result = append(result, CountedCategory{Name: cat, Count: len(metas)})
	}
	return result
}

// CountedCategory contains of a name and the number how much this name occured
// somewhere.
type CountedCategory struct {
	Name  string
	Count int
}

// CountedCategories is the list of CountedCategories.
// Every name must occur only once.
type CountedCategories []CountedCategory

// SortByName sorts the list by the name attribute.
// Since each name must occur only once, two CountedCategories cannot have
// the same name.
func (ccs CountedCategories) SortByName() {
	sort.Slice(ccs, func(i, j int) bool { return ccs[i].Name < ccs[j].Name })
}

// SortByCount sorts the list by the count attribute, descending.
// If two counts are equal, elements are sorted by name.
func (ccs CountedCategories) SortByCount() {
	sort.Slice(ccs, func(i, j int) bool {
		iCount, jCount := ccs[i].Count, ccs[j].Count
		if iCount > jCount {
			return true
		}
		if iCount == jCount {
			return ccs[i].Name < ccs[j].Name
		}
		return false
	})
}

// Categories returns just the category names.
func (ccs CountedCategories) Categories() []string {
	result := make([]string, len(ccs))
	for i, cc := range ccs {
		result[i] = cc.Name
	}
	return result
}

Deleted domain/meta/meta.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








































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 meta provides the domain specific type 'meta'.
package meta

import (
	"bytes"
	"regexp"
	"sort"
	"strings"
	"unicode"
	"unicode/utf8"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/input"
	"zettelstore.de/z/strfun"
)

type keyUsage int

const (
	_             keyUsage = iota
	usageUser              // Key will be manipulated by the user
	usageComputed          // Key is computed by zettelstore
	usageProperty          // Key is computed and not stored by zettelstore
)

// DescriptionKey formally describes each supported metadata key.
type DescriptionKey struct {
	Name    string
	Type    *DescriptionType
	usage   keyUsage
	Inverse string
}

// IsComputed returns true, if metadata is computed and not set by the user.
func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed }

// IsProperty returns true, if metadata is a computed property.
func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty }

// IsStoredComputed retruns true, if metadata is computed, but also stored.
func (kd *DescriptionKey) IsStoredComputed() bool { return kd.usage == usageComputed }

var registeredKeys = make(map[string]*DescriptionKey)

func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) {
	if _, ok := registeredKeys[name]; ok {
		panic("Key '" + name + "' already defined")
	}
	if inverse != "" {
		if t != TypeID && t != TypeIDSet {
			panic("Inversable key '" + name + "' is not identifier type, but " + t.String())
		}
		inv, ok := registeredKeys[inverse]
		if !ok {
			panic("Inverse Key '" + inverse + "' not found")
		}
		if !inv.IsComputed() {
			panic("Inverse Key '" + inverse + "' is not computed.")
		}
		if inv.Type != TypeIDSet {
			panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String())
		}
	}
	registeredKeys[name] = &DescriptionKey{name, t, usage, inverse}
}

// IsComputed returns true, if key denotes a computed metadata key.
func IsComputed(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsComputed()
	}
	return false
}

// IsProperty returns true, if key denotes a property metadata value.
func IsProperty(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsProperty()
	}
	return false
}

// IsStoredComputed returns true, if key denotes a computed metadata key that is stored.
func IsStoredComputed(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsStoredComputed()
	}
	return false
}

// Inverse returns the name of the inverse key.
func Inverse(name string) string {
	if kd, ok := registeredKeys[name]; ok {
		return kd.Inverse
	}
	return ""
}

// GetDescription returns the key description object of the given key name.
func GetDescription(name string) DescriptionKey {
	if d, ok := registeredKeys[name]; ok {
		return *d
	}
	return DescriptionKey{Type: Type(name)}
}

// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name.
func GetSortedKeyDescriptions() []*DescriptionKey {
	keys := maps.Keys(registeredKeys)
	result := make([]*DescriptionKey, 0, len(keys))
	for _, n := range keys {
		result = append(result, registeredKeys[n])
	}
	return result
}

// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")

	// Properties that are inverse keys
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")

	registerKey(api.KeyAuthor, TypeString, usageUser, "")
	registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "")
	registerKey(api.KeyCopyright, TypeString, usageUser, "")
	registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyCredential, TypeCredential, usageUser, "")
	registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)
	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(api.KeyQuery, TypeEmpty, usageUser, "")
	registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
	registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeyURL, TypeURL, usageUser, "")
	registerKey(api.KeyUselessFiles, TypeString, usageProperty, "")
	registerKey(api.KeyUserID, TypeWord, usageUser, "")
	registerKey(api.KeyUserRole, TypeWord, usageUser, "")
	registerKey(api.KeyVisibility, TypeWord, usageUser, "")
}

// NewPrefix is the prefix for metadata key in template zettel for creating new zettel.
const NewPrefix = "new-"

// Meta contains all meta-data of a zettel.
type Meta struct {
	Zid     id.Zid
	pairs   map[string]string
	YamlSep bool
}

// New creates a new chunk for storing metadata.
func New(zid id.Zid) *Meta {
	return &Meta{Zid: zid, pairs: make(map[string]string, 5)}
}

// NewWithData creates metadata object with given data.
func NewWithData(zid id.Zid, data map[string]string) *Meta {
	pairs := make(map[string]string, len(data))
	for k, v := range data {
		pairs[k] = v
	}
	return &Meta{Zid: zid, pairs: pairs}
}

// Length returns the number of bytes stored for the metadata.
func (m *Meta) Length() int {
	if m == nil {
		return 0
	}
	result := 6 // storage needed for Zid
	for k, v := range m.pairs {
		result += len(k) + len(v) + 1 // 1 because separator
	}
	return result
}

// Clone returns a new copy of the metadata.
func (m *Meta) Clone() *Meta {
	return &Meta{
		Zid:     m.Zid,
		pairs:   m.Map(),
		YamlSep: m.YamlSep,
	}
}

// Map returns a copy of the meta data as a string map.
func (m *Meta) Map() map[string]string {
	pairs := make(map[string]string, len(m.pairs))
	for k, v := range m.pairs {
		pairs[k] = v
	}
	return pairs
}

var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$")

// KeyIsValid returns true, the the key is a valid string.
func KeyIsValid(key string) bool {
	return reKey.MatchString(key)
}

// Pair is one key-value-pair of a Zettel meta.
type Pair struct {
	Key   string
	Value string
}

var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax}
var firstKeySet strfun.Set

func init() {
	firstKeySet = strfun.NewSet(firstKeys...)
}

// Set stores the given string value under the given key.
func (m *Meta) Set(key, value string) {
	if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

// SetNonEmpty stores the given value under the given key, if the value is non-empty.
// An empty value will delete the previous association.
func (m *Meta) SetNonEmpty(key, value string) {
	if value == "" {
		delete(m.pairs, key)
	} else if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, input.IsSpace)
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (string, bool) {
	if m == nil {
		return "", false
	}
	if key == api.KeyID {
		return m.Zid.String(), true
	}
	value, ok := m.pairs[key]
	return value, ok
}

// GetDefault retrieves the string value of the given key. If no value was
// stored, the given default value is returned.
func (m *Meta) GetDefault(key, def string) string {
	if value, found := m.Get(key); found {
		return value
	}
	return def
}

// GetTitle returns the title of the metadata. It is the only key that has a
// defined default value: the string representation of the zettel identifier.
func (m *Meta) GetTitle() string {
	if title, found := m.Get(api.KeyTitle); found {
		return title
	}
	return m.Zid.String()
}

// Pairs returns not computed key/values pairs stored, in a specific order.
// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
// MetaContextKey. Then all other pairs are append to the list, ordered by key.
func (m *Meta) Pairs() []Pair {
	return m.doPairs(m.getFirstKeys(), notComputedKey)
}

// ComputedPairs returns all key/values pairs stored, in a specific order. First come
// the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
// MetaContextKey. Then all other pairs are append to the list, ordered by key.
func (m *Meta) ComputedPairs() []Pair {
	return m.doPairs(m.getFirstKeys(), anyKey)
}

// PairsRest returns not computed key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) PairsRest() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	return m.doPairs(result, notComputedKey)
}

// ComputedPairsRest returns all key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) ComputedPairsRest() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	return m.doPairs(result, anyKey)
}

func notComputedKey(key string) bool { return !IsComputed(key) }
func anyKey(string) bool             { return true }

func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair {
	keys := m.getKeysRest(addKeyPred)
	for _, k := range keys {
		firstKeys = append(firstKeys, Pair{k, m.pairs[k]})
	}
	return firstKeys
}

func (m *Meta) getFirstKeys() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	for _, key := range firstKeys {
		if value, ok := m.pairs[key]; ok {
			result = append(result, Pair{key, value})
		}
	}
	return result
}

func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string {
	keys := make([]string, 0, len(m.pairs))
	for k := range m.pairs {
		if !firstKeySet.Has(k) && addKeyPred(k) {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	return keys
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != api.KeyID {
		delete(m.pairs, key)
	}
}

// Equal compares to metas for equality.
func (m *Meta) Equal(o *Meta, allowComputed bool) bool {
	if m == nil && o == nil {
		return true
	}
	if m == nil || o == nil || m.Zid != o.Zid {
		return false
	}
	tested := make(strfun.Set, len(m.pairs))
	for k, v := range m.pairs {
		tested.Set(k)
		if !equalValue(k, v, o, allowComputed) {
			return false
		}
	}
	for k, v := range o.pairs {
		if !tested.Has(k) && !equalValue(k, v, m, allowComputed) {
			return false
		}
	}
	return true
}

func equalValue(key, val string, other *Meta, allowComputed bool) bool {
	if allowComputed || !IsComputed(key) {
		if valO, found := other.pairs[key]; !found || val != valO {
			return false
		}
	}
	return true
}

// Sanitize all metadata keys and values, so that they can be written safely into a file.
func (m *Meta) Sanitize() {
	if m == nil {
		return
	}
	for k, v := range m.pairs {
		m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v)
	}
}

// RemoveNonGraphic changes the given string not to include non-graphical characters.
// It is needed to sanitize meta data.
func RemoveNonGraphic(s string) string {
	if s == "" {
		return ""
	}
	pos := 0
	var buf bytes.Buffer
	for pos < len(s) {
		nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
		if nextPos < 0 {
			break
		}
		buf.WriteString(s[pos:nextPos])
		buf.WriteByte(' ')
		_, size := utf8.DecodeRuneInString(s[nextPos:])
		pos = nextPos + size
	}
	if pos == 0 {
		return strings.TrimSpace(s)
	}
	buf.WriteString(s[pos:])
	return strings.TrimSpace(buf.String())
}

Deleted domain/meta/meta_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
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






































































































































































































































































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

// Package meta_test provides tests for the domain specific type 'meta'.
package meta

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()
	validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)}
	for _, key := range validKeys {
		if !KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as invalid key", key)
		}
	}
	invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)}
	for _, key := range invalidKeys {
		if KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as valid key", key)
		}
	}
}

func TestTitleHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	const st = "A simple text"
	addToMeta(m, api.KeyTitle, " "+st+"  ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != st {
		t.Errorf("Title is not %q, but %q", st, got)
	}
	addToMeta(m, api.KeyTitle, "  "+st+"\t")
	const exp = st + " " + st
	if got, ok := m.Get(api.KeyTitle); !ok || got != exp {
		t.Errorf("Title is not %q, but %q", exp, got)
	}

	m = New(testID)
	const at = "A Title"
	addToMeta(m, api.KeyTitle, at)
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != at {
		t.Errorf("Title is not %q, but %q", at, got)
	}
}

func checkSet(t *testing.T, exp []string, m *Meta, key string) {
	t.Helper()
	got, _ := m.GetList(key)
	for i, tag := range exp {
		if i < len(got) {
			if tag != got[i] {
				t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i])
			}
		} else {
			t.Errorf("Expected %q, but is missing", exp[i])
		}
	}
	if len(exp) < len(got) {
		t.Errorf("Extra tags: %q", got[len(exp):])
	}
}

func TestTagsHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	checkSet(t, []string{}, m, api.KeyTags)

	addToMeta(m, api.KeyTags, "")
	checkSet(t, []string{}, m, api.KeyTags)

	addToMeta(m, api.KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4"}, m, api.KeyTags)

	addToMeta(m, api.KeyTags, "#t5")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags)

	addToMeta(m, api.KeyTags, "t6")
	checkSet(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m, api.KeyTags)
}

func TestSyntax(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, api.KeySyntax, "MarkDown")
	const exp = "markdown"
	if got, ok := m.Get(api.KeySyntax); !ok || got != exp {
		t.Errorf("Syntax is not %q, but %q", exp, got)
	}
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
}

func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) {
	t.Helper()
	got := make(map[string]string, len(gotP))
	for _, p := range gotP {
		got[p.Key] = p.Value
		if _, ok := exp[p.Key]; !ok {
			t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value)
		}
	}
	for k, v := range exp {
		if gv, ok := got[k]; !ok || v != gv {
			if ok {
				t.Errorf("Key %q is not %q, but %q", k, v, got[k])
			} else {
				t.Errorf("Key %q missing, should have value %q", k, v)
			}
		}
	}
}

func TestDefaultHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	addToMeta(m, "h1", "d1")
	addToMeta(m, "H2", "D2")
	addToMeta(m, "H1", "D1.1")
	exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"}
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "", "d0")
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h3", "")
	exp["h3"] = ""
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h3", "  ")
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h4", " ")
	exp["h4"] = ""
	checkHeader(t, exp, m.Pairs())
}

func TestDelete(t *testing.T) {
	t.Parallel()
	m := New(testID)
	m.Set("key", "val")
	if got, ok := m.Get("key"); !ok || got != "val" {
		t.Errorf("Value != %q, got: %v/%q", "val", ok, got)
	}
	m.Set("key", "")
	if got, ok := m.Get("key"); !ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
	m.Delete("key")
	if got, ok := m.Get("key"); ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
}

func TestEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		pairs1, pairs2 []string
		allowComputed  bool
		exp            bool
	}{
		{nil, nil, true, true},
		{nil, nil, false, true},
		{[]string{"a", "a"}, nil, false, false},
		{[]string{"a", "a"}, nil, true, false},
		{[]string{api.KeyFolge, "0"}, nil, true, false},
		{[]string{api.KeyFolge, "0"}, nil, false, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true},
	}
	for i, tc := range testcases {
		m1 := pairs2meta(tc.pairs1)
		m2 := pairs2meta(tc.pairs2)
		got := m1.Equal(m2, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
		got = m2.Equal(m1, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
	}

	// Pathologic cases
	var m1, m2 *Meta
	if !m1.Equal(m2, true) {
		t.Error("Nil metas should be treated equal")
	}
	m1 = New(testID)
	if m1.Equal(m2, true) {
		t.Error("Empty meta should not be equal to nil")
	}
	if m2.Equal(m1, true) {
		t.Error("Nil meta should should not be equal to empty")
	}
	m2 = New(testID + 1)
	if m1.Equal(m2, true) {
		t.Error("Different ID should differentiate")
	}
	if m2.Equal(m1, true) {
		t.Error("Different ID should differentiate")
	}
}

func pairs2meta(pairs []string) *Meta {
	m := New(testID)
	for i := 0; i < len(pairs); i = i + 2 {
		m.Set(pairs[i], pairs[i+1])
	}
	return m
}

func TestRemoveNonGraphic(t *testing.T) {
	testCases := []struct {
		inp string
		exp string
	}{
		{"", ""},
		{" ", ""},
		{"a", "a"},
		{"a ", "a"},
		{"a b", "a b"},
		{"\n", ""},
		{"a\n", "a"},
		{"a\nb", "a b"},
		{"a\tb", "a b"},
	}
	for i, tc := range testCases {
		got := RemoveNonGraphic(tc.inp)
		if tc.exp != got {
			t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got)
		}
	}
}

Deleted domain/meta/parse.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

















































































































































































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

package meta

import (
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/input"
	"zettelstore.de/z/strfun"
)

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()
	}
	meta := New(zid)
	for {
		skipSpace(inp)
		switch inp.Ch {
		case '\r':
			if inp.Peek() == '\n' {
				inp.Next()
			}
			fallthrough
		case '\n':
			inp.Next()
			return meta
		case input.EOS:
			return meta
		case '%':
			skipToEOL(inp)
			inp.EatEOL()
			continue
		}
		parseHeader(meta, inp)
		if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
			skipToEOL(inp)
			inp.EatEOL()
			meta.YamlSep = true
			return meta
		}
	}
}

func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	skipSpace(inp)
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		skipSpace(inp)
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !input.IsSpace(inp.Ch) {
			break
		}
		val = append(val, ' ')
	}
	addToMeta(m, string(key), string(val))
}

func skipSpace(inp *input.Input) {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	}
}

func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
		case '\n', '\r', input.EOS:
			return
		}
		inp.Next()
	}
}

// Return true iff rune is valid for header key.
func isHeader(ch rune) bool {
	return ('a' <= ch && ch <= 'z') ||
		('0' <= ch && ch <= '9') ||
		ch == '-' ||
		('A' <= ch && ch <= 'Z')
}

type predValidElem func(string) bool

func addToSet(set strfun.Set, elems []string, useElem predValidElem) {
	for _, s := range elems {
		if len(s) > 0 && useElem(s) {
			set.Set(s)
		}
	}
}

func addSet(m *Meta, key, val string, useElem predValidElem) {
	newElems := strings.Fields(val)
	oldElems, ok := m.GetList(key)
	if !ok {
		oldElems = nil
	}

	set := make(strfun.Set, len(newElems)+len(oldElems))
	addToSet(set, newElems, useElem)
	if len(set) == 0 {
		// Nothing to add. Maybe because of rejected elements.
		return
	}
	addToSet(set, oldElems, useElem)
	m.SetList(key, maps.Keys(set))
}

func addData(m *Meta, k, v string) {
	if o, ok := m.Get(k); !ok || o == "" {
		m.Set(k, v)
	} else if v != "" {
		m.Set(k, o+" "+v)
	}
}

func addToMeta(m *Meta, key, val string) {
	v := trimValue(val)
	key = strings.ToLower(key)
	if !KeyIsValid(key) {
		return
	}
	switch key {
	case "", api.KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeTagSet:
		addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 })
	case TypeWord:
		m.Set(key, strings.ToLower(v))
	case TypeWordSet:
		addSet(m, key, strings.ToLower(v), func(s string) bool { return true })
	case TypeID:
		if _, err := id.Parse(v); err == nil {
			m.Set(key, v)
		}
	case TypeIDSet:
		addSet(m, key, v, func(s string) bool {
			_, err := id.Parse(s)
			return err == nil
		})
	case TypeTimestamp:
		if _, ok := TimeValue(v); ok {
			m.Set(key, v)
		}
	default:
		addData(m, key, v)
	}
}

Deleted domain/meta/parse_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
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







































































































































































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

package meta_test

import (
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
)

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}

func TestEmpty(t *testing.T) {
	t.Parallel()
	m := parseMetaStr("")
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 {
		t.Errorf("Tags are not nil, but %v", got)
	}
}

func TestTitle(t *testing.T) {
	t.Parallel()
	td := []struct{ s, e string }{
		{api.KeyTitle + ": a title", "a title"},
		{api.KeyTitle + ": a\n\t title", "a title"},
		{api.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{api.KeyTitle + " AbC", "AbC"},
		{api.KeyTitle + " AbC\n ded", "AbC ded"},
		{api.KeyTitle + ": o\ntitle: p", "o p"},
		{api.KeyTitle + ": O\n\ntitle: P", "O"},
		{api.KeyTitle + ": b\r\ntitle: c", "b c"},
		{api.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{api.KeyTitle + ": r\rtitle: q", "r q"},
		{api.KeyTitle + ": R\r\rtitle: Q", "R"},
	}
	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}
}

func TestTags(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		src string
		exp string
	}{
		{"", ""},
		{api.KeyTags + ":", ""},
		{api.KeyTags + ": c", ""},
		{api.KeyTags + ": #", ""},
		{api.KeyTags + ": #c", "c"},
		{api.KeyTags + ": #c #", "c"},
		{api.KeyTags + ": #c #b", "b c"},
		{api.KeyTags + ": #c # #", "c"},
		{api.KeyTags + ": #c # #b", "b c"},
	}
	for i, tc := range testcases {
		m := parseMetaStr(tc.src)
		tags, found := m.GetTags(api.KeyTags)
		if !found {
			if tc.exp != "" {
				t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags)
			}
			continue
		}
		if tc.exp == "" && len(tags) > 0 {
			t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags)
			continue
		}
		got := strings.Join(tags, " ")
		if tc.exp != got {
			t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got)
		}
	}
}

func TestNewFromInput(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		input string
		exp   []meta.Pair
	}{
		{"", []meta.Pair{}},
		{" a:b", []meta.Pair{{"a", "b"}}},
		{"%a:b", []meta.Pair{}},
		{"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}},
		{"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}},
		{"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}},
		{"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}},
	}
	for i, tc := range testcases {
		meta := parseMetaStr(tc.input)
		if got := meta.Pairs(); !equalPairs(tc.exp, got) {
			t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got)
		}
	}

	// Test, whether input position is correct.
	inp := input.NewInput([]byte("---\na:b\n---\nX"))
	m := meta.NewFromInput(testID, inp)
	exp := []meta.Pair{{"a", "b"}}
	if got := m.Pairs(); !equalPairs(exp, got) {
		t.Errorf("Expected=%v, got=%v", exp, got)
	}
	expCh := 'X'
	if gotCh := inp.Ch; gotCh != expCh {
		t.Errorf("Expected=%v, got=%v", expCh, gotCh)
	}
}

func equalPairs(one, two []meta.Pair) bool {
	if len(one) != len(two) {
		return false
	}
	for i := 0; i < len(one); i++ {
		if one[i].Key != two[i].Key || one[i].Value != two[i].Value {
			return false
		}
	}
	return true
}

func TestPrecursorIDSet(t *testing.T) {
	t.Parallel()
	var testdata = []struct {
		inp string
		exp string
	}{
		{"", ""},
		{"123", ""},
		{"12345678901234", "12345678901234"},
		{"123 12345678901234", "12345678901234"},
		{"12345678901234 123", "12345678901234"},
		{"01234567890123 123 12345678901234", "01234567890123 12345678901234"},
		{"12345678901234 01234567890123", "01234567890123 12345678901234"},
	}
	for i, tc := range testdata {
		m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
			t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp)
		}
	}
}

Deleted domain/meta/type.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















































































































































































































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

package meta

import (
	"strconv"
	"strings"
	"sync"
	"time"

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

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}

// String returns the string representation of the given type
func (t DescriptionType) String() string { return t.Name }

var registeredTypes = make(map[string]*DescriptionType)

func registerType(name string, isSet bool) *DescriptionType {
	if _, ok := registeredTypes[name]; ok {
		panic("Type '" + name + "' already registered")
	}
	t := &DescriptionType{name, isSet}
	registeredTypes[name] = t
	return t
}

// Supported key types.
var (
	TypeCredential   = registerType(api.MetaCredential, false)
	TypeEmpty        = registerType(api.MetaEmpty, false)
	TypeID           = registerType(api.MetaID, false)
	TypeIDSet        = registerType(api.MetaIDSet, true)
	TypeNumber       = registerType(api.MetaNumber, false)
	TypeString       = registerType(api.MetaString, false)
	TypeTagSet       = registerType(api.MetaTagSet, true)
	TypeTimestamp    = registerType(api.MetaTimestamp, false)
	TypeURL          = registerType(api.MetaURL, false)
	TypeWord         = registerType(api.MetaWord, false)
	TypeWordSet      = registerType(api.MetaWordSet, true)
	TypeZettelmarkup = registerType(api.MetaZettelmarkup, false)
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeUnknown is returned.
func (*Meta) Type(key string) *DescriptionType {
	return Type(key)
}

var (
	cachedTypedKeys = make(map[string]*DescriptionType)
	mxTypedKey      sync.RWMutex
	suffixTypes     = map[string]*DescriptionType{
		"-number": TypeNumber,
		"-role":   TypeWord,
		"-set":    TypeWordSet,
		"-title":  TypeZettelmarkup,
		"-url":    TypeURL,
		"-zettel": TypeID,
		"-zid":    TypeID,
		"-zids":   TypeIDSet,
	}
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeEmpty is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	mxTypedKey.RLock()
	k, ok := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if ok {
		return k
	}
	for suffix, t := range suffixTypes {
		if strings.HasSuffix(key, suffix) {
			mxTypedKey.Lock()
			defer mxTypedKey.Unlock()
			cachedTypedKeys[key] = t
			return t
		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.
func (m *Meta) SetList(key string, values []string) {
	if key != api.KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}

// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, time.Now().Local().Format(id.ZidLayout))
}

// BoolValue returns the value interpreted as a bool.
func BoolValue(value string) bool {
	if len(value) > 0 {
		switch value[0] {
		case '0', 'f', 'F', 'n', 'N':
			return false
		}
	}
	return true
}

// GetBool returns the boolean value of the given key.
func (m *Meta) GetBool(key string) bool {
	if value, ok := m.Get(key); ok {
		return BoolValue(value)
	}
	return false
}

// TimeValue returns the time value of the given value.
func TimeValue(value string) (time.Time, bool) {
	if t, err := time.Parse(id.ZidLayout, value); err == nil {
		return t, true
	}
	return time.Time{}, false
}

// GetTime returns the time value of the given key.
func (m *Meta) GetTime(key string) (time.Time, bool) {
	if value, ok := m.Get(key); ok {
		return TimeValue(value)
	}
	return time.Time{}, false
}

// ListFromValue transforms a string value into a list value.
func ListFromValue(value string) []string {
	return strings.Fields(value)
}

// GetList retrieves the string list value of a given key. The bool value
// signals, whether there was a value stored or not.
func (m *Meta) GetList(key string) ([]string, bool) {
	value, ok := m.Get(key)
	if !ok {
		return nil, false
	}
	return ListFromValue(value), true
}

// GetTags returns the list of tags as a string list. Each tag does not begin
// with the '#' character, in contrast to `GetList`.
func (m *Meta) GetTags(key string) ([]string, bool) {
	tagsValue, ok := m.Get(key)
	if !ok {
		return nil, false
	}
	tags := ListFromValue(strings.ToLower(tagsValue))
	for i, tag := range tags {
		tags[i] = CleanTag(tag)
	}
	return tags, len(tags) > 0
}

// CleanTag removes the number character ('#') from a tag value and lowercases it.
func CleanTag(tag string) string {
	if len(tag) > 1 && tag[0] == '#' {
		return tag[1:]
	}
	return tag
}

// GetListOrNil retrieves the string list value of a given key. If there was
// nothing stores, a nil list is returned.
func (m *Meta) GetListOrNil(key string) []string {
	if value, ok := m.GetList(key); ok {
		return value
	}
	return nil
}

// GetNumber retrieves the numeric value of a given key.
func (m *Meta) GetNumber(key string, def int64) int64 {
	if value, ok := m.Get(key); ok {
		if num, err := strconv.ParseInt(value, 10, 64); err == nil {
			return num
		}
	}
	return def
}

Deleted domain/meta/type_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
































































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

package meta_test

import (
	"strconv"
	"testing"
	"time"

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

func TestNow(t *testing.T) {
	t.Parallel()
	m := meta.New(id.Invalid)
	m.SetNow("key")
	val, ok := m.Get("key")
	if !ok {
		t.Error("Unable to get value of key")
	}
	if len(val) != 14 {
		t.Errorf("Value is not 14 digits long: %q", val)
	}
	if _, err := strconv.ParseInt(val, 10, 64); err != nil {
		t.Errorf("Unable to parse %q as an int64: %v", val, err)
	}
	if _, ok = m.GetTime("key"); !ok {
		t.Errorf("Unable to get time from value %q", val)
	}
}

func TestGetTime(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		value string
		valid bool
		exp   time.Time
	}{
		{"", false, time.Time{}},
		{"1", false, time.Time{}},
		{"00000000000000", false, time.Time{}},
		{"98765432109876", false, time.Time{}},
		{"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)},
	}
	for i, tc := range testCases {
		got, ok := meta.TimeValue(tc.value)
		if ok != tc.valid {
			t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok)
			continue
		}
		if got != tc.exp {
			t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got)
		}
	}
}

Deleted domain/meta/values.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














































































































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

package meta

import (
	"fmt"

	"zettelstore.de/c/api"
)

// Supported syntax values.
const (
	SyntaxCSS      = api.ValueSyntaxCSS
	SyntaxDraw     = api.ValueSyntaxDraw
	SyntaxGif      = api.ValueSyntaxGif
	SyntaxHTML     = api.ValueSyntaxHTML
	SyntaxJPEG     = "jpeg"
	SyntaxJPG      = "jpg"
	SyntaxMarkdown = api.ValueSyntaxMarkdown
	SyntaxMD       = api.ValueSyntaxMD
	SyntaxMustache = api.ValueSyntaxMustache
	SyntaxNone     = api.ValueSyntaxNone
	SyntaxPlain    = "plain"
	SyntaxPNG      = "png"
	SyntaxSVG      = api.ValueSyntaxSVG
	SyntaxText     = api.ValueSyntaxText
	SyntaxTxt      = "txt"
	SyntaxWebp     = "webp"
	SyntaxZmk      = api.ValueSyntaxZmk
)

// Visibility enumerates the variations of the 'visibility' meta key.
type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityCreator
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[string]Visibility{
	api.ValueVisibilityPublic:  VisibilityPublic,
	api.ValueVisibilityCreator: VisibilityCreator,
	api.ValueVisibilityLogin:   VisibilityLogin,
	api.ValueVisibilityOwner:   VisibilityOwner,
	api.ValueVisibilityExpert:  VisibilityExpert,
}
var revVisMap = map[Visibility]string{}

func init() {
	for k, v := range visMap {
		revVisMap[v] = k
	}
}

// GetVisibility returns the visibility value of the given string
func GetVisibility(val string) Visibility {
	if vis, ok := visMap[val]; ok {
		return vis
	}
	return VisibilityUnknown
}

func (v Visibility) String() string {
	if s, ok := revVisMap[v]; ok {
		return s
	}
	return fmt.Sprintf("Unknown (%d)", v)
}

// UserRole enumerates the supported values of meta key 'user-role'.
type UserRole int

// Supported values for user roles.
const (
	_ UserRole = iota
	UserRoleUnknown
	UserRoleCreator
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[string]UserRole{
	api.ValueUserRoleCreator: UserRoleCreator,
	api.ValueUserRoleReader:  UserRoleReader,
	api.ValueUserRoleWriter:  UserRoleWriter,
	api.ValueUserRoleOwner:   UserRoleOwner,
}

// GetUserRole role returns the user role of the given string.
func GetUserRole(val string) UserRole {
	if ur, ok := urMap[val]; ok {
		return ur
	}
	return UserRoleUnknown
}

Deleted domain/meta/write.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

























































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

package meta

import "io"

// Write writes metadata to a writer, excluding computed and propery values.
func (m *Meta) Write(w io.Writer) (int, error) {
	return m.doWrite(w, IsComputed)
}

// WriteComputed writes metadata to a writer, including computed values,
// but excluding property values.
func (m *Meta) WriteComputed(w io.Writer) (int, error) {
	return m.doWrite(w, IsProperty)
}

func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignoreKeyPred(key) {
			continue
		}
		if err != nil {
			break
		}
		var l int
		l, err = io.WriteString(w, key)
		length += l
		if err == nil {
			l, err = w.Write(colonSpace)
			length += l
		}
		if err == nil {
			l, err = io.WriteString(w, p.Value)
			length += l
		}
		if err == nil {
			l, err = w.Write(newline)
			length += l
		}
	}
	return length, err
}

var (
	colonSpace = []byte{':', ' '}
	newline    = []byte{'\n'}
)

Deleted domain/meta/write_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


























































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

package meta_test

import (
	"bytes"
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {
	m := meta.New(testID)
	if title != "" {
		m.Set(api.KeyTitle, title)
	}
	if tags != nil {
		m.Set(api.KeyTags, strings.Join(tags, " "))
	}
	if syntax != "" {
		m.Set(api.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	var buf bytes.Buffer
	m.Write(&buf)
	if got := buf.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

func TestWriteMeta(t *testing.T) {
	t.Parallel()
	assertWriteMeta(t, newMeta("", nil, ""), "")

	m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax")
	assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n")

	m = newMeta("TITLE", nil, "")
	m.Set("user", "zettel")
	m.Set("auth", "basic")
	assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n")
}

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





























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

// Package domain provides domain specific types, constants, and functions.
package domain

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

// Zettel is the main data object of a zettelstore.
type Zettel struct {
	Meta    *meta.Meta // Some additional meta-data.
	Content Content    // The content of the zettel itself.
}

// Length returns the number of bytes to store the zettel (in a domain view,
// not in a technical view).
func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() }

// Equal compares two zettel for equality.
func (z Zettel) Equal(o Zettel, allowComputed bool) bool {
	return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content)
}

Changes to encoder/encoder.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
1

2
3
4
5
6
7
8
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
// 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 encoder provides a generic interface to encode the abstract syntax
// tree into some text form.
package encoder

import (
	"errors"
	"fmt"
	"io"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

// Encoder is an interface that allows to encode different parts of a zettel.
type Encoder interface {
	WriteZettel(io.Writer, *ast.ZettelNode, EvalMetaFunc) (int, error)
	WriteMeta(io.Writer, *meta.Meta, EvalMetaFunc) (int, error)
	WriteContent(io.Writer, *ast.ZettelNode) (int, error)
41
42
43
44
45
46
47
48

49
50

51
52
53
54
55
56






57
58
59
60
61
62
63
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







-
+

-
+





-
+
+
+
+
+
+







	ErrNoWriteMeta    = errors.New("method WriteMeta is not implemented")
	ErrNoWriteContent = errors.New("method WriteContent is not implemented")
	ErrNoWriteBlocks  = errors.New("method WriteBlocks is not implemented")
	ErrNoWriteInlines = errors.New("method WriteInlines is not implemented")
)

// Create builds a new encoder with the given options.
func Create(enc api.EncodingEnum) Encoder {
func Create(enc api.EncodingEnum, params *CreateParameter) Encoder {
	if create, ok := registry[enc]; ok {
		return create()
		return create(params)
	}
	return nil
}

// CreateFunc produces a new encoder.
type CreateFunc func() Encoder
type CreateFunc func(*CreateParameter) Encoder

// CreateParameter contains values that are needed to create an encoder.
type CreateParameter struct {
	Lang string // default language
}

var registry = map[api.EncodingEnum]CreateFunc{}

// Register the encoder for later retrieval.
func Register(enc api.EncodingEnum, create CreateFunc) {
	if _, ok := registry[enc]; ok {
		panic(fmt.Sprintf("Encoder %q already registered", enc))

Changes to encoder/encoder_blob_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
1

2
3
4
5
6
7
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-2023 Detlef Stern
// 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 encoder_test

import (
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/parser/blob" // Allow to use BLOB parser.
)

type blobTestCase struct {
	descr  string
	blob   []byte
36
37
38
39
40
41
42
43
44
45


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

46

47
48
49
50
51
52
53
54
55







-

-
+
+







			0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
			0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x3a, 0x7e, 0x9b,
			0x55, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x62, 0x00, 0x00, 0x00,
			0x06, 0x00, 0x03, 0x36, 0x37, 0x7c, 0xa8, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae,
			0x42, 0x60, 0x82,
		},
		expect: expectMap{
			encoderZJSON: `[{"":"BLOB","q":[{"":"Text","s":"PNG"}],"s":"png","o":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="}]`,
			encoderHTML:  `<p><img alt="PNG" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="></p>`,
			encoderSexpr: `((BLOB ((TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSz:    `(BLOCK (BLOB ((TEXT "PNG")) "png" "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg=="))`,
			encoderSHTML: `((p (img (@ (alt . "PNG") (src . "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==")))))`,
			encoderText:  "",
			encoderZmk:   `%% Unable to display BLOB with description 'PNG' and syntax 'png'.`,
		},
	},
}

func TestBlob(t *testing.T) {

Changes to encoder/encoder_block_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
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
1

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

-
+






+
+
+









-


+
-
+








-


-
+
+








-


-
+
+








-
-
+

-
+
+






-
+

-
-
-
-
-
+
+
+
+
+







-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+
+
+
+
+
+
+
+
+
+
+
+
+








-


-
+
+








-


-
+
+








-
-
+

-
+
+






-
+

-
-
+

-
-
+
+
+







-

-
-
+
+
+
















-


-
+
+












-
-
+

-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-
-
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+
+




+
+
+
+
+
+
+
+
+
+
+
+




-


-
+
+











-
-
+

-
+
+







-
-
+
+

+
+
+
+
+
-
+
-
+
+
+
+
+
+
+

-
-
+
+
+







-


-
+
+








-
-
-
-
-
+
+
+
+
+






-

+
-
+









//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 encoder_test

var tcsBlock = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSz:    `(BLOCK)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world",
		zmk:   "Hello, world",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]}]`,
			encoderHTML:  "<p>Hello, world</p>",
			encoderMD:    "Hello, world",
			encoderSexpr: `((PARA (TEXT "Hello,") (SPACE) (TEXT "world")))`,
			encoderSz:    `(BLOCK (PARA (TEXT "Hello, world")))`,
			encoderSHTML: `((p "Hello, world"))`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple block comment",
		zmk:   "%%%\nNo\nrender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","s":"No\nrender"}]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-COMMENT () "No\nrender"))`,
			encoderSz:    `(BLOCK (VERBATIM-COMMENT () "No\nrender"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Rendered block comment",
		zmk:   "%%%{-}\nRender\n%%%",
		expect: expectMap{
			encoderZJSON: `[{"":"CommentBlock","a":{"-":""},"s":"Render"}]`,
			encoderHTML:  "<!--\nRender\n-->",
			encoderHTML:  "<!--\nRender\n-->\n",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-COMMENT (("-" "")) "Render"))`,
			encoderSz:    `(BLOCK (VERBATIM-COMMENT (("-" . "")) "Render"))`,
			encoderSHTML: "((@@@ \"Render\"))",
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Heading",
		zmk:   `=== Top`,
		zmk:   `=== Top Job`,
		expect: expectMap{
			encoderZJSON: `[{"":"Heading","n":1,"s":"top","i":[{"":"Text","s":"Top"}]}]`,
			encoderHTML:  "<h2 id=\"top\">Top</h2>",
			encoderMD:    "# Top",
			encoderSexpr: `((HEADING 1 () "top" "top" (TEXT "Top")))`,
			encoderText:  `Top`,
			encoderHTML:  "<h2 id=\"top-job\">Top Job</h2>",
			encoderMD:    "# Top Job",
			encoderSz:    `(BLOCK (HEADING 1 () "top-job" "top-job" (TEXT "Top Job")))`,
			encoderSHTML: `((h2 (@ (id . "top-job")) "Top Job"))`,
			encoderText:  `Top Job`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List",
		zmk:   "* A\n* B\n* C",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"A"}]}],[{"":"Para","i":[{"":"Text","s":"B"}]}],[{"":"Para","i":[{"":"Text","s":"C"}]}]]}]`,
			encoderHTML:  "<ul><li>A</li><li>B</li><li>C</li></ul>",
			encoderMD:    "* A\n* B\n* C",
			encoderSexpr: `((UNORDERED ((TEXT "A")) ((TEXT "B")) ((TEXT "C"))))`,
			encoderSz:    `(BLOCK (UNORDERED (INLINE (TEXT "A")) (INLINE (TEXT "B")) (INLINE (TEXT "C"))))`,
			encoderSHTML: `((ul (li "A") (li "B") (li "C")))`,
			encoderText:  "A\nB\nC",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested List",
		zmk:   "* T1\n** T2\n* T3\n** T4\n** T5\n* T6",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T1"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T2"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T3"}]},{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"T4"}]}],[{"":"Para","i":[{"":"Text","s":"T5"}]}]]}],[{"":"Para","i":[{"":"Text","s":"T6"}]}]]}]`,
			encoderHTML:  `<ul><li><p>T1</p><ul><li>T2</li></ul></li><li><p>T3</p><ul><li>T4</li><li>T5</li></ul></li><li><p>T6</p></li></ul>`,
			encoderMD:    "* T1\n    * T2\n* T3\n    * T4\n    * T5\n* T6",
			encoderSexpr: `((UNORDERED ((PARA (TEXT "T1")) (UNORDERED ((TEXT "T2")))) ((PARA (TEXT "T3")) (UNORDERED ((TEXT "T4")) ((TEXT "T5")))) ((PARA (TEXT "T6")))))`,
			encoderSz:    `(BLOCK (UNORDERED (BLOCK (PARA (TEXT "T1")) (UNORDERED (INLINE (TEXT "T2")))) (BLOCK (PARA (TEXT "T3")) (UNORDERED (INLINE (TEXT "T4")) (INLINE (TEXT "T5")))) (BLOCK (PARA (TEXT "T6")))))`,
			encoderSHTML: `((ul (li (p "T1") (ul (li "T2"))) (li (p "T3") (ul (li "T4") (li "T5"))) (li (p "T6"))))`,
			encoderText:  "T1\nT2\nT3\nT4\nT5\nT6",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Sequence of two lists",
		zmk:   "* Item1.1\n* Item1.2\n* Item1.3\n\n* Item2.1\n* Item2.2",
		expect: expectMap{
			encoderZJSON: `[{"":"Bullet","c":[[{"":"Para","i":[{"":"Text","s":"Item1.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.2"}]}],[{"":"Para","i":[{"":"Text","s":"Item1.3"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.1"}]}],[{"":"Para","i":[{"":"Text","s":"Item2.2"}]}]]}]`,
			encoderHTML:  "<ul><li>Item1.1</li><li>Item1.2</li><li>Item1.3</li><li>Item2.1</li><li>Item2.2</li></ul>",
			encoderMD:    "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
			encoderSexpr: `((UNORDERED ((TEXT "Item1.1")) ((TEXT "Item1.2")) ((TEXT "Item1.3")) ((TEXT "Item2.1")) ((TEXT "Item2.2"))))`,
			encoderSz:    `(BLOCK (UNORDERED (INLINE (TEXT "Item1.1")) (INLINE (TEXT "Item1.2")) (INLINE (TEXT "Item1.3")) (INLINE (TEXT "Item2.1")) (INLINE (TEXT "Item2.2"))))`,
			encoderSHTML: `((ul (li "Item1.1") (li "Item1.2") (li "Item1.3") (li "Item2.1") (li "Item2.2")))`,
			encoderText:  "Item1.1\nItem1.2\nItem1.3\nItem2.1\nItem2.2",
			encoderZmk:   "* Item1.1\n* Item1.2\n* Item1.3\n* Item2.1\n* Item2.2",
		},
	},
	{
		descr: "Simple horizontal rule",
		zmk:   `---`,
		expect: expectMap{
			encoderZJSON: `[{"":"Thematic"}]`,
			encoderHTML:  "<hr>",
			encoderMD:    "---",
			encoderSexpr: `((THEMATIC ()))`,
			encoderSz:    `(BLOCK (THEMATIC ()))`,
			encoderSHTML: `((hr))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Thematic break with attribute",
		zmk:   `---{lang="zmk"}`,
		expect: expectMap{
			encoderHTML:  `<hr lang="zmk">`,
			encoderMD:    "---",
			encoderSz:    `(BLOCK (THEMATIC (("lang" . "zmk"))))`,
			encoderSHTML: `((hr (@ (lang . "zmk"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "No list after paragraph",
		zmk:   "Text\n*abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Soft"},{"":"Text","s":"*abc"}]}]`,
			encoderHTML:  "<p>Text *abc</p>",
			encoderMD:    "Text\n*abc",
			encoderSexpr: `((PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (SOFT) (TEXT "*abc")))`,
			encoderSHTML: `((p "Text" " " "*abc"))`,
			encoderText:  `Text *abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "A list after paragraph",
		zmk:   "Text\n# abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"}]},{"":"Ordered","c":[[{"":"Para","i":[{"":"Text","s":"abc"}]}]]}]`,
			encoderHTML:  "<p>Text</p><ol><li>abc</li></ol>",
			encoderMD:    "Text\n\n1. abc",
			encoderSexpr: `((PARA (TEXT "Text")) (ORDERED ((TEXT "abc"))))`,
			encoderSz:    `(BLOCK (PARA (TEXT "Text")) (ORDERED (INLINE (TEXT "abc"))))`,
			encoderSHTML: `((p "Text") (ol (li "abc")))`,
			encoderText:  "Text\nabc",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple List Quote",
		zmk:   "> ToBeOrNotToBe",
		expect: expectMap{
			encoderZJSON: `[{"":"Quotation","c":[[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}]]}]`,
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p></blockquote>",
			encoderHTML:  "<blockquote>ToBeOrNotToBe</blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSexpr: `((QUOTATION ((TEXT "ToBeOrNotToBe"))))`,
			encoderSz:    `(BLOCK (QUOTATION (INLINE (TEXT "ToBeOrNotToBe"))))`,
			encoderSHTML: `((blockquote (@L "ToBeOrNotToBe")))`,
			encoderText:  "ToBeOrNotToBe",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Quote Block",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo",
		zmk:   "<<<\nToBeOrNotToBe\n<<< Romeo Julia",
		expect: expectMap{
			encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOrNotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`,
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo</cite></blockquote>",
			encoderHTML:  "<blockquote><p>ToBeOrNotToBe</p><cite>Romeo Julia</cite></blockquote>",
			encoderMD:    "> ToBeOrNotToBe",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) ((TEXT "Romeo"))))`,
			encoderText:  "ToBeOrNotToBe\nRomeo",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOrNotToBe"))) (TEXT "Romeo Julia")))`,
			encoderSHTML: `((blockquote (p "ToBeOrNotToBe") (cite "Romeo Julia")))`,
			encoderText:  "ToBeOrNotToBe\nRomeo Julia",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quote Block with multiple paragraphs",
		zmk:   "<<<\nToBeOr\n\nNotToBe\n<<< Romeo",
		expect: expectMap{
			encoderZJSON: `[{"":"Excerpt","b":[{"":"Para","i":[{"":"Text","s":"ToBeOr"}]},{"":"Para","i":[{"":"Text","s":"NotToBe"}]}],"i":[{"":"Text","s":"Romeo"}]}]`,
			encoderHTML:  "<blockquote><p>ToBeOr</p><p>NotToBe</p><cite>Romeo</cite></blockquote>",
			encoderMD:    "> ToBeOr\n\n> NotToBe",
			encoderSexpr: `((REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) ((TEXT "Romeo"))))`,
			encoderMD:    "> ToBeOr\n>\n> NotToBe",
			encoderSz:    `(BLOCK (REGION-QUOTE () ((PARA (TEXT "ToBeOr")) (PARA (TEXT "NotToBe"))) (TEXT "Romeo")))`,
			encoderSHTML: `((blockquote (p "ToBeOr") (p "NotToBe") (cite "Romeo")))`,
			encoderText:  "ToBeOr\nNotToBe\nRomeo",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Verse block",
		zmk: `"""
A line
  another line
Back

Paragraph

    Spacy  Para
""" Author`,
		expect: expectMap{
			encoderZJSON: "[{\"\":\"Poem\",\"b\":[{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"A\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"another\"},{\"\":\"Space\",\"s\":\"\u00a0\"},{\"\":\"Text\",\"s\":\"line\"},{\"\":\"Hard\"},{\"\":\"Text\",\"s\":\"Back\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Text\",\"s\":\"Paragraph\"}]},{\"\":\"Para\",\"i\":[{\"\":\"Space\",\"s\":\"\u00a0\u00a0\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Spacy\"},{\"\":\"Space\",\"s\":\"\u00a0\u00a0\"},{\"\":\"Text\",\"s\":\"Para\"}]}],\"i\":[{\"\":\"Text\",\"s\":\"Author\"}]}]",
			encoderHTML:  "<div><p>A\u00a0line<br>\u00a0\u00a0another\u00a0line<br>Back</p><p>Paragraph</p><p>\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para</p><cite>Author</cite></div>",
			encoderMD:    "",
			encoderSexpr: "((REGION-VERSE () ((PARA (TEXT \"A\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (SPACE \"\u00a0\u00a0\") (TEXT \"another\") (SPACE \"\u00a0\") (TEXT \"line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (SPACE \"\u00a0\u00a0\u00a0\u00a0\") (TEXT \"Spacy\") (SPACE \"\u00a0\u00a0\") (TEXT \"Para\"))) ((TEXT \"Author\"))))",
			encoderSz:    "(BLOCK (REGION-VERSE () ((PARA (TEXT \"A\u00a0line\") (HARD) (TEXT \"\u00a0\u00a0another\u00a0line\") (HARD) (TEXT \"Back\")) (PARA (TEXT \"Paragraph\")) (PARA (TEXT \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\"))) (TEXT \"Author\")))",
			encoderSHTML: "((div (p \"A\u00a0line\" (br) \"\u00a0\u00a0another\u00a0line\" (br) \"Back\") (p \"Paragraph\") (p \"\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\") (cite \"Author\")))",
			encoderText:  "A line\n another line\nBack\nParagraph\n Spacy Para\nAuthor",
			encoderZmk:   "\"\"\"\nA\u00a0line\\\n\u00a0\u00a0another\u00a0line\\\nBack\nParagraph\n\u00a0\u00a0\u00a0\u00a0Spacy\u00a0\u00a0Para\n\"\"\" Author",
		},
	},
	{
		descr: "Span Block",
		zmk: `:::
A simple
   span
and much more
:::`,
		expect: expectMap{
			encoderZJSON: `[{"":"Block","b":[{"":"Para","i":[{"":"Text","s":"A"},{"":"Space"},{"":"Text","s":"simple"},{"":"Soft"},{"":"Space"},{"":"Text","s":"span"},{"":"Soft"},{"":"Text","s":"and"},{"":"Space"},{"":"Text","s":"much"},{"":"Space"},{"":"Text","s":"more"}]}]}]`,
			encoderHTML:  "<div><p>A simple  span and much more</p></div>",
			encoderHTML:  "<div><p>A simple    span and much more</p></div>",
			encoderMD:    "",
			encoderSexpr: `((REGION-BLOCK () ((PARA (TEXT "A") (SPACE) (TEXT "simple") (SOFT) (SPACE) (TEXT "span") (SOFT) (TEXT "and") (SPACE) (TEXT "much") (SPACE) (TEXT "more"))) ()))`,
			encoderSz:    `(BLOCK (REGION-BLOCK () ((PARA (TEXT "A simple") (SOFT) (TEXT "   span") (SOFT) (TEXT "and much more")))))`,
			encoderSHTML: `((div (p "A simple" " " "   span" " " "and much more")))`,
			encoderText:  `A simple  span and much more`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code",
		zmk:   "```\nHello\nWorld\n```",
		expect: expectMap{
			encoderZJSON: `[{"":"CodeBlock","s":"Hello\nWorld"}]`,
			encoderHTML:  "<pre><code>Hello\nWorld</code></pre>",
			encoderMD:    "    Hello\n    World",
			encoderSexpr: `((VERBATIM-CODE () "Hello\nWorld"))`,
			encoderSz:    `(BLOCK (VERBATIM-CODE () "Hello\nWorld"))`,
			encoderSHTML: `((pre (code "Hello\nWorld")))`,
			encoderText:  "Hello\nWorld",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Code with visible spaces",
		zmk:   "```{-}\nHello World\n```",
		expect: expectMap{
			encoderZJSON: `[{"":"CodeBlock","a":{"-":""},"s":"Hello World"}]`,
			encoderHTML:  "<pre><code>Hello\u2423World</code></pre>",
			encoderMD:    "    Hello World",
			encoderSexpr: `((VERBATIM-CODE (("-" "")) "Hello World"))`,
			encoderSz:    `(BLOCK (VERBATIM-CODE (("-" . "")) "Hello World"))`,
			encoderSHTML: "((pre (code \"Hello\u2423World\")))",
			encoderText:  "Hello World",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Eval",
		zmk:   "~~~\nHello\nWorld\n~~~",
		expect: expectMap{
			encoderZJSON: `[{"":"EvalBlock","s":"Hello\nWorld"}]`,
			encoderHTML:  "<pre><code class=\"zs-eval\">Hello\nWorld</code></pre>",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSz:    `(BLOCK (VERBATIM-EVAL () "Hello\nWorld"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-eval\")) \"Hello\\nWorld\")))",
			encoderText:  "Hello\nWorld",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Verbatim Math",
		zmk:   "$$$\nHello\n\\LaTeX\n$$$",
		expect: expectMap{
			encoderZJSON: `[{"":"MathBlock","s":"Hello\n\\LaTeX"}]`,
			encoderHTML:  "<pre><code class=\"zs-math\">Hello\n\\LaTeX</code></pre>",
			encoderMD:    "",
			encoderSexpr: `((VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSz:    `(BLOCK (VERBATIM-MATH () "Hello\n\\LaTeX"))`,
			encoderSHTML: "((pre (code (@ (class . \"zs-math\")) \"Hello\\n\\\\LaTeX\")))",
			encoderText:  "Hello\n\\LaTeX",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Description List",
		zmk:   "; Zettel\n: Paper\n: Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderZJSON: `[{"":"Description","d":[{"i":[{"":"Text","s":"Zettel"}],"e":[[{"":"Para","i":[{"":"Text","s":"Paper"}]}],[{"":"Para","i":[{"":"Text","s":"Note"}]}]]},{"i":[{"":"Text","s":"Zettelkasten"}],"e":[[{"":"Para","i":[{"":"Text","s":"Slip"},{"":"Space"},{"":"Text","s":"box"}]}]]}]}]`,
			encoderHTML:  "<dl><dt>Zettel</dt><dd>Paper</dd><dd>Note</dd><dt>Zettelkasten</dt><dd>Slip box</dd></dl>",
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p></dd><dd><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper"))) (BLOCK (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper")) (dd (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with paragraphs as item",
		zmk:   "; Zettel\n: Paper\n\n  Note\n; Zettelkasten\n: Slip box",
		expect: expectMap{
			encoderHTML:  "<dl><dt>Zettel</dt><dd><p>Paper</p><p>Note</p></dd><dt>Zettelkasten</dt><dd><p>Slip box</p></dd></dl>",
			encoderMD:    "",
			encoderSexpr: `((DESCRIPTION ((TEXT "Zettel")) (((TEXT "Paper")) ((TEXT "Note"))) ((TEXT "Zettelkasten")) (((TEXT "Slip") (SPACE) (TEXT "box")))))`,
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "Zettel")) (BLOCK (BLOCK (PARA (TEXT "Paper")) (PARA (TEXT "Note")))) ((TEXT "Zettelkasten")) (BLOCK (BLOCK (PARA (TEXT "Slip box"))))))`,
			encoderSHTML: `((dl (dt "Zettel") (dd (p "Paper") (p "Note")) (dt "Zettelkasten") (dd (p "Slip box"))))`,
			encoderText:  "Zettel\nPaper\nNote\nZettelkasten\nSlip box",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Description List with keys, but no descriptions",
		zmk:   "; K1\n: D11\n: D12\n; K2\n; K3\n: D31",
		expect: expectMap{
			encoderHTML:  "<dl><dt>K1</dt><dd><p>D11</p></dd><dd><p>D12</p></dd><dt>K2</dt><dt>K3</dt><dd><p>D31</p></dd></dl>",
			encoderMD:    "",
			encoderSz:    `(BLOCK (DESCRIPTION ((TEXT "K1")) (BLOCK (BLOCK (PARA (TEXT "D11"))) (BLOCK (PARA (TEXT "D12")))) ((TEXT "K2")) (BLOCK) ((TEXT "K3")) (BLOCK (BLOCK (PARA (TEXT "D31"))))))`,
			encoderSHTML: `((dl (dt "K1") (dd (p "D11")) (dd (p "D12")) (dt "K2") (dt "K3") (dd (p "D31"))))`,
			encoderText:  "K1\nD11\nD12\nK2\nK3\nD31",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Table",
		zmk:   "|c1|c2|c3\n|d1||d3",
		expect: expectMap{
			encoderZJSON: `[{"":"Table","p":[[],[[{"i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"i":[{"":"Text","s":"c3"}]}],[{"i":[{"":"Text","s":"d1"}]},{"i":[]},{"i":[{"":"Text","s":"d3"}]}]]]}]`,
			encoderHTML:  `<table><tbody><tr><td>c1</td><td>c2</td><td>c3</td></tr><tr><td>d1</td><td></td><td>d3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSexpr: `((TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderSz:    `(BLOCK (TABLE () ((CELL (TEXT "c1")) (CELL (TEXT "c2")) (CELL (TEXT "c3"))) ((CELL (TEXT "d1")) (CELL) (CELL (TEXT "d3")))))`,
			encoderSHTML: `((table (tbody (tr (td "c1") (td "c2") (td "c3")) (tr (td "d1") (td) (td "d3")))))`,
			encoderText:  "c1 c2 c3\nd1  d3",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Table with alignment and comment",
		zmk: `|h1>|=h2|h3:|
|%--+---+---+
|<c1|c2|:c3|
|f1|f2|=f3`,
		expect: expectMap{
			encoderZJSON: `[{"":"Table","p":[[{"s":">","i":[{"":"Text","s":"h1"}]},{"i":[{"":"Text","s":"h2"}]},{"s":":","i":[{"":"Text","s":"h3"}]}],[[{"s":"<","i":[{"":"Text","s":"c1"}]},{"i":[{"":"Text","s":"c2"}]},{"s":":","i":[{"":"Text","s":"c3"}]}],[{"s":">","i":[{"":"Text","s":"f1"}]},{"i":[{"":"Text","s":"f2"}]},{"s":":","i":[{"":"Text","s":"=f3"}]}]]]}]`,
			encoderHTML:  `<table><thead><tr><td class="right">h1</td><td>h2</td><td class="center">h3</td></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderHTML:  `<table><thead><tr><th class="right">h1</th><th>h2</th><th class="center">h3</th></tr></thead><tbody><tr><td class="left">c1</td><td>c2</td><td class="center">c3</td></tr><tr><td class="right">f1</td><td>f2</td><td class="center">=f3</td></tr></tbody></table>`,
			encoderMD:    "",
			encoderSexpr: `((TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSz:    `(BLOCK (TABLE ((CELL-RIGHT (TEXT "h1")) (CELL (TEXT "h2")) (CELL-CENTER (TEXT "h3"))) ((CELL-LEFT (TEXT "c1")) (CELL (TEXT "c2")) (CELL-CENTER (TEXT "c3"))) ((CELL-RIGHT (TEXT "f1")) (CELL (TEXT "f2")) (CELL-CENTER (TEXT "=f3")))))`,
			encoderSHTML: `((table (thead (tr (th (@ (class . "right")) "h1") (th "h2") (th (@ (class . "center")) "h3"))) (tbody (tr (td (@ (class . "left")) "c1") (td "c2") (td (@ (class . "center")) "c3")) (tr (td (@ (class . "right")) "f1") (td "f2") (td (@ (class . "center")) "=f3")))))`,
			encoderText:  "h1 h2 h3\nc1 c2 c3\nf1 f2 =f3",
			encoderZmk: `|=h1>|=h2|=h3:
|<c1|c2|c3
|f1|f2|=f3`,
		},
	},
	{
		descr: "Simple Endnotes",
		zmk:   `Text[^Footnote]`,
		descr: "Simple Endnote",
		zmk:   `Text[^Endnote]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote"))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote",
			encoderZJSON: `[{"":"Para","i":[{"":"Text","s":"Text"},{"":"Footnote","i":[{"":"Text","s":"Footnote"}]}]}]`,
			encoderZmk:   useZmk,
			encoderHTML:  `<p>Text<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup></p><ol class="zs-endnotes"><li class="zs-endnote" id="fn:1" role="doc-endnote" value="1">Footnote <a class="zs-endnote-backref" href="#fnref:1" role="doc-backlink">&#x21a9;&#xfe0e;</a></li></ol>`,
		},
	},
	{
		descr: "Nested Endnotes",
		zmk:   `Text[^Endnote[^Nested]]`,
		expect: expectMap{
			encoderHTML:  "<p>Text<sup id=\"fnref:1\"><a class=\"zs-noteref\" href=\"#fn:1\" role=\"doc-noteref\">1</a></sup></p><ol class=\"zs-endnotes\"><li class=\"zs-endnote\" id=\"fn:1\" role=\"doc-endnote\" value=\"1\">Endnote<sup id=\"fnref:2\"><a class=\"zs-noteref\" href=\"#fn:2\" role=\"doc-noteref\">2</a></sup> <a class=\"zs-endnote-backref\" href=\"#fnref:1\" role=\"doc-backlink\">\u21a9\ufe0e</a></li><li class=\"zs-endnote\" id=\"fn:2\" role=\"doc-endnote\" value=\"2\">Nested <a class=\"zs-endnote-backref\" href=\"#fnref:2\" role=\"doc-backlink\">\u21a9\ufe0e</a></li></ol>",
			encoderMD:    "Text",
			encoderSexpr: `((PARA (TEXT "Text") (FOOTNOTE () (TEXT "Footnote"))))`,
			encoderText:  "Text Footnote",
			encoderSz:    `(BLOCK (PARA (TEXT "Text") (ENDNOTE () (TEXT "Endnote") (ENDNOTE () (TEXT "Nested")))))`,
			encoderSHTML: "((p \"Text\" (sup (@ (id . \"fnref:1\")) (a (@ (class . \"zs-noteref\") (href . \"#fn:1\") (role . \"doc-noteref\")) \"1\"))))",
			encoderText:  "Text Endnote Nested",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Transclusion",
		zmk:   `{{{http://example.com/image}}}{width="100px"}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Transclude","a":{"width":"100px"},"q":"external","s":"http://example.com/image"}]`,
			encoderHTML:  `<p><img class="external" src="http://example.com/image" width="100px"></p>`,
			encoderMD:    "",
			encoderSexpr: `((TRANSCLUDE (("width" "100px")) (EXTERNAL "http://example.com/image")))`,
			encoderSz:    `(BLOCK (TRANSCLUDE (("width" . "100px")) (EXTERNAL "http://example.com/image")))`,
			encoderSHTML: `((p (img (@ (class . "external") (src . "http://example.com/image") (width . "100px")))))`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "A paragraph with a inline comment only should be empty in HTML",
		zmk:   `%% Comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Para","i":[{"":"Comment","s":"Comment"}]}]`,
			encoderHTML:  ``,
			encoderSexpr: `((PARA (LITERAL-COMMENT () "Comment")))`,
			encoderText:  "",
			encoderZmk:   useZmk,
			// encoderHTML:  ``,
			encoderSz: `(BLOCK (PARA (LITERAL-COMMENT () "Comment")))`,
			// encoderSHTML: ``,
			encoderText: "",
			encoderZmk:  useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderSz:    `(BLOCK)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
}

// func TestEncoderBlock(t *testing.T) {
// 	executeTestCases(t, tcsBlock)
// }

Changes to encoder/encoder_inline_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
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
1

2
3
4
5
6
7
8
9
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
674
675
676

-
+






+
+
+









-


+
-
+








-


-
+
+




+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-
-
-
-
+
+
+
+








-
-
-
-
+
+
+
+





+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-

-
-
+
+
+








-

-
-
+
+
+








-


-
+
+








-
-
-
-
+
+
+
+








-


-
+
+





+
+
+
+
+
+
+
+
+
+
+



-


-
+
+








-


-
+
+








-


+
-
+








-


-
+
+








-
-
+

-
+
+





-
-
+
+

-


-
-
+
+
+







-


-
+
+








-


-
+
+




+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
-
+

-
+
+








-
-
+

-
+
+








-
-
+

-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+








-


-
+
+






-
+

-


-
+

+







-


-
+
+








-


-
+
+








-


-
+
+








-
-
+

-
+
+








-


+
-
+








-


-
+
+








-


+
-
+





//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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 encoder_test

var tcsInline = []zmkTestCase{
	{
		descr: "Empty Zettelmarkup should produce near nothing (inline)",
		zmk:   "",
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  "",
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  "",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple text: Hello, world (inline)",
		zmk:   `Hello, world`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Hello,"},{"":"Space"},{"":"Text","s":"world"}]`,
			encoderHTML:  "Hello, world",
			encoderMD:    "Hello, world",
			encoderSexpr: `((TEXT "Hello,") (SPACE) (TEXT "world"))`,
			encoderSz:    `(INLINE (TEXT "Hello, world"))`,
			encoderSHTML: `("Hello, world")`,
			encoderText:  "Hello, world",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Soft Break",
		zmk:   "soft\nbreak",
		expect: expectMap{
			encoderHTML:  "soft break",
			encoderMD:    "soft\nbreak",
			encoderSz:    `(INLINE (TEXT "soft") (SOFT) (TEXT "break"))`,
			encoderSHTML: `("soft" " " "break")`,
			encoderText:  "soft break",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hard Break",
		zmk:   "hard\\\nbreak",
		expect: expectMap{
			encoderHTML:  "hard<br>break",
			encoderMD:    "hard\\\nbreak",
			encoderSz:    `(INLINE (TEXT "hard") (HARD) (TEXT "break"))`,
			encoderSHTML: `("hard" (br) "break")`,
			encoderText:  "hard\nbreak",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Emphasized formatting",
		zmk:   "__emph__",
		expect: expectMap{
			encoderZJSON: `[{"":"Emph","i":[{"":"Text","s":"emph"}]}]`,
			encoderHTML:  "<em>emph</em>",
			encoderMD:    "*emph*",
			encoderSexpr: `((FORMAT-EMPH () (TEXT "emph")))`,
			encoderSz:    `(INLINE (FORMAT-EMPH () (TEXT "emph")))`,
			encoderSHTML: `((em "emph"))`,
			encoderText:  "emph",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Strong formatting",
		zmk:   "**strong**",
		expect: expectMap{
			encoderZJSON: `[{"":"Strong","i":[{"":"Text","s":"strong"}]}]`,
			encoderHTML:  "<strong>strong</strong>",
			encoderMD:    "__strong__",
			encoderSexpr: `((FORMAT-STRONG () (TEXT "strong")))`,
			encoderSz:    `(INLINE (FORMAT-STRONG () (TEXT "strong")))`,
			encoderSHTML: `((strong "strong"))`,
			encoderText:  "strong",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Insert formatting",
		zmk:   ">>insert>>",
		expect: expectMap{
			encoderZJSON: `[{"":"Insert","i":[{"":"Text","s":"insert"}]}]`,
			encoderHTML:  "<ins>insert</ins>",
			encoderMD:    "insert",
			encoderSexpr: `((FORMAT-INSERT () (TEXT "insert")))`,
			encoderSz:    `(INLINE (FORMAT-INSERT () (TEXT "insert")))`,
			encoderSHTML: `((ins "insert"))`,
			encoderText:  "insert",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Delete formatting",
		zmk:   "~~delete~~",
		expect: expectMap{
			encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"delete"}]}]`,
			encoderHTML:  "<del>delete</del>",
			encoderMD:    "delete",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "delete")))`,
			encoderSz:    `(INLINE (FORMAT-DELETE () (TEXT "delete")))`,
			encoderSHTML: `((del "delete"))`,
			encoderText:  "delete",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Update formatting",
		zmk:   "~~old~~>>new>>",
		expect: expectMap{
			encoderZJSON: `[{"":"Delete","i":[{"":"Text","s":"old"}]},{"":"Insert","i":[{"":"Text","s":"new"}]}]`,
			encoderHTML:  "<del>old</del><ins>new</ins>",
			encoderMD:    "oldnew",
			encoderSexpr: `((FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderSz:    `(INLINE (FORMAT-DELETE () (TEXT "old")) (FORMAT-INSERT () (TEXT "new")))`,
			encoderSHTML: `((del "old") (ins "new"))`,
			encoderText:  "oldnew",
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Superscript formatting",
		zmk:   "^^superscript^^",
		expect: expectMap{
			encoderZJSON: `[{"":"Super","i":[{"":"Text","s":"superscript"}]}]`,
			encoderHTML:  `<sup>superscript</sup>`,
			encoderMD:    "superscript",
			encoderSexpr: `((FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSz:    `(INLINE (FORMAT-SUPER () (TEXT "superscript")))`,
			encoderSHTML: `((sup "superscript"))`,
			encoderText:  `superscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Subscript formatting",
		zmk:   ",,subscript,,",
		expect: expectMap{
			encoderZJSON: `[{"":"Sub","i":[{"":"Text","s":"subscript"}]}]`,
			encoderHTML:  `<sub>subscript</sub>`,
			encoderMD:    "subscript",
			encoderSexpr: `((FORMAT-SUB () (TEXT "subscript")))`,
			encoderSz:    `(INLINE (FORMAT-SUB () (TEXT "subscript")))`,
			encoderSHTML: `((sub "subscript"))`,
			encoderText:  `subscript`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting",
		zmk:   `""quotes""`,
		expect: expectMap{
			encoderZJSON: `[{"":"Quote","i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:  "<q>quotes</q>",
			encoderMD:    "<q>quotes</q>",
			encoderSexpr: `((FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderHTML:  "&ldquo;quotes&rdquo;",
			encoderMD:    "&ldquo;quotes&rdquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "quotes")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "quotes" (@H "&rdquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Quotes formatting (german)",
		zmk:   `""quotes""{lang=de}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Quote","a":{"lang":"de"},"i":[{"":"Text","s":"quotes"}]}]`,
			encoderHTML:  `<span lang="de"><q>quotes</q></span>`,
			encoderMD:    "<q>quotes</q>",
			encoderSexpr: `((FORMAT-QUOTE (("lang" "de")) (TEXT "quotes")))`,
			encoderHTML:  `<span lang="de">&bdquo;quotes&ldquo;</span>`,
			encoderMD:    "&bdquo;quotes&ldquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "de")) (TEXT "quotes")))`,
			encoderSHTML: `((span (@ (lang . "de")) (@H "&bdquo;") "quotes" (@H "&ldquo;")))`,
			encoderText:  `quotes`,
			encoderZmk:   `""quotes""{lang="de"}`,
		},
	},
	{
		descr: "Empty quotes (default)",
		zmk:   `""""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;&rdquo;`,
			encoderMD:    "&ldquo;&rdquo;",
			encoderSz:    `(INLINE (FORMAT-QUOTE ()))`,
			encoderSHTML: `((@L (@H "&ldquo;" "&rdquo;")))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Empty quotes (unknown)",
		zmk:   `""""{lang=unknown}`,
		expect: expectMap{
			encoderHTML:  `<span lang="unknown">&quot;&quot;</span>`,
			encoderMD:    "&quot;&quot;",
			encoderSz:    `(INLINE (FORMAT-QUOTE (("lang" . "unknown"))))`,
			encoderSHTML: `((span (@ (lang . "unknown")) (@H "&quot;" "&quot;")))`,
			encoderText:  ``,
			encoderZmk:   `""""{lang="unknown"}`,
		},
	},
	{
		descr: "Nested quotes (default)",
		zmk:   `""say: ::""yes, ::""or?""::""::""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;say: <span>&lsquo;yes, <span>&ldquo;or?&rdquo;</span>&rsquo;</span>&rdquo;`,
			encoderMD:    `&ldquo;say: &lsquo;yes, &ldquo;or?&rdquo;&rsquo;&rdquo;`,
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "say: ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "yes, ") (FORMAT-SPAN () (FORMAT-QUOTE () (TEXT "or?")))))))`,
			encoderSHTML: `((@L (@H "&ldquo;") "say: " (span (@L (@H "&lsquo;") "yes, " (span (@L (@H "&ldquo;") "or?" (@H "&rdquo;"))) (@H "&rsquo;"))) (@H "&rdquo;")))`,
			encoderText:  `say: yes, or?`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Two quotes",
		zmk:   `""yes"" or ""no""`,
		expect: expectMap{
			encoderHTML:  `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderMD:    `&ldquo;yes&rdquo; or &ldquo;no&rdquo;`,
			encoderSz:    `(INLINE (FORMAT-QUOTE () (TEXT "yes")) (TEXT " or ") (FORMAT-QUOTE () (TEXT "no")))`,
			encoderSHTML: `((@L (@H "&ldquo;") "yes" (@H "&rdquo;")) " or " (@L (@H "&ldquo;") "no" (@H "&rdquo;")))`,
			encoderText:  `yes or no`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark formatting",
		zmk:   `##marked##`,
		expect: expectMap{
			encoderHTML:  `<mark>marked</mark>`,
			encoderMD:    "<mark>marked</mark>",
			encoderSz:    `(INLINE (FORMAT-MARK () (TEXT "marked")))`,
			encoderSHTML: `((mark "marked"))`,
			encoderText:  `marked`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Span formatting",
		zmk:   `::span::`,
		expect: expectMap{
			encoderZJSON: `[{"":"Span","i":[{"":"Text","s":"span"}]}]`,
			encoderHTML:  `<span>span</span>`,
			encoderMD:    "span",
			encoderSexpr: `((FORMAT-SPAN () (TEXT "span")))`,
			encoderSz:    `(INLINE (FORMAT-SPAN () (TEXT "span")))`,
			encoderSHTML: `((span "span"))`,
			encoderText:  `span`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting",
		zmk:   "``code``",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"code"}]`,
			encoderHTML:  `<code>code</code>`,
			encoderMD:    "`code`",
			encoderSexpr: `((LITERAL-CODE () "code"))`,
			encoderSz:    `(INLINE (LITERAL-CODE () "code"))`,
			encoderSHTML: `((code "code"))`,
			encoderText:  `code`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Code formatting with visible space",
		zmk:   "``x y``{-}",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","a":{"-":""},"s":"x y"}]`,
			encoderHTML:  "<code>x\u2423y</code>",
			encoderMD:    "`x y`",
			encoderSexpr: `((LITERAL-CODE (("-" "")) "x y"))`,
			encoderSz:    `(INLINE (LITERAL-CODE (("-" . "")) "x y"))`,
			encoderSHTML: "((code \"x\u2423y\"))",
			encoderText:  `x y`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "HTML in Code formatting",
		zmk:   "``<script `` abc",
		expect: expectMap{
			encoderZJSON: `[{"":"Code","s":"<script "},{"":"Space"},{"":"Text","s":"abc"}]`,
			encoderHTML:  "<code>&lt;script </code> abc",
			encoderMD:    "`<script ` abc",
			encoderSexpr: `((LITERAL-CODE () "<script ") (SPACE) (TEXT "abc"))`,
			encoderSz:    `(INLINE (LITERAL-CODE () "<script ") (TEXT " abc"))`,
			encoderSHTML: `((code "<script ") " abc")`,
			encoderText:  `<script  abc`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Input formatting",
		zmk:   `''input''`,
		expect: expectMap{
			encoderZJSON: `[{"":"Input","s":"input"}]`,
			encoderHTML:  `<kbd>input</kbd>`,
			encoderMD:    "input",
			encoderSexpr: `((LITERAL-INPUT () "input"))`,
			encoderMD:    "`input`",
			encoderSz:    `(INLINE (LITERAL-INPUT () "input"))`,
			encoderSHTML: `((kbd "input"))`,
			encoderText:  `input`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Output formatting",
		zmk:   `==output==`,
		expect: expectMap{
			encoderZJSON: `[{"":"Output","s":"output"}]`,
			encoderHTML:  `<samp>output</samp>`,
			encoderMD:    "output",
			encoderSexpr: `((LITERAL-OUTPUT () "output"))`,
			encoderMD:    "`output`",
			encoderSz:    `(INLINE (LITERAL-OUTPUT () "output"))`,
			encoderSHTML: `((samp "output"))`,
			encoderText:  `output`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Math formatting",
		zmk:   `$$\TeX$$`,
		expect: expectMap{
			encoderZJSON: `[{"":"Math","s":"\\TeX"}]`,
			encoderHTML:  `<code class="zs-math">\TeX</code>`,
			encoderMD:    "\\TeX",
			encoderSexpr: `((LITERAL-MATH () "\\TeX"))`,
			encoderSz:    `(INLINE (LITERAL-MATH () "\\TeX"))`,
			encoderSHTML: `((code (@ (class . "zs-math")) "\\TeX"))`,
			encoderText:  `\TeX`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Nested Span Quote formatting",
		zmk:   `::""abc""::{lang=fr}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Span","a":{"lang":"fr"},"i":[{"":"Quote","i":[{"":"Text","s":"abc"}]}]}]`,
			encoderHTML:  `<span lang="fr"><q>abc</q></span>`,
			encoderMD:    "<q>abc</q>",
			encoderSexpr: `((FORMAT-SPAN (("lang" "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderHTML:  `<span lang="fr">&laquo;&nbsp;abc&nbsp;&raquo;</span>`,
			encoderMD:    "&laquo;&nbsp;abc&nbsp;&raquo;",
			encoderSz:    `(INLINE (FORMAT-SPAN (("lang" . "fr")) (FORMAT-QUOTE () (TEXT "abc"))))`,
			encoderSHTML: `((span (@ (lang . "fr")) (@L (@H "&laquo;" "&nbsp;") "abc" (@H "&nbsp;" "&raquo;"))))`,
			encoderText:  `abc`,
			encoderZmk:   `::""abc""::{lang="fr"}`,
		},
	},
	{
		descr: "Simple Citation",
		zmk:   `[@Stern18]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Cite","s":"Stern18"}]`,
			encoderHTML:  `<span>Stern18</span>`, // TODO
			encoderMD:    "",
			encoderSexpr: `((CITE () "Stern18"))`,
			encoderSz:    `(INLINE (CITE () "Stern18"))`,
			encoderSHTML: `((span "Stern18"))`, // TODO
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Citation",
		zmk:   `[@Stern18 p.23]`,
		expect: expectMap{
			encoderHTML:  `<span>Stern18, p.23</span>`, // TODO
			encoderMD:    "p.23",
			encoderSz:    `(INLINE (CITE () "Stern18" (TEXT "p.23")))`,
			encoderSHTML: `((span "Stern18" ", " "p.23"))`, // TODO
			encoderText:  `p.23`,
			encoderZmk:   useZmk,
		},
	}, {
		descr: "No comment",
		zmk:   `% comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"%"},{"":"Space"},{"":"Text","s":"comment"}]`,
			encoderHTML:  `% comment`,
			encoderMD:    "% comment",
			encoderSexpr: `((TEXT "%") (SPACE) (TEXT "comment"))`,
			encoderSz:    `(INLINE (TEXT "% comment"))`,
			encoderSHTML: `("% comment")`,
			encoderText:  `% comment`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment (nogen HTML)",
		zmk:   `%% line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","s":"line comment"}]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSexpr: `((LITERAL-COMMENT () "line comment"))`,
			encoderSz:    `(INLINE (LITERAL-COMMENT () "line comment"))`,
			encoderSHTML: `(())`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Line comment",
		zmk:   `%%{-} line comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Comment","a":{"-":""},"s":"line comment"}]`,
			encoderHTML:  `<!-- line comment -->`,
			encoderMD:    "",
			encoderSz:    `(INLINE (LITERAL-COMMENT (("-" . "")) "line comment"))`,
			encoderSexpr: `((LITERAL-COMMENT (("-" "")) "line comment"))`,
			encoderSHTML: `((@@ "line comment"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text",
		zmk:   `Text %%{-} comment`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment"}]`,
			encoderHTML:  `Text<!-- comment -->`,
			encoderMD:    "Text",
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment"))`,
			encoderSz:    `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment"))`,
			encoderSHTML: `("Text" (@@ "comment"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Comment after text and with -->",
		zmk:   `Text %%{-} comment --> end`,
		expect: expectMap{
			encoderZJSON: `[{"":"Text","s":"Text"},{"":"Comment","a":{"-":""},"s":"comment --> end"}]`,
			encoderHTML:  `Text<!-- comment --&gt; end -->`,
			encoderHTML:  `Text<!-- comment -&#45;> end -->`,
			encoderMD:    "Text",
			encoderSexpr: `((TEXT "Text") (LITERAL-COMMENT (("-" "")) "comment --> end"))`,
			encoderSz:    `(INLINE (TEXT "Text") (LITERAL-COMMENT (("-" . "")) "comment --> end"))`,
			encoderSHTML: `("Text" (@@ "comment --> end"))`,
			encoderText:  `Text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple footnote",
		zmk:   `[^footnote]`,
		descr: "Simple inline endnote",
		zmk:   `[^endnote]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Footnote","i":[{"":"Text","s":"footnote"}]}]`,
			encoderHTML:  `<sup id="fnref:1"><a class="zs-noteref" href="#fn:1" role="doc-noteref">1</a></sup>`,
			encoderMD:    "",
			encoderSexpr: `((FOOTNOTE () (TEXT "footnote")))`,
			encoderText:  `footnote`,
			encoderSz:    `(INLINE (ENDNOTE () (TEXT "endnote")))`,
			encoderSHTML: `((sup (@ (id . "fnref:1")) (a (@ (class . "zs-noteref") (href . "#fn:1") (role . "doc-noteref")) "1")))`,
			encoderText:  `endnote`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple mark",
		zmk:   `[!mark]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark"}]`,
			encoderHTML:  `<a id="mark"></a>`,
			encoderMD:    "",
			encoderSexpr: `((MARK "mark" "mark" "mark"))`,
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark"))`,
			encoderSHTML: `((a (@ (id . "mark"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Mark with text",
		zmk:   `[!mark|with text]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Mark","s":"mark","q":"mark","i":[{"":"Text","s":"with"},{"":"Space"},{"":"Text","s":"text"}]}]`,
			encoderHTML:  `<a id="mark">with text</a>`,
			encoderMD:    "with text",
			encoderSexpr: `((MARK "mark" "mark" "mark" (TEXT "with") (SPACE) (TEXT "text")))`,
			encoderSz:    `(INLINE (MARK "mark" "mark" "mark" (TEXT "with text")))`,
			encoderSHTML: `((a (@ (id . "mark")) "with text"))`,
			encoderText:  `with text`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Link",
		zmk:   `[[link|00000000000000]]`,
		expect: expectMap{
			encoderHTML:  `<span>link</span>`,
			encoderMD:    "[link](00000000000000)",
			encoderSz:    `(INLINE (LINK-INVALID () "00000000000000" (TEXT "link")))`,
			encoderSHTML: `((span "link"))`,
			encoderText:  `link`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Invalid Simple Link",
		zmk:   `[[00000000000000]]`,
		expect: expectMap{
			encoderHTML:  `<span>00000000000000</span>`,
			encoderMD:    "[00000000000000](00000000000000)",
			encoderSz:    `(INLINE (LINK-INVALID () "00000000000000"))`,
			encoderSHTML: `((span "00000000000000"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Link",
		zmk:   `[[abc]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"abc"}]`,
			encoderHTML:  `<a class="external" href="abc">abc</a>`,
			encoderHTML:  `<a href="abc" rel="external">abc</a>`,
			encoderMD:    "[abc](abc)",
			encoderSexpr: `((LINK-EXTERNAL () "abc"))`,
			encoderSz:    `(INLINE (LINK-EXTERNAL () "abc"))`,
			encoderSHTML: `((a (@ (href . "abc") (rel . "external")) "abc"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple URL",
		zmk:   `[[https://zettelstore.de]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de"}]`,
			encoderHTML:  `<a class="external" href="https://zettelstore.de">https://zettelstore.de</a>`,
			encoderHTML:  `<a href="https://zettelstore.de" rel="external">https://zettelstore.de</a>`,
			encoderMD:    "<https://zettelstore.de>",
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de"))`,
			encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "https://zettelstore.de"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "URL with Text",
		zmk:   `[[Home|https://zettelstore.de]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"external","s":"https://zettelstore.de","i":[{"":"Text","s":"Home"}]}]`,
			encoderHTML:  `<a class="external" href="https://zettelstore.de">Home</a>`,
			encoderHTML:  `<a href="https://zettelstore.de" rel="external">Home</a>`,
			encoderMD:    "[Home](https://zettelstore.de)",
			encoderSexpr: `((LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSz:    `(INLINE (LINK-EXTERNAL () "https://zettelstore.de" (TEXT "Home")))`,
			encoderSHTML: `((a (@ (href . "https://zettelstore.de") (rel . "external")) "Home"))`,
			encoderText:  `Home`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID",
		zmk:   `[[00000000000100]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100"}]`,
			encoderHTML:  `<a href="00000000000100">00000000000100</a>`,
			encoderMD:    "[00000000000100](00000000000100)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100"))`,
			encoderSz:    `(INLINE (LINK-ZETTEL () "00000000000100"))`,
			encoderSHTML: `((a (@ (href . "00000000000100")) "00000000000100"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text",
		zmk:   `[[Config|00000000000100]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:  `<a href="00000000000100">Config</a>`,
			encoderMD:    "[Config](00000000000100)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderSz:    `(INLINE (LINK-ZETTEL () "00000000000100" (TEXT "Config")))`,
			encoderSHTML: `((a (@ (href . "00000000000100")) "Config"))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Simple Zettel ID with fragment",
		zmk:   `[[00000000000100#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag"}]`,
			encoderHTML:  `<a href="00000000000100#frag">00000000000100#frag</a>`,
			encoderMD:    "[00000000000100#frag](00000000000100#frag)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag"))`,
			encoderSz:    `(INLINE (LINK-ZETTEL () "00000000000100#frag"))`,
			encoderSHTML: `((a (@ (href . "00000000000100#frag")) "00000000000100#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Zettel ID with Text and fragment",
		zmk:   `[[Config|00000000000100#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"zettel","s":"00000000000100#frag","i":[{"":"Text","s":"Config"}]}]`,
			encoderHTML:  `<a href="00000000000100#frag">Config</a>`,
			encoderMD:    "[Config](00000000000100#frag)",
			encoderSexpr: `((LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderSz:    `(INLINE (LINK-ZETTEL () "00000000000100#frag" (TEXT "Config")))`,
			encoderSHTML: `((a (@ (href . "00000000000100#frag")) "Config"))`,
			encoderText:  `Config`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Fragment link to self",
		zmk:   `[[#frag]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"self","s":"#frag"}]`,
			encoderHTML:  `<a href="#frag">#frag</a>`,
			encoderMD:    "[#frag](#frag)",
			encoderSexpr: `((LINK-SELF () "#frag"))`,
			encoderSz:    `(INLINE (LINK-SELF () "#frag"))`,
			encoderSHTML: `((a (@ (href . "#frag")) "#frag"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Hosted link",
		zmk:   `[[H|/hosted]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"/hosted","i":[{"":"Text","s":"H"}]}]`,
			encoderHTML:  `<a href="/hosted">H</a>`,
			encoderMD:    "[H](/hosted)",
			encoderSexpr: `((LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSz:    `(INLINE (LINK-HOSTED () "/hosted" (TEXT "H")))`,
			encoderSHTML: `((a (@ (href . "/hosted")) "H"))`,
			encoderText:  `H`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Based link",
		zmk:   `[[B|/based]]`,
		zmk:   `[[B|//based]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"/based","i":[{"":"Text","s":"B"}]}]`,
			encoderHTML:  `<a href="/based">B</a>`,
			encoderMD:    "[B](/based)",
			encoderSexpr: `((LINK-HOSTED () "/based" (TEXT "B")))`,
			encoderSz:    `(INLINE (LINK-BASED () "/based" (TEXT "B")))`,
			encoderText:  `B`,
			encoderSHTML: `((a (@ (href . "/based")) "B"))`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Relative link",
		zmk:   `[[R|../relative]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"local","s":"../relative","i":[{"":"Text","s":"R"}]}]`,
			encoderHTML:  `<a href="../relative">R</a>`,
			encoderMD:    "[R](../relative)",
			encoderSexpr: `((LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderSz:    `(INLINE (LINK-HOSTED () "../relative" (TEXT "R")))`,
			encoderSHTML: `((a (@ (href . "../relative")) "R"))`,
			encoderText:  `R`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link w/o text",
		zmk:   `[[query:title:syntax]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax"}]`,
			encoderHTML:  `<a href="?q=title%3Asyntax">title:syntax</a>`,
			encoderMD:    "",
			encoderSexpr: `((LINK-QUERY () "title:syntax"))`,
			encoderSz:    `(INLINE (LINK-QUERY () "title:syntax"))`,
			encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "title:syntax"))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Query link with text",
		zmk:   `[[Q|query:title:syntax]]`,
		expect: expectMap{
			encoderZJSON: `[{"":"Link","q":"query","s":"title:syntax","i":[{"":"Text","s":"Q"}]}]`,
			encoderHTML:  `<a href="?q=title%3Asyntax">Q</a>`,
			encoderMD:    "Q",
			encoderSexpr: `((LINK-QUERY () "title:syntax" (TEXT "Q")))`,
			encoderSz:    `(INLINE (LINK-QUERY () "title:syntax" (TEXT "Q")))`,
			encoderSHTML: `((a (@ (href . "?q=title%3Asyntax")) "Q"))`,
			encoderText:  `Q`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Dummy Embed",
		zmk:   `{{abc}}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Embed","s":"abc"}]`,
			encoderHTML:  `<img alt="alternate description missing" src="abc">`,
			encoderHTML:  `<img src="abc">`,
			encoderMD:    "![abc](abc)",
			encoderSexpr: `((EMBED () (EXTERNAL "abc") ""))`,
			encoderSz:    `(INLINE (EMBED () (EXTERNAL "abc") ""))`,
			encoderSHTML: `((img (@ (src . "abc"))))`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "Inline HTML Zettel",
		zmk:   `@@<hr>@@{="html"}`,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   ``,
		},
	},
	{
		descr: "Inline Text Zettel",
		zmk:   `@@<hr>@@{="text"}`,
		expect: expectMap{
			encoderZJSON: `[{"":"Zettel","a":{"":"text"},"s":"<hr>"}]`,
			encoderHTML:  ``,
			encoderMD:    "<hr>",
			encoderSexpr: `((LITERAL-ZETTEL (("" "text")) "<hr>"))`,
			encoderSz:    `(INLINE (LITERAL-ZETTEL (("" . "text")) "<hr>"))`,
			encoderSHTML: `(())`,
			encoderText:  `<hr>`,
			encoderZmk:   useZmk,
		},
	},
	{
		descr: "",
		zmk:   ``,
		expect: expectMap{
			encoderZJSON: `[]`,
			encoderHTML:  ``,
			encoderMD:    "",
			encoderSz:    `(INLINE)`,
			encoderSexpr: `()`,
			encoderSHTML: `()`,
			encoderText:  ``,
			encoderZmk:   useZmk,
		},
	},
}

Changes to encoder/encoder_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

17
18
19
20



21
22
23
24
25

26


27
28
29
30
31
32

33
34

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

51
52

53
54
55
56
57
58
59
60
61

-
+






+
+
+





-

+


-
-
-
+
+
+


-

-
-
+
+



+
-
+

-
















-


-
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 encoder_test

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/sexpr"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"

	_ "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/sexprenc" // Allow to use sexpr 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/zjsonenc" // Allow to use ZJSON encoder.
	_ "zettelstore.de/z/encoder/zmkenc"   // Allow to use zmk encoder.
	"zettelstore.de/z/parser/cleaner"
	_ "zettelstore.de/z/parser/zettelmark" // Allow to use zettelmark parser.
)

type zmkTestCase struct {
	descr  string
	zmk    string
	inline bool
	expect expectMap
}

type expectMap map[api.EncodingEnum]string

const useZmk = "\000"
const (
	encoderZJSON = api.EncoderZJSON
	encoderHTML  = api.EncoderHTML
	encoderMD    = api.EncoderMD
	encoderSexpr = api.EncoderSexpr
	encoderSz    = api.EncoderSz
	encoderSHTML = api.EncoderSHTML
	encoderText  = api.EncoderText
	encoderZmk   = api.EncoderZmk
)

func TestEncoder(t *testing.T) {
	for i := range tcsInline {
		tcsInline[i].inline = true
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
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







-
+





-
+


+
+
+
+
+
-
+
















-
+

-
+





-
+




-
+
-
-
-
-




















-
-
+
+


-
+









-
-
+
+


-
+



			is := parser.ParseInlines(inp, meta.SyntaxZmk)
			cleaner.CleanInlineSlice(&is)
			pe = &peInlines{is: is}
		} else {
			pe = &peBlocks{bs: parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)}
		}
		checkEncodings(t, testNum, pe, tc.descr, tc.expect, tc.zmk)
		checkSexpr(t, testNum, pe, tc.descr)
		checkSz(t, testNum, pe, tc.descr)
	}
}

func checkEncodings(t *testing.T, testNum int, pe parserEncoder, descr string, expected expectMap, zmkDefault string) {
	for enc, exp := range expected {
		encdr := encoder.Create(enc)
		encdr := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		got, err := pe.encode(encdr)
		if err != nil {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
			t.Error("pe.encode:", err)
			t.Errorf("%s\nEncoder:  %s\nError:    %v", prefix, enc, err)
			continue
		}
		if enc == api.EncoderZmk && exp == useZmk {
			exp = zmkDefault
		}
		if got != exp {
			prefix := fmt.Sprintf("Test #%d", testNum)
			if d := descr; d != "" {
				prefix += "\nReason:   " + d
			}
			prefix += "\nMode:     " + pe.mode()
			t.Errorf("%s\nEncoder:  %s\nExpected: %q\nGot:      %q", prefix, enc, exp, got)
		}
	}
}

func checkSexpr(t *testing.T, testNum int, pe parserEncoder, descr string) {
func checkSz(t *testing.T, testNum int, pe parserEncoder, descr string) {
	t.Helper()
	encdr := encoder.Create(encoderSexpr)
	encdr := encoder.Create(encoderSz, nil)
	exp, err := pe.encode(encdr)
	if err != nil {
		t.Error(err)
		return
	}
	val, err := sxpf.ParseString(sexpr.Smk, exp)
	val, err := sxreader.MakeReader(strings.NewReader(exp)).Read()
	if err != nil {
		t.Error(err)
		return
	}
	got, err := sxpf.Repr(val)
	got := val.String()
	if err != nil {
		t.Error(err)
		return
	}
	if exp != got {
		prefix := fmt.Sprintf("Test #%d", testNum)
		if d := descr; d != "" {
			prefix += "\nReason:   " + d
		}
		prefix += "\nMode:     " + pe.mode()
		t.Errorf("%s\n\nExpected: %q\nGot:      %q", prefix, exp, got)
	}
}

type parserEncoder interface {
	encode(encoder.Encoder) (string, error)
	mode() string
}

type peInlines struct {
	is ast.InlineSlice
}

func (in peInlines) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteInlines(&buf, &in.is); err != nil {
	var sb strings.Builder
	if _, err := encdr.WriteInlines(&sb, &in.is); err != nil {
		return "", err
	}
	return buf.String(), nil
	return sb.String(), nil
}

func (peInlines) mode() string { return "inline" }

type peBlocks struct {
	bs ast.BlockSlice
}

func (bl peBlocks) encode(encdr encoder.Encoder) (string, error) {
	var buf bytes.Buffer
	if _, err := encdr.WriteBlocks(&buf, &bl.bs); err != nil {
	var sb strings.Builder
	if _, err := encdr.WriteBlocks(&sb, &bl.bs); err != nil {
		return "", err
	}
	return buf.String(), nil
	return sb.String(), nil

}
func (peBlocks) mode() string { return "block" }

Changes to encoder/htmlenc/htmlenc.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
1

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

-
+






+
+
+









-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+



-
+
+
+
+



+
+
+
-
-
+
+
+
+
+
+
+
+
+

+
+
+



-
-
-
-


+
+
+
+
+
-
+
+
+


-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+

-
-
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+




+
-
-
-
-
+
+
+
+
+
+
+
+
+





-
-
-
+
+
+

+
-
-
+
+
+
+
+
+
+
+





-
-
+
+
-
-
-
+
-
-
+
+
-
-
+
-
-
-
+
+
-
-
-
-
+
-
-
-
-
-
-
-
+
+
-
-
-
-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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 htmlenc encodes the abstract syntax tree into HTML5 via zettelstore-client.
package htmlenc

import (
	"io"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/sexprenc"
	"zettelstore.de/z/encoder/textenc"
	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderHTML, func() encoder.Encoder { return Create() })
	encoder.Register(
		api.EncoderHTML,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
}

// Create an encoder.
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
func Create() *Encoder { return &mySHE }

	return &Encoder{
		tx:      szenc.NewTransformer(),
		th:      shtml.NewEvaluator(1),
		lang:    params.Lang,
		textEnc: textenc.Create(),
	}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	tx      *szenc.Transformer
	th      *shtml.Evaluator
	lang    string
	textEnc *textenc.Encoder
}

var mySHE = Encoder{
	textEnc: textenc.Create(),
}

// WriteZettel encodes a full zettel as HTML5.
func (he *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hm, err := he.th.Evaluate(he.tx.GetMeta(zn.InhMeta, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	io.WriteString(w, "<html>\n<head>\n<meta charset=\"utf-8\">\n")

	var isTitle ast.InlineSlice
	var htitle *sx.Pair
	plainTitle, hasTitle := zn.InhMeta.Get(api.KeyTitle)
	if hasTitle {
		io.WriteString(w, "<title>")
		is := evalMeta(plainTitle)
		he.textEnc.WriteInlines(w, &is)
		io.WriteString(w, "</title>\n")
		isTitle = parser.ParseSpacedText(plainTitle)
		xtitle := he.tx.GetSz(&isTitle)
		htitle, err = he.th.Evaluate(xtitle, &env)
		if err != nil {
			return 0, err
		}
	}

	xast := he.tx.GetSz(&zn.Ast)
	hast, err := he.th.Evaluate(xast, &env)
	if err != nil {
		return 0, err
	}
	hen := shtml.Endnotes(&env)

	var head sx.ListBuilder
	head.Add(shtml.SymHead)
	head.Add(sx.Nil().Cons(sx.Nil().Cons(sx.Cons(sx.MakeSymbol("charset"), sx.MakeString("utf-8"))).Cons(sxhtml.SymAttr)).Cons(shtml.SymMeta))
	head.ExtendBang(hm)
	var sb strings.Builder
	if hasTitle {
		he.textEnc.WriteInlines(&sb, &isTitle)
	} else {
		sb.Write(zn.Meta.Zid.Bytes())

	acceptMeta(w, he.textEnc, zn.InhMeta, evalMeta)
	io.WriteString(w, "</head>\n<body>\n")
	env := html.NewEncEnvironment(w, 1)
	}
	head.Add(sx.MakeList(shtml.SymAttrTitle, sx.MakeString(sb.String())))

	var body sx.ListBuilder
	body.Add(shtml.SymBody)
	if hasTitle {
		if isTitle := evalMeta(plainTitle); len(isTitle) > 0 {
			io.WriteString(w, "<h1>")
			if err := acceptInlines(env, &isTitle); err != nil {
				return 0, err
			}
		body.Add(htitle.Cons(shtml.SymH1))
	}
			io.WriteString(w, "</h1>\n")
		}
	}

	body.ExtendBang(hast)
	if hen != nil {
		body.Add(sx.Cons(shtml.SymHR, nil))
		body.Add(hen)
	}

	doc := sx.MakeList(
		sxhtml.SymDoctype,
		sx.MakeList(shtml.SymHtml, head.List(), body.List()),
	)
	err := acceptBlocks(env, &zn.Ast)
	if err == nil {
		// env.WriteEndnotes()
		io.WriteString(w, "</body>\n</html>")
	}
	return 0, err

	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteHTML(w, doc)
}

// WriteMeta encodes meta data as HTML5.
func (he *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	acceptMeta(w, he.textEnc, m, evalMeta)
	return 0, nil
}

	hm, err := he.th.Evaluate(he.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	gen := sxhtml.NewGenerator().SetNewline()
	return gen.WriteListHTML(w, hm)
}

// WriteContent encodes the zettel content.
func (he *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return he.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks encodes a block slice.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := html.NewEncEnvironment(w, 1)
	err := acceptBlocks(env, bs)
func (he *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	hobj, err := he.th.Evaluate(he.tx.GetSz(bs), &env)
	if err == nil {
		gen := sxhtml.NewGenerator()
		env.WriteEndnotes()
		err = env.GetError()
		length, err2 := gen.WriteListHTML(w, hobj)
		if err2 != nil {
			return length, err2
		}

		l, err2 := gen.WriteHTML(w, shtml.Endnotes(&env))
		length += l
		return length, err2
	}
	return 0, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := html.NewEncEnvironment(w, 1)
func (he *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := shtml.MakeEnvironment(he.lang)
	return 0, acceptInlines(env, is)
}

	hobj, err := he.th.Evaluate(he.tx.GetSz(is), &env)
func acceptMeta(w io.Writer, textEnc encoder.Encoder, m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
	if err == nil {
		gen := sxhtml.NewGenerator()
		io.WriteString(w, `<meta name="zs-`)
		io.WriteString(w, p.Key)
		length, err2 := gen.WriteListHTML(w, hobj)
		io.WriteString(w, `" content="`)
		is := evalMeta(p.Value)
		var sb strings.Builder
		if err2 != nil {
			return length, err2
		textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(w, sb.String())
		io.WriteString(w, "\">\n")
	}
		}
}

func acceptBlocks(env *html.EncEnvironment, bs *ast.BlockSlice) error {
	lst := sexprenc.GetSexpr(bs)
	sxpf.Eval(env, lst)
	return env.GetError()
}
		return length, nil
	}
func acceptInlines(env *html.EncEnvironment, is *ast.InlineSlice) error {
	lst := sexprenc.GetSexpr(is)
	sxpf.Eval(env, lst)
	return env.GetError()
	return 0, err
}

Changes to encoder/mdenc/mdenc.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
1

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

-
+






+
+
+








-
+
+
+

-
-
+
+



-
+
+
+
+



-
-
-
-
+
+
+
+
+
+
+
+
-


-
-
+
+












-
-
+
+



















+
-
-
+
+



-
-
+
+






-
-
+
+







-
-
-
+
+
+
+
+


-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 mdenc encodes the abstract syntax tree back into Markdown.
package mdenc

import (
	"io"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderMD, func() encoder.Encoder { return Create() })
	encoder.Register(
		api.EncoderMD,
		func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) },
	)
}

// Create an encoder.
func Create() *Encoder { return &myME }

type Encoder struct{}

func Create(params *encoder.CreateParameter) *Encoder {
	return &Encoder{lang: params.Lang}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	lang string
}
var myME Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
func (me *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(zn.InhMeta, evalMeta)
	if zn.InhMeta.YamlSep {
		v.b.WriteString("---\n")
	} else {
		v.b.WriteByte('\n')
	}
	ast.Walk(v, &zn.Ast)
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as markdown.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
func (me *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w, me.lang)
	v.acceptMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (v *visitor) acceptMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		v.b.WriteStrings(key, ": ")
		if meta.Type(key) == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
func (me *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return me.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
func (me *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w, me.lang)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w)
func (me *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newVisitor(w, me.lang)
	ast.Walk(v, is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an EncWriter.
type visitor struct {
	b          encoder.EncWriter
	listInfo   []int
	listPrefix string
	b            encoder.EncWriter
	listInfo     []int
	listPrefix   string
	langStack    shtml.LangStack
	quoteNesting uint
}

func newVisitor(w io.Writer) *visitor {
	return &visitor{b: encoder.NewEncWriter(w)}
func newVisitor(w io.Writer, lang string) *visitor {
	return &visitor{b: encoder.NewEncWriter(w), langStack: shtml.NewLangStack(lang)}
}

// pushAttribute adds the current attributes to the visitor.
func (v *visitor) pushAttributes(a attrs.Attributes) {
	if value, ok := a.Get("lang"); ok {
		v.langStack.Push(value)
	} else {
		v.langStack.Dup()
	}
}

// popAttributes removes the current attributes from the visitor.
func (v *visitor) popAttributes() { v.langStack.Pop() }

// getLanguage returns the current language,
func (v *visitor) getLanguage() string { return v.langStack.Top() }

func (v *visitor) getQuotes() (string, string, bool) {
	qi := shtml.GetQuoteInfo(v.getLanguage())
	leftQ, rightQ := qi.GetQuotes(v.quoteNesting)
	return leftQ, rightQ, qi.GetNBSp()
}

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
	case *ast.VerbatimNode:
113
114
115
116
117
118
119
120
121
122

123
124
125
126
127
128
129
148
149
150
151
152
153
154



155
156
157
158
159
160
161
162







-
-
-
+







	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		return nil // Should write no content
	case *ast.TableNode:
		return nil // Should write no content
	case *ast.TextNode:
		v.visitText(n)
	case *ast.SpaceNode:
		v.b.WriteString(n.Lexeme)
		v.b.WriteString(n.Text)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.FootnoteNode:
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
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







+
+
+







-
+








+
+
+







	}
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	if rn.Kind != ast.RegionQuote {
		return
	}
	v.pushAttributes(rn.Attrs)
	defer v.popAttributes()

	first := true
	for _, bn := range rn.Blocks {
		pn, ok := bn.(*ast.ParaNode)
		if !ok {
			continue
		}
		if !first {
			v.b.WriteString("\n\n")
			v.b.WriteString("\n>\n")
		}
		first = false
		v.b.WriteString("> ")
		ast.Walk(v, &pn.Inlines)
	}
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.pushAttributes(hn.Attrs)
	defer v.popAttributes()

	const headingSigns = "###### "
	v.b.WriteString(headingSigns[len(headingSigns)-hn.Level-1:])
	ast.Walk(v, &hn.Inlines)
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	switch ln.Kind {
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







-
-
-
-

















+
+
+




+
+
+







			ast.Walk(v, in)
		}
	}

	v.listPrefix = prefix
}

func (v *visitor) visitText(tn *ast.TextNode) {
	v.b.WriteString(tn.Text)
}

func (v *visitor) visitBreak(bn *ast.BreakNode) {
	if bn.Hard {
		v.b.WriteString("\\\n")
	} else {
		v.b.WriteByte('\n')
	}
	if l := len(v.listInfo); l > 0 {
		if v.listPrefix == "" {
			v.writeSpaces(4*l - 4 + v.listInfo[l-1])
		} else {
			v.writeSpaces(4*l - 4)
			v.b.WriteString(v.listPrefix)
		}
	}
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.pushAttributes(ln.Attrs)
	defer v.popAttributes()

	v.writeReference(ln.Ref, ln.Inlines)
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.pushAttributes(en.Attrs)
	defer v.popAttributes()

	v.b.WriteByte('!')
	v.writeReference(en.Ref, en.Inlines)
}

func (v *visitor) writeReference(ref *ast.Reference, is ast.InlineSlice) {
	if ref.State == ast.RefStateQuery {
		ast.Walk(v, &is)
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
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







+
+
+










+
+
-
+

-
+




+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+










-
+



	if ref.State != ast.RefStateExternal || ref.URL == nil {
		return false
	}
	return ref.URL.Scheme != ""
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	v.pushAttributes(fn.Attrs)
	defer v.popAttributes()

	switch fn.Kind {
	case ast.FormatEmph:
		v.b.WriteByte('*')
		ast.Walk(v, &fn.Inlines)
		v.b.WriteByte('*')
	case ast.FormatStrong:
		v.b.WriteString("__")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("__")
	case ast.FormatQuote:
		v.writeQuote(fn)
	case ast.FormatMark:
		v.b.WriteString("<q>")
		v.b.WriteString("<mark>")
		ast.Walk(v, &fn.Inlines)
		v.b.WriteString("</q>")
		v.b.WriteString("</mark>")
	default:
		ast.Walk(v, &fn.Inlines)
	}
}

func (v *visitor) writeQuote(fn *ast.FormatNode) {
	leftQ, rightQ, withNbsp := v.getQuotes()
	v.b.WriteString(leftQ)
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.quoteNesting++
	ast.Walk(v, &fn.Inlines)
	v.quoteNesting--
	if withNbsp {
		v.b.WriteString("&nbsp;")
	}
	v.b.WriteString(rightQ)
}

func (v *visitor) visitLiteral(ln *ast.LiteralNode) {
	switch ln.Kind {
	case ast.LiteralProg:
	case ast.LiteralProg, ast.LiteralInput, ast.LiteralOutput:
		v.b.WriteByte('`')
		v.b.Write(ln.Content)
		v.b.WriteByte('`')
	case ast.LiteralComment, ast.LiteralHTML: // ignore everything
	default:
		v.b.Write(ln.Content)
	}
}

func (v *visitor) writeSpaces(n int) {
	for i := 0; i < n; i++ {
	for range n {
		v.b.WriteByte(' ')
	}
}

Deleted encoder/sexprenc/sexprenc.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



























































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 sexprenc encodes the abstract syntax tree into a s-expr.
package sexprenc

import (
	"io"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

func init() {
	encoder.Register(api.EncoderSexpr, func() encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder { return &mySE }

type Encoder struct{}

var mySE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	content := GetSexpr(&zn.Ast)
	meta := GetMeta(zn.InhMeta, evalMeta)
	return io.WriteString(w, sxpf.NewPair(meta, sxpf.NewPair(content, nil)).String())
}

// WriteMeta encodes meta data as JSON.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	return io.WriteString(w, GetMeta(m, evalMeta).String())
}

func (se *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return se.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	return io.WriteString(w, GetSexpr(bs).String())
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	return io.WriteString(w, GetSexpr(is).String())
}

Deleted encoder/sexprenc/transform.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
























































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-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 sexprenc

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"log"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
)

// GetSexpr returns the given node as a s-expression.
func GetSexpr(node ast.Node) *sxpf.Pair {
	t := transformer{}
	return t.getSexpr(node)
}

type transformer struct {
	inVerse bool
}

func (t *transformer) getSexpr(node ast.Node) *sxpf.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockSlice(n)
	case *ast.InlineSlice:
		return t.getInlineSlice(*n)
	case *ast.ParaNode:
		return sxpf.NewPair(sexpr.SymPara, t.getInlineSlice(n.Inlines))
	case *ast.VerbatimNode:
		return sxpf.NewPairFromValues(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return sxpf.NewPair(
			sexpr.SymHeading,
			sxpf.NewPair(
				sxpf.NewInteger(int64(n.Level)),
				sxpf.NewPair(
					getAttributes(n.Attrs),
					sxpf.NewPair(
						sxpf.NewString(n.Slug),
						sxpf.NewPair(
							sxpf.NewString(n.Fragment),
							t.getInlineSlice(n.Inlines),
						),
					),
				),
			),
		)
	case *ast.HRuleNode:
		return sxpf.NewPairFromValues(sexpr.SymThematic, getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sxpf.NewPairFromValues(sexpr.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sxpf.NewPairFromValues(sexpr.SymText, sxpf.NewString(n.Text))
	case *ast.SpaceNode:
		if t.inVerse {
			return sxpf.NewPairFromValues(sexpr.SymSpace, sxpf.NewString(n.Lexeme))
		}
		return sxpf.NewPairFromValues(sexpr.SymSpace)
	case *ast.BreakNode:
		if n.Hard {
			return sxpf.NewPairFromValues(sexpr.SymHard)
		} else {
			return sxpf.NewPairFromValues(sexpr.SymSoft)
		}
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return sxpf.NewPair(
			sexpr.SymEmbed,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				sxpf.NewPair(
					getReference(n.Ref),
					sxpf.NewPair(
						sxpf.NewString(n.Syntax),
						t.getInlineSlice(n.Inlines),
					),
				),
			),
		)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return sxpf.NewPair(
			sexpr.SymCite,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				sxpf.NewPair(
					sxpf.NewString(n.Key),
					t.getInlineSlice(n.Inlines),
				),
			),
		)
	case *ast.FootnoteNode:
		return sxpf.NewPair(
			sexpr.SymFootnote,
			sxpf.NewPair(
				getAttributes(n.Attrs),
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.MarkNode:
		return sxpf.NewPair(
			sexpr.SymMark,
			sxpf.NewPair(
				sxpf.NewString(n.Mark),
				sxpf.NewPair(
					sxpf.NewString(n.Slug),
					sxpf.NewPair(
						sxpf.NewString(n.Fragment),
						t.getInlineSlice(n.Inlines),
					),
				),
			),
		)
	case *ast.FormatNode:
		return sxpf.NewPair(
			mapGetS(mapFormatKindS, n.Kind),
			sxpf.NewPair(
				getAttributes(n.Attrs),
				t.getInlineSlice(n.Inlines),
			),
		)
	case *ast.LiteralNode:
		return sxpf.NewPairFromValues(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sxpf.NewString(string(n.Content)),
		)
	}
	log.Printf("SEXPR %T %v\n", node, node)
	return sxpf.NewPairFromValues(sexpr.SymUnknown, sxpf.NewString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sxpf.Symbol{
	ast.VerbatimZettel:  sexpr.SymVerbatimZettel,
	ast.VerbatimProg:    sexpr.SymVerbatimProg,
	ast.VerbatimEval:    sexpr.SymVerbatimEval,
	ast.VerbatimMath:    sexpr.SymVerbatimMath,
	ast.VerbatimComment: sexpr.SymVerbatimComment,
	ast.VerbatimHTML:    sexpr.SymVerbatimHTML,
}

var mapRegionKindS = map[ast.RegionKind]*sxpf.Symbol{
	ast.RegionSpan:  sexpr.SymRegionBlock,
	ast.RegionQuote: sexpr.SymRegionQuote,
	ast.RegionVerse: sexpr.SymRegionVerse,
}

func (t *transformer) getRegion(rn *ast.RegionNode) *sxpf.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
	symBlocks := t.getSexpr(&rn.Blocks)
	t.inVerse = saveInVerse
	return sxpf.NewPairFromValues(
		mapGetS(mapRegionKindS, rn.Kind),
		getAttributes(rn.Attrs),
		symBlocks,
		t.getSexpr(&rn.Inlines),
	)
}

var mapNestedListKindS = map[ast.NestedListKind]*sxpf.Symbol{
	ast.NestedListOrdered:   sexpr.SymListOrdered,
	ast.NestedListUnordered: sexpr.SymListUnordered,
	ast.NestedListQuote:     sexpr.SymListQuote,
}

func (t *transformer) getNestedList(ln *ast.NestedListNode) *sxpf.Pair {
	nlistVals := make([]sxpf.Value, len(ln.Items)+1)
	nlistVals[0] = mapGetS(mapNestedListKindS, ln.Kind)
	isCompact := isCompactList(ln.Items)
	for i, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.getSexpr(item[0])
			nlistVals[i+1] = paragraph.GetTail()
			continue
		}
		itemVals := make([]sxpf.Value, len(item))
		for j, in := range item {
			itemVals[j] = t.getSexpr(in)
		}
		nlistVals[i+1] = sxpf.NewPairFromValues(itemVals...)
	}
	return sxpf.NewPairFromValues(nlistVals...)
}
func isCompactList(itemSlice []ast.ItemSlice) bool {
	for _, items := range itemSlice {
		if len(items) > 1 {
			return false
		}
		if len(items) == 1 {
			if _, ok := items[0].(*ast.ParaNode); !ok {
				return false
			}
		}
	}
	return true
}

func (t *transformer) getDescriptionList(dn *ast.DescriptionListNode) *sxpf.Pair {
	dlVals := make([]sxpf.Value, 2*len(dn.Descriptions)+1)
	dlVals[0] = sexpr.SymDescription
	for i, def := range dn.Descriptions {
		dlVals[2*i+1] = t.getInlineSlice(def.Term)
		descVals := make([]sxpf.Value, len(def.Descriptions))
		for j, b := range def.Descriptions {
			if len(b) == 1 {
				descVals[j] = t.getSexpr(b[0]).GetTail()
				continue
			}
			dVal := make([]sxpf.Value, len(b))
			for k, dn := range b {
				dVal[k] = t.getSexpr(dn)
			}
			descVals[j] = sxpf.NewPairFromValues(dVal...)
		}
		dlVals[2*i+2] = sxpf.NewPairFromValues(descVals...)
	}
	return sxpf.NewPairFromValues(dlVals...)
}

func (t *transformer) getTable(tn *ast.TableNode) *sxpf.Pair {
	tVals := make([]sxpf.Value, len(tn.Rows)+2)
	tVals[0] = sexpr.SymTable
	tVals[1] = t.getRow(tn.Header)
	for i, row := range tn.Rows {
		tVals[i+2] = t.getRow(row)
	}
	return sxpf.NewPairFromValues(tVals...)
}
func (t *transformer) getRow(row ast.TableRow) *sxpf.Pair {
	rVals := make([]sxpf.Value, len(row))
	for i, cell := range row {
		rVals[i] = t.getCell(cell)
	}
	return sxpf.NewPairFromValues(rVals...)
}

var alignmentSymbolS = map[ast.Alignment]*sxpf.Symbol{
	ast.AlignDefault: sexpr.SymCell,
	ast.AlignLeft:    sexpr.SymCellLeft,
	ast.AlignCenter:  sexpr.SymCellCenter,
	ast.AlignRight:   sexpr.SymCellRight,
}

func (t *transformer) getCell(cell *ast.TableCell) *sxpf.Pair {
	return sxpf.NewPair(mapGetS(alignmentSymbolS, cell.Align), t.getInlineSlice(cell.Inlines))
}

func (t *transformer) getBLOB(bn *ast.BLOBNode) *sxpf.Pair {
	var lastValue sxpf.Value
	if bn.Syntax == meta.SyntaxSVG {
		lastValue = sxpf.NewString(string(bn.Blob))
	} else {
		lastValue = getBase64String(bn.Blob)
	}
	return sxpf.NewPairFromValues(
		sexpr.SymBLOB,
		t.getInlineSlice(bn.Description),
		sxpf.NewString(bn.Syntax),
		lastValue,
	)
}

var mapRefStateLink = map[ast.RefState]*sxpf.Symbol{
	ast.RefStateInvalid:  sexpr.SymLinkInvalid,
	ast.RefStateZettel:   sexpr.SymLinkZettel,
	ast.RefStateSelf:     sexpr.SymLinkSelf,
	ast.RefStateFound:    sexpr.SymLinkFound,
	ast.RefStateBroken:   sexpr.SymLinkBroken,
	ast.RefStateHosted:   sexpr.SymLinkHosted,
	ast.RefStateBased:    sexpr.SymLinkBased,
	ast.RefStateQuery:    sexpr.SymLinkQuery,
	ast.RefStateExternal: sexpr.SymLinkExternal,
}

func (t *transformer) getLink(ln *ast.LinkNode) *sxpf.Pair {
	return sxpf.NewPair(
		mapGetS(mapRefStateLink, ln.Ref.State),
		sxpf.NewPair(
			getAttributes(ln.Attrs),
			sxpf.NewPair(
				sxpf.NewString(ln.Ref.Value),
				t.getInlineSlice(ln.Inlines),
			),
		),
	)
}

func (t *transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sxpf.Pair {
	tail := t.getInlineSlice(en.Inlines)
	if en.Syntax == meta.SyntaxSVG {
		tail = sxpf.NewPair(sxpf.NewString(string(en.Blob)), tail)
	} else {
		tail = sxpf.NewPair(getBase64String(en.Blob), tail)
	}
	return sxpf.NewPair(
		sexpr.SymEmbedBLOB,
		sxpf.NewPair(
			getAttributes(en.Attrs),
			sxpf.NewPair(
				sxpf.NewString(en.Syntax),
				tail,
			),
		),
	)
}

var mapFormatKindS = map[ast.FormatKind]*sxpf.Symbol{
	ast.FormatEmph:   sexpr.SymFormatEmph,
	ast.FormatStrong: sexpr.SymFormatStrong,
	ast.FormatDelete: sexpr.SymFormatDelete,
	ast.FormatInsert: sexpr.SymFormatInsert,
	ast.FormatSuper:  sexpr.SymFormatSuper,
	ast.FormatSub:    sexpr.SymFormatSub,
	ast.FormatQuote:  sexpr.SymFormatQuote,
	ast.FormatSpan:   sexpr.SymFormatSpan,
}

var mapLiteralKindS = map[ast.LiteralKind]*sxpf.Symbol{
	ast.LiteralZettel:  sexpr.SymLiteralZettel,
	ast.LiteralProg:    sexpr.SymLiteralProg,
	ast.LiteralInput:   sexpr.SymLiteralInput,
	ast.LiteralOutput:  sexpr.SymLiteralOutput,
	ast.LiteralComment: sexpr.SymLiteralComment,
	ast.LiteralHTML:    sexpr.SymLiteralHTML,
	ast.LiteralMath:    sexpr.SymLiteralMath,
}

func (t *transformer) getBlockSlice(bs *ast.BlockSlice) *sxpf.Pair {
	lstVals := make([]sxpf.Value, len(*bs))
	for i, n := range *bs {
		lstVals[i] = t.getSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
}
func (t *transformer) getInlineSlice(is ast.InlineSlice) *sxpf.Pair {
	lstVals := make([]sxpf.Value, len(is))
	for i, n := range is {
		lstVals[i] = t.getSexpr(n)
	}
	return sxpf.NewPairFromSlice(lstVals)
}

func getAttributes(a attrs.Attributes) sxpf.Value {
	if a.IsEmpty() {
		return sxpf.Nil()
	}
	keys := a.Keys()
	lstVals := make([]sxpf.Value, 0, len(keys))
	for _, k := range keys {
		lstVals = append(lstVals, sxpf.NewPair(sxpf.NewString(k), sxpf.NewPair(sxpf.NewString(a[k]), nil)))
	}
	return sxpf.NewPairFromSlice(lstVals)
}

var mapRefStateS = map[ast.RefState]*sxpf.Symbol{
	ast.RefStateInvalid:  sexpr.SymRefStateInvalid,
	ast.RefStateZettel:   sexpr.SymRefStateZettel,
	ast.RefStateSelf:     sexpr.SymRefStateSelf,
	ast.RefStateFound:    sexpr.SymRefStateFound,
	ast.RefStateBroken:   sexpr.SymRefStateBroken,
	ast.RefStateHosted:   sexpr.SymRefStateHosted,
	ast.RefStateBased:    sexpr.SymRefStateBased,
	ast.RefStateQuery:    sexpr.SymRefStateQuery,
	ast.RefStateExternal: sexpr.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sxpf.Pair {
	return sxpf.NewPair(
		mapGetS(mapRefStateS, ref.State),
		sxpf.NewPair(
			sxpf.NewString(ref.Value),
			sxpf.Nil()))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sxpf.Symbol{
	meta.TypeCredential:   sexpr.SymTypeCredential,
	meta.TypeEmpty:        sexpr.SymTypeEmpty,
	meta.TypeID:           sexpr.SymTypeID,
	meta.TypeIDSet:        sexpr.SymTypeIDSet,
	meta.TypeNumber:       sexpr.SymTypeNumber,
	meta.TypeString:       sexpr.SymTypeString,
	meta.TypeTagSet:       sexpr.SymTypeTagSet,
	meta.TypeTimestamp:    sexpr.SymTypeTimestamp,
	meta.TypeURL:          sexpr.SymTypeURL,
	meta.TypeWord:         sexpr.SymTypeWord,
	meta.TypeWordSet:      sexpr.SymTypeWordSet,
	meta.TypeZettelmarkup: sexpr.SymTypeZettelmarkup,
}

func GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sxpf.Pair {
	pairs := m.ComputedPairs()
	lstVals := make([]sxpf.Value, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		strKey := sxpf.NewString(key)
		var val sxpf.Value
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setVals := make([]sxpf.Value, len(setList))
			for i, val := range setList {
				setVals[i] = sxpf.NewString(val)
			}
			val = sxpf.NewPairFromSlice(setVals)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			t := transformer{}
			val = t.getSexpr(&is)
		} else {
			val = sxpf.NewString(p.Value)
		}
		lstVals = append(lstVals, sxpf.NewPair(symType, sxpf.NewPair(strKey, sxpf.NewPair(val, nil))))
	}
	return sxpf.NewPairFromSlice(lstVals)
}

func mapGetS[T comparable](m map[T]*sxpf.Symbol, k T) *sxpf.Symbol {
	if result, found := m[k]; found {
		return result
	}
	log.Println("MISS", k, m)
	return sexpr.Smk.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
}

func getBase64String(data []byte) *sxpf.String {
	var buf bytes.Buffer
	encoder := base64.NewEncoder(base64.StdEncoding, &buf)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sxpf.NewString(buf.String())
	}
	return nil
}

Added encoder/shtmlenc/shtmlenc.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) 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 shtmlenc encodes the abstract syntax tree into a s-expr which represents HTML.
package shtmlenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSHTML, func(params *encoder.CreateParameter) encoder.Encoder { return Create(params) })
}

// Create a SHTML encoder
func Create(params *encoder.CreateParameter) *Encoder {
	// We need a new transformer every time, because tx.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{
		tx:   szenc.NewTransformer(),
		th:   shtml.NewEvaluator(1),
		lang: params.Lang,
	}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	tx   *szenc.Transformer
	th   *shtml.Evaluator
	lang string
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(zn.InhMeta, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	contentSHTML, err := enc.th.Evaluate(enc.tx.GetSz(&zn.Ast), &env)
	if err != nil {
		return 0, err
	}
	result := sx.Cons(metaSHTML, contentSHTML)
	return result.Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	metaSHTML, err := enc.th.Evaluate(enc.tx.GetMeta(m, evalMeta), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, metaSHTML)
}

// WriteContent encodes the zettel content.
func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(bs), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, hval)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	env := shtml.MakeEnvironment(enc.lang)
	hval, err := enc.th.Evaluate(enc.tx.GetSz(is), &env)
	if err != nil {
		return 0, err
	}
	return sx.Print(w, hval)
}

Added encoder/szenc/szenc.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 szenc encodes the abstract syntax tree into a s-expr for zettel.
package szenc

import (
	"io"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderSz, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create a S-expr encoder
func Create() *Encoder {
	// We need a new transformer every time, because trans.inVerse must be unique.
	// If we can refactor it out, the transformer can be created only once.
	return &Encoder{trans: NewTransformer()}
}

// Encoder contains all data needed for encoding.
type Encoder struct {
	trans *Transformer
}

// WriteZettel writes the encoded zettel to the writer.
func (enc *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	content := enc.trans.GetSz(&zn.Ast)
	meta := enc.trans.GetMeta(zn.InhMeta, evalMeta)
	return sx.MakeList(meta, content).Print(w)
}

// WriteMeta encodes meta data as s-expression.
func (enc *Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	return enc.trans.GetMeta(m, evalMeta).Print(w)
}

// WriteContent encodes the zettel content.
func (enc *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return enc.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (enc *Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	return enc.trans.GetSz(bs).Print(w)
}

// WriteInlines writes an inline slice to the writer
func (enc *Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	return enc.trans.GetSz(is).Print(w)
}

Added encoder/szenc/transform.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 szenc

import (
	"encoding/base64"
	"fmt"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

// NewTransformer returns a new transformer to create s-expressions from AST nodes.
func NewTransformer() *Transformer {
	t := Transformer{}
	return &t
}

// Transformer contains all data needed to transform into a s-expression.
type Transformer struct {
	inVerse bool
}

// GetSz transforms the given node into a sx list.
func (t *Transformer) GetSz(node ast.Node) *sx.Pair {
	switch n := node.(type) {
	case *ast.BlockSlice:
		return t.getBlockList(n).Cons(sz.SymBlock)
	case *ast.InlineSlice:
		return t.getInlineList(*n).Cons(sz.SymInline)
	case *ast.ParaNode:
		return t.getInlineList(n.Inlines).Cons(sz.SymPara)
	case *ast.VerbatimNode:
		return sx.MakeList(
			mapGetS(mapVerbatimKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	case *ast.RegionNode:
		return t.getRegion(n)
	case *ast.HeadingNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Fragment)).
			Cons(sx.MakeString(n.Slug)).
			Cons(getAttributes(n.Attrs)).
			Cons(sx.Int64(int64(n.Level))).
			Cons(sz.SymHeading)
	case *ast.HRuleNode:
		return sx.MakeList(sz.SymThematic, getAttributes(n.Attrs))
	case *ast.NestedListNode:
		return t.getNestedList(n)
	case *ast.DescriptionListNode:
		return t.getDescriptionList(n)
	case *ast.TableNode:
		return t.getTable(n)
	case *ast.TranscludeNode:
		return sx.MakeList(sz.SymTransclude, getAttributes(n.Attrs), getReference(n.Ref))
	case *ast.BLOBNode:
		return t.getBLOB(n)
	case *ast.TextNode:
		return sx.MakeList(sz.SymText, sx.MakeString(n.Text))
	case *ast.BreakNode:
		if n.Hard {
			return sx.MakeList(sz.SymHard)
		}
		return sx.MakeList(sz.SymSoft)
	case *ast.LinkNode:
		return t.getLink(n)
	case *ast.EmbedRefNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Syntax)).
			Cons(getReference(n.Ref)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymEmbed)
	case *ast.EmbedBLOBNode:
		return t.getEmbedBLOB(n)
	case *ast.CiteNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Key)).
			Cons(getAttributes(n.Attrs)).
			Cons(sz.SymCite)
	case *ast.FootnoteNode:
		// (ENDNODE attrs InlineElement ...)
		return t.getInlineList(n.Inlines).Cons(getAttributes(n.Attrs)).Cons(sz.SymEndnote)
	case *ast.MarkNode:
		return t.getInlineList(n.Inlines).
			Cons(sx.MakeString(n.Fragment)).
			Cons(sx.MakeString(n.Slug)).
			Cons(sx.MakeString(n.Mark)).
			Cons(sz.SymMark)
	case *ast.FormatNode:
		return t.getInlineList(n.Inlines).
			Cons(getAttributes(n.Attrs)).
			Cons(mapGetS(mapFormatKindS, n.Kind))
	case *ast.LiteralNode:
		return sx.MakeList(
			mapGetS(mapLiteralKindS, n.Kind),
			getAttributes(n.Attrs),
			sx.MakeString(string(n.Content)),
		)
	}
	return sx.MakeList(sz.SymUnknown, sx.MakeString(fmt.Sprintf("%T %v", node, node)))
}

var mapVerbatimKindS = map[ast.VerbatimKind]*sx.Symbol{
	ast.VerbatimZettel:  sz.SymVerbatimZettel,
	ast.VerbatimProg:    sz.SymVerbatimProg,
	ast.VerbatimEval:    sz.SymVerbatimEval,
	ast.VerbatimMath:    sz.SymVerbatimMath,
	ast.VerbatimComment: sz.SymVerbatimComment,
	ast.VerbatimHTML:    sz.SymVerbatimHTML,
}

var mapFormatKindS = map[ast.FormatKind]*sx.Symbol{
	ast.FormatEmph:   sz.SymFormatEmph,
	ast.FormatStrong: sz.SymFormatStrong,
	ast.FormatDelete: sz.SymFormatDelete,
	ast.FormatInsert: sz.SymFormatInsert,
	ast.FormatSuper:  sz.SymFormatSuper,
	ast.FormatSub:    sz.SymFormatSub,
	ast.FormatQuote:  sz.SymFormatQuote,
	ast.FormatMark:   sz.SymFormatMark,
	ast.FormatSpan:   sz.SymFormatSpan,
}

var mapLiteralKindS = map[ast.LiteralKind]*sx.Symbol{
	ast.LiteralZettel:  sz.SymLiteralZettel,
	ast.LiteralProg:    sz.SymLiteralProg,
	ast.LiteralInput:   sz.SymLiteralInput,
	ast.LiteralOutput:  sz.SymLiteralOutput,
	ast.LiteralComment: sz.SymLiteralComment,
	ast.LiteralHTML:    sz.SymLiteralHTML,
	ast.LiteralMath:    sz.SymLiteralMath,
}

var mapRegionKindS = map[ast.RegionKind]*sx.Symbol{
	ast.RegionSpan:  sz.SymRegionBlock,
	ast.RegionQuote: sz.SymRegionQuote,
	ast.RegionVerse: sz.SymRegionVerse,
}

func (t *Transformer) getRegion(rn *ast.RegionNode) *sx.Pair {
	saveInVerse := t.inVerse
	if rn.Kind == ast.RegionVerse {
		t.inVerse = true
	}
	symBlocks := t.getBlockList(&rn.Blocks)
	t.inVerse = saveInVerse
	return t.getInlineList(rn.Inlines).
		Cons(symBlocks).
		Cons(getAttributes(rn.Attrs)).
		Cons(mapGetS(mapRegionKindS, rn.Kind))
}

var mapNestedListKindS = map[ast.NestedListKind]*sx.Symbol{
	ast.NestedListOrdered:   sz.SymListOrdered,
	ast.NestedListUnordered: sz.SymListUnordered,
	ast.NestedListQuote:     sz.SymListQuote,
}

func (t *Transformer) getNestedList(ln *ast.NestedListNode) *sx.Pair {
	nlistObjs := make(sx.Vector, len(ln.Items)+1)
	nlistObjs[0] = mapGetS(mapNestedListKindS, ln.Kind)
	isCompact := isCompactList(ln.Items)
	for i, item := range ln.Items {
		if isCompact && len(item) > 0 {
			paragraph := t.GetSz(item[0])
			nlistObjs[i+1] = paragraph.Tail().Cons(sz.SymInline)
			continue
		}
		itemObjs := make(sx.Vector, len(item))
		for j, in := range item {
			itemObjs[j] = t.GetSz(in)
		}
		if isCompact {
			nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymInline)
		} else {
			nlistObjs[i+1] = sx.MakeList(itemObjs...).Cons(sz.SymBlock)
		}
	}
	return sx.MakeList(nlistObjs...)
}
func isCompactList(itemSlice []ast.ItemSlice) bool {
	for _, items := range itemSlice {
		if len(items) > 1 {
			return false
		}
		if len(items) == 1 {
			if _, ok := items[0].(*ast.ParaNode); !ok {
				return false
			}
		}
	}
	return true
}

func (t *Transformer) getDescriptionList(dn *ast.DescriptionListNode) *sx.Pair {
	dlObjs := make(sx.Vector, 2*len(dn.Descriptions)+1)
	dlObjs[0] = sz.SymDescription
	for i, def := range dn.Descriptions {
		dlObjs[2*i+1] = t.getInlineList(def.Term)
		descObjs := make(sx.Vector, len(def.Descriptions))
		for j, b := range def.Descriptions {
			dVal := make(sx.Vector, len(b))
			for k, dn := range b {
				dVal[k] = t.GetSz(dn)
			}
			descObjs[j] = sx.MakeList(dVal...).Cons(sz.SymBlock)
		}
		dlObjs[2*i+2] = sx.MakeList(descObjs...).Cons(sz.SymBlock)
	}
	return sx.MakeList(dlObjs...)
}

func (t *Transformer) getTable(tn *ast.TableNode) *sx.Pair {
	tObjs := make(sx.Vector, len(tn.Rows)+2)
	tObjs[0] = sz.SymTable
	tObjs[1] = t.getHeader(tn.Header)
	for i, row := range tn.Rows {
		tObjs[i+2] = t.getRow(row)
	}
	return sx.MakeList(tObjs...)
}
func (t *Transformer) getHeader(header ast.TableRow) *sx.Pair {
	if len(header) == 0 {
		return nil
	}
	return t.getRow(header)
}
func (t *Transformer) getRow(row ast.TableRow) *sx.Pair {
	rObjs := make(sx.Vector, len(row))
	for i, cell := range row {
		rObjs[i] = t.getCell(cell)
	}
	return sx.MakeList(rObjs...)
}

var alignmentSymbolS = map[ast.Alignment]*sx.Symbol{
	ast.AlignDefault: sz.SymCell,
	ast.AlignLeft:    sz.SymCellLeft,
	ast.AlignCenter:  sz.SymCellCenter,
	ast.AlignRight:   sz.SymCellRight,
}

func (t *Transformer) getCell(cell *ast.TableCell) *sx.Pair {
	return t.getInlineList(cell.Inlines).Cons(mapGetS(alignmentSymbolS, cell.Align))
}

func (t *Transformer) getBLOB(bn *ast.BLOBNode) *sx.Pair {
	var lastObj sx.Object
	if bn.Syntax == meta.SyntaxSVG {
		lastObj = sx.MakeString(string(bn.Blob))
	} else {
		lastObj = getBase64String(bn.Blob)
	}
	return sx.MakeList(
		sz.SymBLOB,
		t.getInlineList(bn.Description),
		sx.MakeString(bn.Syntax),
		lastObj,
	)
}

var mapRefStateLink = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymLinkInvalid,
	ast.RefStateZettel:   sz.SymLinkZettel,
	ast.RefStateSelf:     sz.SymLinkSelf,
	ast.RefStateFound:    sz.SymLinkFound,
	ast.RefStateBroken:   sz.SymLinkBroken,
	ast.RefStateHosted:   sz.SymLinkHosted,
	ast.RefStateBased:    sz.SymLinkBased,
	ast.RefStateQuery:    sz.SymLinkQuery,
	ast.RefStateExternal: sz.SymLinkExternal,
}

func (t *Transformer) getLink(ln *ast.LinkNode) *sx.Pair {
	return t.getInlineList(ln.Inlines).
		Cons(sx.MakeString(ln.Ref.Value)).
		Cons(getAttributes(ln.Attrs)).
		Cons(mapGetS(mapRefStateLink, ln.Ref.State))
}

func (t *Transformer) getEmbedBLOB(en *ast.EmbedBLOBNode) *sx.Pair {
	tail := t.getInlineList(en.Inlines)
	if en.Syntax == meta.SyntaxSVG {
		tail = tail.Cons(sx.MakeString(string(en.Blob)))
	} else {
		tail = tail.Cons(getBase64String(en.Blob))
	}
	return tail.Cons(sx.MakeString(en.Syntax)).Cons(getAttributes(en.Attrs)).Cons(sz.SymEmbedBLOB)
}

func (t *Transformer) getBlockList(bs *ast.BlockSlice) *sx.Pair {
	objs := make(sx.Vector, len(*bs))
	for i, n := range *bs {
		objs[i] = t.GetSz(n)
	}
	return sx.MakeList(objs...)
}
func (t *Transformer) getInlineList(is ast.InlineSlice) *sx.Pair {
	objs := make(sx.Vector, len(is))
	for i, n := range is {
		objs[i] = t.GetSz(n)
	}
	return sx.MakeList(objs...)
}

func getAttributes(a attrs.Attributes) sx.Object {
	if a.IsEmpty() {
		return sx.Nil()
	}
	keys := a.Keys()
	objs := make(sx.Vector, 0, len(keys))
	for _, k := range keys {
		objs = append(objs, sx.Cons(sx.MakeString(k), sx.MakeString(a[k])))
	}
	return sx.MakeList(objs...)
}

var mapRefStateS = map[ast.RefState]*sx.Symbol{
	ast.RefStateInvalid:  sz.SymRefStateInvalid,
	ast.RefStateZettel:   sz.SymRefStateZettel,
	ast.RefStateSelf:     sz.SymRefStateSelf,
	ast.RefStateFound:    sz.SymRefStateFound,
	ast.RefStateBroken:   sz.SymRefStateBroken,
	ast.RefStateHosted:   sz.SymRefStateHosted,
	ast.RefStateBased:    sz.SymRefStateBased,
	ast.RefStateQuery:    sz.SymRefStateQuery,
	ast.RefStateExternal: sz.SymRefStateExternal,
}

func getReference(ref *ast.Reference) *sx.Pair {
	return sx.MakeList(mapGetS(mapRefStateS, ref.State), sx.MakeString(ref.Value))
}

var mapMetaTypeS = map[*meta.DescriptionType]*sx.Symbol{
	meta.TypeCredential:   sz.SymTypeCredential,
	meta.TypeEmpty:        sz.SymTypeEmpty,
	meta.TypeID:           sz.SymTypeID,
	meta.TypeIDSet:        sz.SymTypeIDSet,
	meta.TypeNumber:       sz.SymTypeNumber,
	meta.TypeString:       sz.SymTypeString,
	meta.TypeTagSet:       sz.SymTypeTagSet,
	meta.TypeTimestamp:    sz.SymTypeTimestamp,
	meta.TypeURL:          sz.SymTypeURL,
	meta.TypeWord:         sz.SymTypeWord,
	meta.TypeZettelmarkup: sz.SymTypeZettelmarkup,
}

// GetMeta transforms the given metadata into a sx list.
func (t *Transformer) GetMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	pairs := m.ComputedPairs()
	objs := make(sx.Vector, 0, len(pairs))
	for _, p := range pairs {
		key := p.Key
		ty := m.Type(key)
		symType := mapGetS(mapMetaTypeS, ty)
		var obj sx.Object
		if ty.IsSet {
			setList := meta.ListFromValue(p.Value)
			setObjs := make(sx.Vector, len(setList))
			for i, val := range setList {
				setObjs[i] = sx.MakeString(val)
			}
			obj = sx.MakeList(setObjs...)
		} else if ty == meta.TypeZettelmarkup {
			is := evalMeta(p.Value)
			obj = t.getInlineList(is)
		} else {
			obj = sx.MakeString(p.Value)
		}
		objs = append(objs, sx.Nil().Cons(obj).Cons(sx.MakeSymbol(key)).Cons(symType))
	}
	return sx.MakeList(objs...).Cons(sz.SymMeta)
}

func mapGetS[T comparable](m map[T]*sx.Symbol, k T) *sx.Symbol {
	if result, found := m[k]; found {
		return result
	}
	return sx.MakeSymbol(fmt.Sprintf("**%v:NOT-FOUND**", k))
}

func getBase64String(data []byte) sx.String {
	var sb strings.Builder
	encoder := base64.NewEncoder(base64.StdEncoding, &sb)
	_, err := encoder.Write(data)
	if err == nil {
		err = encoder.Close()
	}
	if err == nil {
		return sx.MakeString(sb.String())
	}
	return sx.MakeString("")
}

Changes to encoder/textenc/textenc.go.

1
2

3
4
5
6
7
8



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

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

20
21
22


23
24
25
26
27

28
29
30
31
32
33
34
35
36
37
38
39
40
41

-
+






+
+
+








-
+
+

-
-
+
+



-
+





+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 textenc encodes the abstract syntax tree into its text.
package textenc

import (
	"io"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderText, func() encoder.Encoder { return Create() })
	encoder.Register(api.EncoderText, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myTE }

// Encoder contains all data needed for encoding.
type Encoder struct{}

var myTE Encoder // Only a singleton is required.

// WriteZettel writes metadata and content.
func (te *Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
65
66
67
68
69
70
71

72
73
74
75
76
77
78
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84







+







			buf.WriteByte(' ')
		}
		buf.WriteString(meta.CleanTag(tag))
	}

}

// WriteContent encodes the zettel content.
func (te *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return te.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
125
126
127
128
129
130
131
132

133
134
135
136
137
138
139
140
141
142
131
132
133
134
135
136
137

138



139
140
141
142
143
144
145







-
+
-
-
-







		return nil
	case *ast.TableNode:
		v.visitTable(n)
		return nil
	case *ast.TranscludeNode, *ast.BLOBNode:
		return nil
	case *ast.TextNode:
		v.b.WriteString(n.Text)
		v.visitText(n.Text)
		return nil
	case *ast.SpaceNode:
		v.b.WriteByte(' ')
		return nil
	case *ast.BreakNode:
		if n.Hard {
			v.b.WriteByte('\n')
		} else {
			v.b.WriteByte(' ')
		}
223
224
225
226
227
228
229















230
231
232
233
234
235
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







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






func (v *visitor) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		v.inlinePos = i
		ast.Walk(v, in)
	}
	v.inlinePos = 0
}

func (v *visitor) visitText(s string) {
	spaceFound := false
	for _, ch := range s {
		if input.IsSpace(ch) {
			if !spaceFound {
				v.b.WriteByte(' ')
				spaceFound = true
			}
			continue
		}
		spaceFound = false
		v.b.WriteString(string(ch))
	}
}

func (v *visitor) writePosChar(pos int, ch byte) {
	if pos > 0 {
		v.b.WriteByte(ch)
	}
}

Changes to encoder/write.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 encoder

import (
	"encoding/base64"
	"io"

Deleted encoder/zjsonenc/zjsonenc.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




















































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 zjsonenc encodes the abstract syntax tree into JSON.
//
// Deprecated in v0.11
package zjsonenc

import (
	"fmt"
	"io"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"zettelstore.de/c/zjson"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/strfun"
)

func init() {
	encoder.Register(api.EncoderZJSON, func() encoder.Encoder { return Create() })
}

// Create a ZJSON encoder
func Create() *Encoder { return &myJE }

type Encoder struct{}

var myJE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w)
	v.b.WriteString(`{"meta":`)
	v.writeMeta(zn.InhMeta, evalMeta)
	v.b.WriteString(`,"content":`)
	ast.Walk(v, &zn.Ast)
	v.b.WriteByte('}')
	length, err := v.b.Flush()
	return length, err
}

// WriteMeta encodes meta data as JSON.
func (*Encoder) WriteMeta(w io.Writer, m *meta.Meta, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newDetailVisitor(w)
	v.writeMeta(m, evalMeta)
	length, err := v.b.Flush()
	return length, err
}

func (je *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return je.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes a block slice to the writer
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newDetailVisitor(w)
	ast.Walk(v, bs)
	length, err := v.b.Flush()
	return length, err
}

// WriteInlines writes an inline slice to the writer
func (*Encoder) WriteInlines(w io.Writer, is *ast.InlineSlice) (int, error) {
	v := newDetailVisitor(w)
	ast.Walk(v, is)
	length, err := v.b.Flush()
	return length, err
}

// visitor writes the abstract syntax tree to an io.Writer.
type visitor struct {
	b       encoder.EncWriter
	inVerse bool // Visiting a verse block: save spaces in ZJSON object
}

func newDetailVisitor(w io.Writer) *visitor { return &visitor{b: encoder.NewEncWriter(w)} }

func (v *visitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.BlockSlice:
		v.visitBlockSlice(n)
		return nil
	case *ast.InlineSlice:
		v.walkInlineSlice(n)
		return nil
	case *ast.ParaNode:
		v.writeNodeStart(zjson.TypeParagraph)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.VerbatimNode:
		v.visitVerbatim(n)
	case *ast.RegionNode:
		v.visitRegion(n)
	case *ast.HeadingNode:
		v.visitHeading(n)
	case *ast.HRuleNode:
		v.writeNodeStart(zjson.TypeBreakThematic)
		v.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		v.visitNestedList(n)
	case *ast.DescriptionListNode:
		v.visitDescriptionList(n)
	case *ast.TableNode:
		v.visitTable(n)
	case *ast.TranscludeNode:
		v.writeNodeStart(zjson.TypeTransclude)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString2)
		writeEscaped(&v.b, mapRefState[n.Ref.State])
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Ref.String())
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.writeNodeStart(zjson.TypeText)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Text)
	case *ast.SpaceNode:
		v.writeNodeStart(zjson.TypeSpace)
		if v.inVerse {
			v.writeContentStart(zjson.NameString)
			writeEscaped(&v.b, n.Lexeme)
		}
	case *ast.BreakNode:
		if n.Hard {
			v.writeNodeStart(zjson.TypeBreakHard)
		} else {
			v.writeNodeStart(zjson.TypeBreakSoft)
		}
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
		v.visitEmbedBLOB(n)
	case *ast.CiteNode:
		v.writeNodeStart(zjson.TypeCitation)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, n.Key)
		if len(n.Inlines) > 0 {
			v.writeContentStart(zjson.NameInline)
			ast.Walk(v, &n.Inlines)
		}
	case *ast.FootnoteNode:
		v.writeNodeStart(zjson.TypeFootnote)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.MarkNode:
		v.visitMark(n)
	case *ast.FormatNode:
		v.writeNodeStart(mapFormatKind[n.Kind])
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &n.Inlines)
	case *ast.LiteralNode:
		kind, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown literal kind %v", n.Kind))
		}
		v.writeNodeStart(kind)
		v.visitAttributes(n.Attrs)
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, string(n.Content))
	default:
		return v
	}
	v.b.WriteByte('}')
	return nil
}

var mapVerbatimKind = map[ast.VerbatimKind]string{
	ast.VerbatimZettel:  zjson.TypeVerbatimZettel,
	ast.VerbatimProg:    zjson.TypeVerbatimCode,
	ast.VerbatimEval:    zjson.TypeVerbatimEval,
	ast.VerbatimMath:    zjson.TypeVerbatimMath,
	ast.VerbatimComment: zjson.TypeVerbatimComment,
	ast.VerbatimHTML:    zjson.TypeVerbatimHTML,
}

func (v *visitor) visitVerbatim(vn *ast.VerbatimNode) {
	kind, ok := mapVerbatimKind[vn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown verbatim kind %v", vn.Kind))
	}
	v.writeNodeStart(kind)
	v.visitAttributes(vn.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, string(vn.Content))
}

var mapRegionKind = map[ast.RegionKind]string{
	ast.RegionSpan:  zjson.TypeBlock,
	ast.RegionQuote: zjson.TypeExcerpt,
	ast.RegionVerse: zjson.TypePoem,
}

func (v *visitor) visitRegion(rn *ast.RegionNode) {
	kind, ok := mapRegionKind[rn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown region kind %v", rn.Kind))
	}
	saveInVerse := v.inVerse
	if rn.Kind == ast.RegionVerse {
		v.inVerse = true
	}
	v.writeNodeStart(kind)
	v.visitAttributes(rn.Attrs)
	v.writeContentStart(zjson.NameBlock)
	ast.Walk(v, &rn.Blocks)
	if len(rn.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &rn.Inlines)
	}
	v.inVerse = saveInVerse
}

func (v *visitor) visitHeading(hn *ast.HeadingNode) {
	v.writeNodeStart(zjson.TypeHeading)
	v.visitAttributes(hn.Attrs)
	v.writeContentStart(zjson.NameNumeric)
	v.b.WriteString(strconv.Itoa(hn.Level))
	if fragment := hn.Fragment; fragment != "" {
		v.writeContentStart(zjson.NameString)
		v.b.WriteStrings(`"`, fragment, `"`)
	}
	v.writeContentStart(zjson.NameInline)
	ast.Walk(v, &hn.Inlines)
}

var mapNestedListKind = map[ast.NestedListKind]string{
	ast.NestedListOrdered:   zjson.TypeListOrdered,
	ast.NestedListUnordered: zjson.TypeListBullet,
	ast.NestedListQuote:     zjson.TypeListQuotation,
}

func (v *visitor) visitNestedList(ln *ast.NestedListNode) {
	v.writeNodeStart(mapNestedListKind[ln.Kind])
	v.writeContentStart(zjson.NameList)
	for i, item := range ln.Items {
		v.writeComma(i)
		v.b.WriteByte('[')
		for j, in := range item {
			v.writeComma(j)
			ast.Walk(v, in)
		}
		v.b.WriteByte(']')
	}
	v.b.WriteByte(']')
}

func (v *visitor) visitDescriptionList(dn *ast.DescriptionListNode) {
	v.writeNodeStart(zjson.TypeDescrList)
	v.writeContentStart(zjson.NameDescrList)
	for i, def := range dn.Descriptions {
		v.writeComma(i)
		v.b.WriteStrings(`{"`, zjson.NameInline, `":`)
		ast.Walk(v, &def.Term)

		if len(def.Descriptions) > 0 {
			v.writeContentStart(zjson.NameDescription)
			for j, b := range def.Descriptions {
				v.writeComma(j)
				v.b.WriteByte('[')
				for k, dn := range b {
					v.writeComma(k)
					ast.Walk(v, dn)
				}
				v.b.WriteByte(']')
			}
			v.b.WriteByte(']')
		}
		v.b.WriteByte('}')
	}
	v.b.WriteByte(']')
}

func (v *visitor) visitTable(tn *ast.TableNode) {
	v.writeNodeStart(zjson.TypeTable)
	v.writeContentStart(zjson.NameTable)

	// Table header
	v.b.WriteByte('[')
	for i, cell := range tn.Header {
		v.writeComma(i)
		v.writeCell(cell)
	}
	v.b.WriteString("],")

	// Table rows
	v.b.WriteByte('[')
	for i, row := range tn.Rows {
		v.writeComma(i)
		v.b.WriteByte('[')
		for j, cell := range row {
			v.writeComma(j)
			v.writeCell(cell)
		}
		v.b.WriteByte(']')
	}
	v.b.WriteString("]]")
}

var alignmentCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
	ast.AlignCenter:  ":",
	ast.AlignRight:   ">",
}

func (v *visitor) writeCell(cell *ast.TableCell) {
	if aCode := alignmentCode[cell.Align]; aCode != "" {
		v.b.WriteStrings(`{"`, zjson.NameString, `":"`, aCode, `","`, zjson.NameInline, `":`)
	} else {
		v.b.WriteStrings(`{"`, zjson.NameInline, `":`)
	}
	ast.Walk(v, &cell.Inlines)
	v.b.WriteByte('}')
}

func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	v.writeNodeStart(zjson.TypeBLOB)
	if len(bn.Description) > 0 {
		v.writeContentStart(zjson.NameString2)
		ast.Walk(v, &bn.Description)
	}
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, bn.Syntax)
	if bn.Syntax == meta.SyntaxSVG {
		v.writeContentStart(zjson.NameString3)
		writeEscaped(&v.b, string(bn.Blob))
	} else {
		v.writeContentStart(zjson.NameBinary)
		v.b.WriteBase64(bn.Blob)
		v.b.WriteByte('"')
	}
}

var mapRefState = map[ast.RefState]string{
	ast.RefStateInvalid:  zjson.RefStateInvalid,
	ast.RefStateZettel:   zjson.RefStateZettel,
	ast.RefStateSelf:     zjson.RefStateSelf,
	ast.RefStateFound:    zjson.RefStateFound,
	ast.RefStateBroken:   zjson.RefStateBroken,
	ast.RefStateHosted:   zjson.RefStateHosted,
	ast.RefStateBased:    zjson.RefStateBased,
	ast.RefStateQuery:    zjson.RefStateQuery,
	ast.RefStateExternal: zjson.RefStateExternal,
}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.writeNodeStart(zjson.TypeLink)
	v.visitAttributes(ln.Attrs)
	v.writeContentStart(zjson.NameString2)
	writeEscaped(&v.b, mapRefState[ln.Ref.State])
	v.writeContentStart(zjson.NameString)
	if ln.Ref.State == ast.RefStateQuery {
		writeEscaped(&v.b, ln.Ref.Value)
	} else {
		writeEscaped(&v.b, ln.Ref.String())
	}
	if len(ln.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &ln.Inlines)
	}
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.writeNodeStart(zjson.TypeEmbed)
	v.visitAttributes(en.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, en.Ref.String())

	if len(en.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &en.Inlines)
	}
	if en.Syntax != "" {
		v.writeContentStart(zjson.NameString2)
		writeEscaped(&v.b, en.Syntax)
	}
}

func (v *visitor) visitEmbedBLOB(en *ast.EmbedBLOBNode) {
	v.writeNodeStart(zjson.TypeEmbedBLOB)
	v.visitAttributes(en.Attrs)
	v.writeContentStart(zjson.NameString)
	writeEscaped(&v.b, en.Syntax)
	if en.Syntax == meta.SyntaxSVG {
		v.writeContentStart(zjson.NameString3)
		writeEscaped(&v.b, string(en.Blob))
	} else {
		v.writeContentStart(zjson.NameBinary)
		v.b.WriteBase64(en.Blob)
		v.b.WriteByte('"')
	}
	if len(en.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &en.Inlines)
	}
}

func (v *visitor) visitMark(mn *ast.MarkNode) {
	v.writeNodeStart(zjson.TypeMark)
	if text := mn.Mark; text != "" {
		v.writeContentStart(zjson.NameString)
		writeEscaped(&v.b, text)
	}
	if fragment := mn.Fragment; fragment != "" {
		v.writeContentStart(zjson.NameString2)
		v.b.WriteByte('"')
		v.b.WriteString(fragment)
		v.b.WriteByte('"')
	}
	if len(mn.Inlines) > 0 {
		v.writeContentStart(zjson.NameInline)
		ast.Walk(v, &mn.Inlines)
	}
}

var mapFormatKind = map[ast.FormatKind]string{
	ast.FormatEmph:   zjson.TypeFormatEmph,
	ast.FormatStrong: zjson.TypeFormatStrong,
	ast.FormatDelete: zjson.TypeFormatDelete,
	ast.FormatInsert: zjson.TypeFormatInsert,
	ast.FormatSuper:  zjson.TypeFormatSuper,
	ast.FormatSub:    zjson.TypeFormatSub,
	ast.FormatQuote:  zjson.TypeFormatQuote,
	ast.FormatSpan:   zjson.TypeFormatSpan,
}

var mapLiteralKind = map[ast.LiteralKind]string{
	ast.LiteralZettel:  zjson.TypeLiteralZettel,
	ast.LiteralProg:    zjson.TypeLiteralCode,
	ast.LiteralInput:   zjson.TypeLiteralInput,
	ast.LiteralOutput:  zjson.TypeLiteralOutput,
	ast.LiteralComment: zjson.TypeLiteralComment,
	ast.LiteralHTML:    zjson.TypeLiteralHTML,
	ast.LiteralMath:    zjson.TypeLiteralMath,
}

func (v *visitor) visitBlockSlice(bs *ast.BlockSlice) {
	v.b.WriteByte('[')
	for i, bn := range *bs {
		v.writeComma(i)
		ast.Walk(v, bn)
	}
	v.b.WriteByte(']')
}

func (v *visitor) walkInlineSlice(is *ast.InlineSlice) {
	v.b.WriteByte('[')
	for i, in := range *is {
		v.writeComma(i)
		ast.Walk(v, in)
	}
	v.b.WriteByte(']')
}

// visitAttributes write JSON attributes
func (v *visitor) visitAttributes(a attrs.Attributes) {
	if a.IsEmpty() {
		return
	}

	v.writeContentStart(zjson.NameAttribute)
	for i, k := range a.Keys() {
		if i > 0 {
			v.b.WriteString(`","`)
		}
		strfun.JSONEscape(&v.b, k)
		v.b.WriteString(`":"`)
		strfun.JSONEscape(&v.b, a[k])
	}
	v.b.WriteString(`"}`)
}

func (v *visitor) writeNodeStart(t string) {
	v.b.WriteStrings(`{"":"`, t, `"`)
}

var valueStart = map[string]string{
	zjson.NameBlock:       "",
	zjson.NameAttribute:   `{"`,
	zjson.NameList:        "[",
	zjson.NameDescrList:   "[",
	zjson.NameDescription: "[",
	zjson.NameInline:      "",
	zjson.NameBLOB:        "{",
	zjson.NameNumeric:     "",
	zjson.NameBinary:      `"`,
	zjson.NameTable:       "[",
	zjson.NameString2:     "",
	zjson.NameString:      "",
	zjson.NameString3:     "",
}

func (v *visitor) writeContentStart(jsonName string) {
	s, ok := valueStart[jsonName]
	if !ok {
		panic("Unknown object name " + jsonName)
	}
	v.b.WriteStrings(`,"`, jsonName, `":`, s)
}

func (v *visitor) writeMeta(m *meta.Meta, evalMeta encoder.EvalMetaFunc) {
	v.b.WriteByte('{')
	for i, p := range m.ComputedPairs() {
		if i > 0 {
			v.b.WriteByte(',')
		}
		v.b.WriteByte('"')
		key := p.Key
		strfun.JSONEscape(&v.b, key)
		t := m.Type(key)
		v.b.WriteStrings(`":{"`, zjson.NameType, `":"`, t.Name, `","`)
		if t.IsSet {
			v.b.WriteStrings(zjson.NameSet, `":`)
			v.writeSetValue(p.Value)
		} else if t == meta.TypeZettelmarkup {
			v.b.WriteStrings(zjson.NameInline, `":`)
			is := evalMeta(p.Value)
			ast.Walk(v, &is)
		} else {
			v.b.WriteStrings(zjson.NameString, `":`)
			writeEscaped(&v.b, p.Value)
		}
		v.b.WriteByte('}')
	}
	v.b.WriteByte('}')
}

func (v *visitor) writeSetValue(value string) {
	v.b.WriteByte('[')
	for i, val := range meta.ListFromValue(value) {
		v.writeComma(i)
		writeEscaped(&v.b, val)
	}
	v.b.WriteByte(']')
}

func (v *visitor) writeComma(pos int) {
	if pos > 0 {
		v.b.WriteByte(',')
	}
}

func writeEscaped(b *encoder.EncWriter, s string) {
	b.WriteByte('"')
	strfun.JSONEscape(b, s)
	b.WriteByte('"')
}

Changes to encoder/zmkenc/zmkenc.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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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 zmkenc encodes the abstract syntax tree back into Zettelmarkup.
package zmkenc

import (
	"bytes"
	"fmt"
	"io"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	encoder.Register(api.EncoderZmk, func() encoder.Encoder { return Create() })
	encoder.Register(api.EncoderZmk, func(*encoder.CreateParameter) encoder.Encoder { return Create() })
}

// Create an encoder.
func Create() *Encoder { return &myZE }

// Encoder contains all data needed for encoding.
type Encoder struct{}

var myZE Encoder

// WriteZettel writes the encoded zettel to the writer.
func (*Encoder) WriteZettel(w io.Writer, zn *ast.ZettelNode, evalMeta encoder.EvalMetaFunc) (int, error) {
	v := newVisitor(w)
68
69
70
71
72
73
74

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







+







		} else {
			v.b.WriteString(p.Value)
		}
		v.b.WriteByte('\n')
	}
}

// WriteContent encodes the zettel content.
func (ze *Encoder) WriteContent(w io.Writer, zn *ast.ZettelNode) (int, error) {
	return ze.WriteBlocks(w, &zn.Ast)
}

// WriteBlocks writes the content of a block slice to the writer.
func (*Encoder) WriteBlocks(w io.Writer, bs *ast.BlockSlice) (int, error) {
	v := newVisitor(w)
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
138
139
140
141
142
143
144


145
146
147
148
149
150
151







-
-







	case *ast.TranscludeNode:
		v.b.WriteStrings("{{{", n.Ref.String(), "}}}")
		v.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		v.visitBLOB(n)
	case *ast.TextNode:
		v.visitText(n)
	case *ast.SpaceNode:
		v.b.WriteString(n.Lexeme)
	case *ast.BreakNode:
		v.visitBreak(n)
	case *ast.LinkNode:
		v.visitLink(n)
	case *ast.EmbedRefNode:
		v.visitEmbedRef(n)
	case *ast.EmbedBLOBNode:
285
286
287
288
289
290
291




292


293
294
295
296
297
298
299
288
289
290
291
292
293
294
295
296
297
298

299
300
301
302
303
304
305
306
307







+
+
+
+
-
+
+







			v.b.WriteByte('\n')
		}
		v.b.WriteString("; ")
		ast.Walk(v, &descr.Term)

		for _, b := range descr.Descriptions {
			v.b.WriteString("\n: ")
			for jj, dn := range b {
				if jj > 0 {
					v.b.WriteString("\n\n  ")
				}
			ast.WalkDescriptionSlice(v, b)
				ast.Walk(v, dn)
			}
		}
	}
}

var alignCode = map[ast.Alignment]string{
	ast.AlignDefault: "",
	ast.AlignLeft:    "<",
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
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







-
-
-
+
+
+



-
+















-
+







func (v *visitor) visitBLOB(bn *ast.BLOBNode) {
	if bn.Syntax == meta.SyntaxSVG {
		v.b.WriteStrings("@@@", bn.Syntax, "\n")
		v.b.Write(bn.Blob)
		v.b.WriteString("\n@@@\n")
		return
	}
	var buf bytes.Buffer
	v.textEnc.WriteInlines(&buf, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", buf.String(), "' and syntax '", bn.Syntax, "'.")
	var sb strings.Builder
	v.textEnc.WriteInlines(&sb, &bn.Description)
	v.b.WriteStrings("%% Unable to display BLOB with description '", sb.String(), "' and syntax '", bn.Syntax, "'.")
}

var escapeSeqs = strfun.NewSet(
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==",
	"\\", "__", "**", "~~", "^^", ",,", ">>", `""`, "::", "''", "``", "++", "==", "##",
)

func (v *visitor) visitText(tn *ast.TextNode) {
	last := 0
	for i := 0; i < len(tn.Text); i++ {
		if b := tn.Text[i]; b == '\\' {
			v.b.WriteString(tn.Text[last:i])
			v.b.WriteBytes('\\', b)
			last = i + 1
			continue
		}
		if i < len(tn.Text)-1 {
			s := tn.Text[i : i+2]
			if escapeSeqs.Has(s) {
				v.b.WriteString(tn.Text[last:i])
				for j := 0; j < len(s); j++ {
				for j := range len(s) {
					v.b.WriteBytes('\\', s[j])
				}
				i++
				last = i + 1
				continue
			}
		}
389
390
391
392
393
394
395



396
397
398
399
400
401
402
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413







+
+
+







}

func (v *visitor) visitLink(ln *ast.LinkNode) {
	v.b.WriteString("[[")
	if len(ln.Inlines) > 0 {
		ast.Walk(v, &ln.Inlines)
		v.b.WriteByte('|')
	}
	if ln.Ref.State == ast.RefStateBased {
		v.b.WriteByte('/')
	}
	v.b.WriteStrings(ln.Ref.String(), "]]")
}

func (v *visitor) visitEmbedRef(en *ast.EmbedRefNode) {
	v.b.WriteString("{{")
	if len(en.Inlines) > 0 {
415
416
417
418
419
420
421
422

423
424
425
426
427
428
429
426
427
428
429
430
431
432

433
434
435
436
437
438
439
440







-
+







	}
	v.b.WriteString("{{TODO: display inline BLOB}}")
}

func (v *visitor) visitCite(cn *ast.CiteNode) {
	v.b.WriteStrings("[@", cn.Key)
	if len(cn.Inlines) > 0 {
		v.b.WriteString(", ")
		v.b.WriteByte(' ')
		ast.Walk(v, &cn.Inlines)
	}
	v.b.WriteByte(']')
	v.visitAttributes(cn.Attrs)
}

func (v *visitor) visitMark(mn *ast.MarkNode) {
440
441
442
443
444
445
446

447
448
449
450
451
452
453
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465







+







	ast.FormatEmph:   []byte("__"),
	ast.FormatStrong: []byte("**"),
	ast.FormatInsert: []byte(">>"),
	ast.FormatDelete: []byte("~~"),
	ast.FormatSuper:  []byte("^^"),
	ast.FormatSub:    []byte(",,"),
	ast.FormatQuote:  []byte(`""`),
	ast.FormatMark:   []byte("##"),
	ast.FormatSpan:   []byte("::"),
}

func (v *visitor) visitFormat(fn *ast.FormatNode) {
	kind, ok := mapFormatKind[fn.Kind]
	if !ok {
		panic(fmt.Sprintf("Unknown format kind %d", fn.Kind))
514
515
516
517
518
519
520
521

522
523
524
525
526
527
528
529
530
531
532
533
526
527
528
529
530
531
532

533
534
535
536
537
538
539
540
541
542
543
544
545







-
+












		}
	}
	v.b.WriteByte('}')
}

func (v *visitor) writeEscaped(s string, toEscape byte) {
	last := 0
	for i := 0; i < len(s); i++ {
	for i := range len(s) {
		if b := s[i]; b == toEscape || b == '\\' {
			v.b.WriteString(s[last:i])
			v.b.WriteBytes('\\', b)
			last = i + 1
		}
	}
	v.b.WriteString(s[last:])
}

func syntaxToHTML(a attrs.Attributes) attrs.Attributes {
	return a.Clone().Set("", meta.SyntaxHTML).Remove(api.KeySyntax)
}

Deleted encoding/atom/atom.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




















































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2022-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 atom provides an Atom encoding.
package atom

import (
	"bytes"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
)

const ContentType = "application/atom+xml"

type Configuration struct {
	Title            string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

func (c *Configuration) Setup(cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)

	c.Title = cfg.GetSiteName()
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	atomUpdated := encoding.LastUpdated(ml, time.RFC3339)
	feedLink := c.NewURLBuilderAbs().String()

	var buf bytes.Buffer
	buf.WriteString(`<feed xmlns="http://www.w3.org/2005/Atom">` + "\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "id", feedLink)
	buf.WriteString(`  <link rel="self" href="`)
	if s := q.String(); s != "" {
		strfun.XMLEscape(&buf, c.NewURLBuilderAbs().AppendQuery(s).String())
	} else {
		strfun.XMLEscape(&buf, feedLink)
	}
	buf.WriteString(`"/>` + "\n")
	if atomUpdated != "" {
		xml.WriteTag(&buf, "  ", "updated", atomUpdated)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <author><name>Unknown</name></author>\n")

	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</feed>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	entryUpdated := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
			entryUpdated = published.UTC().Format(time.RFC3339)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <entry>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "id", link)
	buf.WriteString(`    <link rel="self" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")
	buf.WriteString(`    <link rel="alternate" type="text/html" href="`)
	strfun.XMLEscape(buf, link)
	buf.WriteString(`"/>` + "\n")

	if entryUpdated != "" {
		xml.WriteTag(buf, "    ", "updated", entryUpdated)
	}
	marshalTags(buf, m)
	buf.WriteString("  </entry>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				buf.WriteString(`    <category term="`)
				strfun.XMLEscape(buf, tag)
				buf.WriteString("\"/>\n")
			}
		}
	}
}

Deleted encoding/encoding.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) 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 encoding provides helper functions for encodings.
package encoding

import (
	"bytes"
	"time"

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

// LastUpdated returns the formated time of the zettel which was updated at the latest time.
func LastUpdated(ml []*meta.Meta, timeFormat string) string {
	maxPublished := time.Date(1, time.January, 1, 0, 0, 0, 0, time.Local)
	for _, m := range ml {
		if val, found := m.Get(api.KeyPublished); found {
			if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
				if maxPublished.Before(published) {
					maxPublished = published
				}
			}
		}
	}
	if maxPublished.Year() > 1 {
		return maxPublished.UTC().Format(timeFormat)
	}
	return ""
}

var textEnc = textenc.Create()

// TitleAsText returns the title of a zettel as plain text
func TitleAsText(m *meta.Meta) string {
	var title bytes.Buffer
	titleIns := parser.ParseMetadata(m.GetTitle())
	if _, err := textEnc.WriteInlines(&title, &titleIns); err != nil {
		return m.GetTitle()
	}
	return title.String()
}

Deleted encoding/rss/rss.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






















































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 rss provides a RSS encoding.
package rss

import (
	"bytes"
	"context"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoding"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
)

const ContentType = "application/rss+xml"

type Configuration struct {
	Title            string
	Language         string
	Copyright        string
	Generator        string
	NewURLBuilderAbs func() *api.URLBuilder
}

func (c *Configuration) Setup(ctx context.Context, cfg config.Config) {
	baseURL := kernel.Main.GetConfig(kernel.WebService, kernel.WebBaseURL).(string)
	defVals := cfg.AddDefaultValues(ctx, &meta.Meta{})

	c.Title = cfg.GetSiteName()
	c.Language = defVals.GetDefault(api.KeyLang, "")
	c.Copyright = defVals.GetDefault(api.KeyCopyright, "")
	c.Generator = (kernel.Main.GetConfig(kernel.CoreService, kernel.CoreProgname).(string) +
		" " +
		kernel.Main.GetConfig(kernel.CoreService, kernel.CoreVersion).(string))
	c.NewURLBuilderAbs = func() *api.URLBuilder { return api.NewURLBuilder(baseURL, 'h') }
}

func (c *Configuration) Marshal(q *query.Query, ml []*meta.Meta) []byte {
	rssPublished := encoding.LastUpdated(ml, time.RFC1123Z)

	atomLink := ""
	if s := q.String(); s != "" {
		atomLink = c.NewURLBuilderAbs().AppendQuery(s).String()
	}
	var buf bytes.Buffer
	buf.WriteString(`<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">` + "\n<channel>\n")
	xml.WriteTag(&buf, "  ", "title", c.Title)
	xml.WriteTag(&buf, "  ", "link", c.NewURLBuilderAbs().String())
	xml.WriteTag(&buf, "  ", "description", "")
	xml.WriteTag(&buf, "  ", "language", c.Language)
	xml.WriteTag(&buf, "  ", "copyright", c.Copyright)
	if rssPublished != "" {
		xml.WriteTag(&buf, "  ", "pubDate", rssPublished)
		xml.WriteTag(&buf, "  ", "lastBuildDate", rssPublished)
	}
	xml.WriteTag(&buf, "  ", "generator", c.Generator)
	buf.WriteString("  <docs>https://www.rssboard.org/rss-specification</docs>\n")
	if atomLink != "" {
		buf.WriteString(`  <atom:link href="`)
		strfun.XMLEscape(&buf, atomLink)
		buf.WriteString(`" rel="self" type="application/rss+xml"></atom:link>` + "\n")
	}
	for _, m := range ml {
		c.marshalMeta(&buf, m)
	}

	buf.WriteString("</channel>\n</rss>")
	return buf.Bytes()
}

func (c *Configuration) marshalMeta(buf *bytes.Buffer, m *meta.Meta) {
	itemPublished := ""
	if val, found := m.Get(api.KeyPublished); found {
		if published, err := time.ParseInLocation(id.ZidLayout, val, time.Local); err == nil {
			itemPublished = published.UTC().Format(time.RFC1123Z)
		}
	}

	link := c.NewURLBuilderAbs().SetZid(api.ZettelID(m.Zid.String())).String()

	buf.WriteString("  <item>\n")
	xml.WriteTag(buf, "    ", "title", encoding.TitleAsText(m))
	xml.WriteTag(buf, "    ", "link", link)
	xml.WriteTag(buf, "    ", "guid", link)
	if itemPublished != "" {
		xml.WriteTag(buf, "    ", "pubDate", itemPublished)
	}
	marshalTags(buf, m)
	buf.WriteString("  </item>\n")
}

func marshalTags(buf *bytes.Buffer, m *meta.Meta) {
	if tags, found := m.GetList(api.KeyTags); found && len(tags) > 0 {
		for _, tag := range tags {
			for len(tag) > 0 && tag[0] == '#' {
				tag = tag[1:]
			}
			if tag != "" {
				xml.WriteTag(buf, "    ", "category", tag)
			}
		}
	}
}

Deleted encoding/xml/xml.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) 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 xml provides helper for a XML-based encoding.
package xml

import (
	"bytes"

	"zettelstore.de/z/strfun"
)

// Header contains the string that should start all XML documents.
const Header = `<?xml version="1.0" encoding="UTF-8"?>` + "\n"

// WriteTag writes a simple XML tag with a given prefix and a specific value.
func WriteTag(buf *bytes.Buffer, prefix, tag, value string) {
	buf.WriteString(prefix)
	buf.WriteByte('<')
	buf.WriteString(tag)
	buf.WriteByte('>')
	strfun.XMLEscape(buf, value)
	buf.WriteString("</")
	buf.WriteString(tag)
	buf.WriteString(">\n")
}

Changes to evaluator/evaluator.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
1

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

-
+






+
+
+






+







+
+
-
-
+
+
+



-
-
-
-




+
+
+




-
-
-
+
+





+
-
+


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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 evaluator interprets and evaluates the AST.
package evaluator

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"path"
	"strconv"
	"strings"

	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxreader"
	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"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/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/parser/draw"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Port contains all methods to retrieve zettel (or part of it) to evaluate a zettel.
type Port interface {
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (domain.Zettel, error)
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// EvaluateZettel evaluates the given zettel in the given context, with the
// given ports, and the given environment.
func EvaluateZettel(ctx context.Context, port Port, rtConfig config.Config, zn *ast.ZettelNode) {
	switch zn.Syntax {
	if zn.Syntax == meta.SyntaxNone {
	case meta.SyntaxNone:
		// AST is empty, evaluate to a description list of metadata.
		zn.Ast = evaluateMetadata(zn.Meta)
	case meta.SyntaxSxn:
		zn.Ast = evaluateSxn(zn.Ast)
	default:
		EvaluateBlock(ctx, port, rtConfig, &zn.Ast)
	}
}

func evaluateSxn(bs ast.BlockSlice) ast.BlockSlice {
	// Check for structure made in parser/plain/plain.go:parseSxnBlocks
	if len(bs) == 1 {
		// If len(bs) > 1 --> an error was found during parsing
		if vn, isVerbatim := bs[0].(*ast.VerbatimNode); isVerbatim && vn.Kind == ast.VerbatimProg {
			if classAttr, hasClass := vn.Attrs.Get(""); hasClass && classAttr == meta.SyntaxSxn {
				rd := sxreader.MakeReader(bytes.NewReader(vn.Content))
				if objs, err := rd.ReadAll(); err == nil {
					result := make(ast.BlockSlice, len(objs))
					for i, obj := range objs {
						var buf bytes.Buffer
						sxbuiltins.Print(&buf, obj)
						result[i] = &ast.VerbatimNode{
							Kind:    ast.VerbatimProg,
							Attrs:   attrs.Attributes{"": classAttr},
							Content: buf.Bytes(),
						}
					}
		return
	}
					return result
				}
	EvaluateBlock(ctx, port, rtConfig, &zn.Ast)
			}
		}
	}
	return bs
}

// EvaluateBlock evaluates the given block list in the given context, with
// the given ports, and the given environment.
func EvaluateBlock(ctx context.Context, port Port, rtConfig config.Config, bns *ast.BlockSlice) {
	evaluateNode(ctx, port, rtConfig, bns)
	cleaner.CleanBlockSlice(bns, true)
163
164
165
166
167
168
169
170

171
172

173
174
175
176
177
178
179
197
198
199
200
201
202
203

204
205

206
207
208
209
210
211
212
213







-
+

-
+







	}
	return vn
}

func (e *evaluator) evalVerbatimZettel(vn *ast.VerbatimNode) ast.BlockNode {
	m := meta.New(id.Invalid)
	m.Set(api.KeySyntax, getSyntax(vn.Attrs, meta.SyntaxText))
	zettel := domain.Zettel{
	zettel := zettel.Zettel{
		Meta:    m,
		Content: domain.NewContent(vn.Content),
		Content: zettel.NewContent(vn.Content),
	}
	e.transcludeCount++
	zn := e.evaluateEmbeddedZettel(zettel)
	return &zn.Ast
}

func getSyntax(a attrs.Attributes, defSyntax string) string {
251
252
253
254
255
256
257
258

259
260
261
262
263
264
265

266
267
268
269
270
271
272
285
286
287
288
289
290
291

292
293
294
295
296
297
298

299
300
301
302
303
304
305
306







-
+






-
+







		e.transcludeCount += cost.ec
	}
	return &zn.Ast
}

func (e *evaluator) evalQueryTransclusion(expr string) ast.BlockNode {
	q := query.Parse(expr)
	ml, err := e.port.SelectMeta(e.ctx, q)
	ml, err := e.port.QueryMeta(e.ctx, q)
	if err != nil {
		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		return makeBlockNode(createInlineErrorText(nil, "Unable", "to", "search", "zettel"))
	}
	result := QueryAction(e.ctx, q, ml, e.rtConfig)
	result, _ := QueryAction(e.ctx, q, ml)
	if result != nil {
		ast.Walk(e, result)
	}
	return result
}

func (e *evaluator) checkMaxTransclusions(ref *ast.Reference) ast.InlineNode {
341
342
343
344
345
346
347
348

349
350
351
352
353
354
355
375
376
377
378
379
380
381

382
383
384
385
386
387
388
389







-
+







	}
	ref := ln.Ref
	if ref == nil || ref.State != ast.RefStateZettel {
		return ln
	}

	zid := mustParseZid(ref)
	_, err := e.port.GetMeta(box.NoEnrichContext(e.ctx), zid)
	_, err := e.port.GetZettel(box.NoEnrichContext(e.ctx), zid)
	if errors.Is(err, &box.ErrNotAllowed{}) {
		return &ast.FormatNode{
			Kind:    ast.FormatSpan,
			Attrs:   ln.Attrs,
			Inlines: getLinkInline(ln),
		}
	} else if err != nil {
404
405
406
407
408
409
410
411

412
413
414
415
416
417
418
438
439
440
441
442
443
444

445
446
447
448
449
450
451
452







-
+







		if errors.Is(err, &box.ErrNotAllowed{}) {
			return nil
		}
		e.transcludeCount++
		return createInlineErrorImage(en)
	}

	if syntax := zettel.Meta.GetDefault(api.KeySyntax, ""); parser.IsImageFormat(syntax) {
	if syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax); parser.IsImageFormat(syntax) {
		e.updateImageRefNode(en, zettel.Meta, syntax)
		return en
	} else if !parser.IsASTParser(syntax) {
		// Not embeddable.
		e.transcludeCount++
		return createInlineErrorText(ref, "Not", "embeddable (syntax="+syntax+")")
	}
532
533
534
535
536
537
538
539
540


541
542
543
544
545
546
547
566
567
568
569
570
571
572


573
574
575
576
577
578
579
580
581







-
-
+
+








func (e *evaluator) evaluateEmbeddedInline(content []byte, syntax string) ast.InlineSlice {
	is := parser.ParseInlines(input.NewInput(content), syntax)
	ast.Walk(e, &is)
	return is
}

func (e *evaluator) evaluateEmbeddedZettel(zettel domain.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, ""), e.rtConfig)
func (e *evaluator) evaluateEmbeddedZettel(zettel zettel.Zettel) *ast.ZettelNode {
	zn := parser.ParseZettel(e.ctx, zettel, zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax), e.rtConfig)
	ast.Walk(e, &zn.Ast)
	return zn
}

func findInlineSlice(bs *ast.BlockSlice, fragment string) ast.InlineSlice {
	if fragment == "" {
		return firstInlinesToEmbed(*bs)
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
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







-
+


-
+










-
+


-







		ast.Walk(fs, bn)
	}
}

func (fs *fragmentSearcher) visitInlineSlice(is *ast.InlineSlice) {
	for i, in := range *is {
		if mn, ok := in.(*ast.MarkNode); ok && mn.Fragment == fs.fragment {
			ris := skipSpaceNodes((*is)[i+1:])
			ris := skipBreakeNodes((*is)[i+1:])
			if len(mn.Inlines) > 0 {
				fs.result = append(ast.InlineSlice{}, mn.Inlines...)
				fs.result = append(fs.result, &ast.SpaceNode{Lexeme: " "})
				fs.result = append(fs.result, &ast.TextNode{Text: " "})
				fs.result = append(fs.result, ris...)
			} else {
				fs.result = ris
			}
			return
		}
		ast.Walk(fs, in)
	}
}

func skipSpaceNodes(ins ast.InlineSlice) ast.InlineSlice {
func skipBreakeNodes(ins ast.InlineSlice) ast.InlineSlice {
	for i, in := range ins {
		switch in.(type) {
		case *ast.SpaceNode:
		case *ast.BreakNode:
		default:
			return ins[i:]
		}
	}
	return nil
}

Changes to evaluator/list.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
1

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

-
+






+
+
+








-
+



-
-
+
+

-
+
-
-
+
-
-
+
-



-
+

-
-
-
-
-
-
+
+
+
+
+
+
-







-
-
+
+



-
+

-
+



-
+

-
+



-
-
+
+
-



-
+


-
-
-
+
-
-









-
-
+
+


+
+
-
+
+
+



-
-
-
-
-
-
+
+
+
+
+
+
-


-
+



-
+
















-
+


-
+



-
+









-
+


















-
+



-
-
-
+
+
+

-
-
+
+

-
+


-
+









-
+







-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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 evaluator

import (
	"bytes"
	"context"
	"math"
	"sort"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/c/attrs"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/query"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/query"
)

// QueryAction transforms a list of metadata according to query actions into a AST nested list.
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta, rtConfig config.Config) ast.BlockNode {
func QueryAction(ctx context.Context, q *query.Query, ml []*meta.Meta) (ast.BlockNode, int) {
	ap := actionPara{
		ctx:   ctx,
		q:     q,
		ml:    ml,
		kind:  ast.NestedListUnordered,
		min:   -1,
		max:   -1,
		ctx:    ctx,
		q:      q,
		ml:     ml,
		kind:   ast.NestedListUnordered,
		minVal: -1,
		maxVal: -1,
		title: rtConfig.GetSiteName(),
	}
	actions := q.Actions()
	if len(actions) == 0 {
		return ap.createBlockNodeMeta("")
	}

	acts := make([]string, 0, len(actions))
	for i, act := range actions {
		if strings.HasPrefix(act, "N") {
	for _, act := range actions {
		if strings.HasPrefix(act, api.NumberedAction[0:1]) {
			ap.kind = ast.NestedListOrdered
			continue
		}
		if strings.HasPrefix(act, "MIN") {
		if strings.HasPrefix(act, api.MinAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.min = num
				ap.minVal = num
				continue
			}
		}
		if strings.HasPrefix(act, "MAX") {
		if strings.HasPrefix(act, api.MaxAction) {
			if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
				ap.max = num
				ap.maxVal = num
				continue
			}
		}
		if act == "TITLE" && i+1 < len(actions) {
			ap.title = strings.Join(actions[i+1:], " ")
		if act == api.ReIndexAction {
			continue
			break
		}
		acts = append(acts, act)
	}
	var firstUnknownKey string
	var firstUnknowAct string
	for _, act := range acts {
		switch act {
		case "ATOM":
			return ap.createBlockNodeAtom(rtConfig)
		case "RSS":
		case api.KeysAction:
			return ap.createBlockNodeRSS(rtConfig)
		case "KEYS":
			return ap.createBlockNodeMetaKeys()
		}
		key := strings.ToLower(act)
		switch meta.Type(key) {
		case meta.TypeWord:
			return ap.createBlockNodeWord(key)
		case meta.TypeTagSet:
			return ap.createBlockNodeTagSet(key)
		}
		if firstUnknownKey == "" {
			firstUnknownKey = key
		if firstUnknowAct == "" {
			firstUnknowAct = act
		}
	}
	bn, numItems := ap.createBlockNodeMeta(strings.ToLower(firstUnknowAct))
	if bn != nil && numItems == 0 && firstUnknowAct == strings.ToUpper(firstUnknowAct) {
	return ap.createBlockNodeMeta(firstUnknownKey)
		bn, numItems = ap.createBlockNodeMeta("")
	}
	return bn, numItems
}

type actionPara struct {
	ctx   context.Context
	q     *query.Query
	ml    []*meta.Meta
	kind  ast.NestedListKind
	min   int
	max   int
	ctx    context.Context
	q      *query.Query
	ml     []*meta.Meta
	kind   ast.NestedListKind
	minVal int
	maxVal int
	title string
}

func (ap *actionPara) createBlockNodeWord(key string) ast.BlockNode {
func (ap *actionPara) createBlockNodeWord(key string) (ast.BlockNode, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ccs))
	ccs.SortByName()
	for _, cat := range ccs {
		buf.WriteString(cat.Name)
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(buf.String()),
			Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
		})})
		buf.Truncate(bufLen)
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}
	}, len(items)
}

func (ap *actionPara) createBlockNodeTagSet(key string) ast.BlockNode {
func (ap *actionPara) createBlockNodeTagSet(key string) (ast.BlockNode, int) {
	var buf bytes.Buffer
	ccs, bufLen := ap.prepareCatAction(key, &buf)
	if len(ccs) == 0 {
		return nil
		return nil, 0
	}
	ccs.SortByCount()
	ccs = ap.limitTags(ccs)
	countMap := ap.calcFontSizes(ccs)

	para := make(ast.InlineSlice, 0, len(ccs))
	ccs.SortByName()
	for i, cat := range ccs {
		if i > 0 {
			para = append(para, &ast.SpaceNode{Lexeme: " "})
			para = append(para, &ast.TextNode{Text: " "})
		}
		buf.WriteString(cat.Name)
		para = append(para,
			&ast.LinkNode{
				Attrs: countMap[cat.Count],
				Ref:   ast.ParseReference(buf.String()),
				Inlines: ast.InlineSlice{
					&ast.TextNode{Text: cat.Name},
				},
			},
			&ast.FormatNode{
				Kind:    ast.FormatSuper,
				Attrs:   nil,
				Inlines: ast.InlineSlice{&ast.TextNode{Text: strconv.Itoa(cat.Count)}},
			},
		)
		buf.Truncate(bufLen)
	}
	return &ast.ParaNode{Inlines: para}
	return &ast.ParaNode{Inlines: para}, len(ccs)
}

func (ap *actionPara) limitTags(ccs meta.CountedCategories) meta.CountedCategories {
	if min, max := ap.min, ap.max; min > 0 || max > 0 {
		if min < 0 {
			min = ccs[len(ccs)-1].Count
	if minVal, maxVal := ap.minVal, ap.maxVal; minVal > 0 || maxVal > 0 {
		if minVal < 0 {
			minVal = ccs[len(ccs)-1].Count
		}
		if max < 0 {
			max = ccs[0].Count
		if maxVal < 0 {
			maxVal = ccs[0].Count
		}
		if ccs[len(ccs)-1].Count < min || max < ccs[0].Count {
		if ccs[len(ccs)-1].Count < minVal || maxVal < ccs[0].Count {
			temp := make(meta.CountedCategories, 0, len(ccs))
			for _, cat := range ccs {
				if min <= cat.Count && cat.Count <= max {
				if minVal <= cat.Count && cat.Count <= maxVal {
					temp = append(temp, cat)
				}
			}
			return temp
		}
	}
	return ccs
}

func (ap *actionPara) createBlockNodeMetaKeys() ast.BlockNode {
func (ap *actionPara) createBlockNodeMetaKeys() (ast.BlockNode, int) {
	arr := make(meta.Arrangement, 128)
	for _, m := range ap.ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	if len(arr) == 0 {
		return nil
		return nil, 0
	}
	ccs := arr.Counted()
	ccs.SortByName()

	var buf bytes.Buffer
	bufLen := ap.prepareSimpleQuery(&buf)
	items := make([]ast.ItemSlice, 0, len(ccs))
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
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







-
+













-
+


-
+

-
+








-
-
-
-
-


-
-
+
+






-
+








		items = append(items, ast.ItemSlice{ast.CreateParaNode(
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q1),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: cat.Name}},
			},
			&ast.SpaceNode{Lexeme: " "},
			&ast.TextNode{Text: " "},
			&ast.TextNode{Text: "(" + strconv.Itoa(cat.Count) + ", "},
			&ast.LinkNode{
				Attrs:   nil,
				Ref:     ast.ParseReference(q2),
				Inlines: ast.InlineSlice{&ast.TextNode{Text: "values"}},
			},
			&ast.TextNode{Text: ")"},
		)})
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}
	}, len(items)
}

func (ap *actionPara) createBlockNodeMeta(key string) ast.BlockNode {
func (ap *actionPara) createBlockNodeMeta(key string) (ast.BlockNode, int) {
	if len(ap.ml) == 0 {
		return nil
		return nil, 0
	}
	items := make([]ast.ItemSlice, 0, len(ap.ml))
	for _, m := range ap.ml {
		if key != "" {
			if _, found := m.Get(key); !found {
				continue
			}
		}
		zid := m.Zid.String()
		title, found := m.Get(api.KeyTitle)
		if !found {
			title = zid
		}
		items = append(items, ast.ItemSlice{ast.CreateParaNode(&ast.LinkNode{
			Attrs:   nil,
			Ref:     ast.ParseReference(zid),
			Inlines: parser.ParseMetadataNoLink(title),
			Ref:     ast.ParseReference(m.Zid.String()),
			Inlines: parser.ParseSpacedText(m.GetTitle()),
		})})
	}
	return &ast.NestedListNode{
		Kind:  ap.kind,
		Items: items,
		Attrs: nil,
	}
	}, len(items)
}

func (ap *actionPara) prepareCatAction(key string, buf *bytes.Buffer) (meta.CountedCategories, int) {
	if len(ap.ml) == 0 {
		return nil, 0
	}
	ccs := meta.CreateArrangement(ap.ml, key).Counted()
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
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







-
+












-
+








const fontSizes = 6 // Must be the number of CSS classes zs-font-size-* in base.css
const fontSizes64 = float64(fontSizes)

func (*actionPara) calcFontSizes(ccs meta.CountedCategories) map[int]attrs.Attributes {
	var fsAttrs [fontSizes]attrs.Attributes
	var a attrs.Attributes
	for i := 0; i < fontSizes; i++ {
	for i := range fontSizes {
		fsAttrs[i] = a.AddClass("zs-font-size-" + strconv.Itoa(i))
	}

	countMap := make(map[int]int, len(ccs))
	for _, cat := range ccs {
		countMap[cat.Count]++
	}

	countList := make([]int, 0, len(countMap))
	for count := range countMap {
		countList = append(countList, count)
	}
	sort.Ints(countList)
	slices.Sort(countList)

	result := make(map[int]attrs.Attributes, len(countList))
	if len(countList) <= fontSizes {
		// If we have less different counts, center them inside the fsAttrs vector.
		curSize := (fontSizes - len(countList)) / 2
		for _, count := range countList {
			result[count] = fsAttrs[curSize]
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
333
334
335
336
337
338
339

































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
			}
		}
	}
	return result
}

func calcBudget(total, curSize float64) float64 { return math.Round(total / (fontSizes64 - curSize)) }

func (ap *actionPara) createBlockNodeRSS(cfg config.Config) ast.BlockNode {
	var rssConfig rss.Configuration
	rssConfig.Setup(ap.ctx, cfg)
	rssConfig.Title = ap.title
	data := rssConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}
}

func (ap *actionPara) createBlockNodeAtom(cfg config.Config) ast.BlockNode {
	var atomConfig atom.Configuration
	atomConfig.Setup(cfg)
	atomConfig.Title = ap.title
	data := atomConfig.Marshal(ap.q, ap.ml)

	return &ast.VerbatimNode{
		Kind:    ast.VerbatimProg,
		Attrs:   attrs.Attributes{"lang": "xml"},
		Content: data,
	}
}

Changes to evaluator/metadata.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15

16
17
18
19
20
21
22
1

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) 2021-2022 Detlef Stern
// 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 evaluator

import (
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/zettel/meta"
)

func evaluateMetadata(m *meta.Meta) ast.BlockSlice {
	descrlist := &ast.DescriptionListNode{}
	for _, p := range m.Pairs() {
		descrlist.Descriptions = append(
			descrlist.Descriptions, getMetadataDescription(p.Key, p.Value))
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
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







-
-
-
-
+
-




-
+













		sliceData = meta.ListFromValue(value)
		if len(sliceData) == 0 {
			return nil
		}
	} else {
		sliceData = []string{value}
	}
	var makeLink bool
	switch dt {
	case meta.TypeID, meta.TypeIDSet:
		makeLink = true
	makeLink := dt == meta.TypeID || dt == meta.TypeIDSet
	}

	result := make(ast.InlineSlice, 0, 2*len(sliceData)-1)
	for i, val := range sliceData {
		if i > 0 {
			result = append(result, &ast.SpaceNode{Lexeme: " "})
			result = append(result, &ast.TextNode{Text: " "})
		}
		tn := &ast.TextNode{Text: val}
		if makeLink {
			result = append(result, &ast.LinkNode{
				Ref:     ast.ParseReference(val),
				Inlines: ast.InlineSlice{tn},
			})
		} else {
			result = append(result, tn)
		}
	}
	return result
}

Changes to go.mod.

1
2
3

4
5
6
7

8
9
10
11
12
13







14
15

16



1
2

3
4
5


6






7
8
9
10
11
12
13
14
15
16

17
18
19


-
+


-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
+


+
-
+
+
+
module zettelstore.de/z

go 1.19
go 1.23

require (
	codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0
	github.com/fsnotify/fsnotify v1.6.0
	github.com/fsnotify/fsnotify v1.8.0
	github.com/pascaldekloe/jwt v1.12.0
	github.com/yuin/goldmark v1.5.3
	golang.org/x/crypto v0.5.0
	golang.org/x/term v0.4.0
	golang.org/x/text v0.6.0
	zettelstore.de/c v0.10.0
	github.com/yuin/goldmark v1.7.8
	golang.org/x/crypto v0.31.0
	golang.org/x/term v0.27.0
	golang.org/x/text v0.21.0
	t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5
	t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f
	t73f.de/r/zsc v0.0.0-20241213141447-7902e0d34990
)

require (
require golang.org/x/sys v0.4.0 // indirect
	golang.org/x/sys v0.28.0 // indirect
	t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd // indirect
)

Changes to go.sum.

1
2
3
4


5
6
7
8
9
10




11
12
13
14
15
16
17
18
19


















1
2






3
4
5
6









7
8
9
10
11
12
13
14
15
16
17
18
19
20
-
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0 h1:viya/OgeF16+i8caBPJmcLQhGpZodPh+/nxtJzSSO1s=
codeberg.org/t73fde/sxpf v0.0.0-20220719090054-749a39d0a7a0/go.mod h1:4fAHEF3VH+ofbZkF6NzqiItTNy2X11tVCnZX99jXouA=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/pascaldekloe/jwt v1.12.0 h1:imQSkPOtAIBAXoKKjL9ZVJuF/rVqJ+ntiLGpLyeqMUQ=
github.com/pascaldekloe/jwt v1.12.0/go.mod h1:LiIl7EwaglmH1hWThd/AmydNCnHf/mmfluBlNqHbk8U=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
zettelstore.de/c v0.10.0 h1:e5VOrH5aEEojNrUfO/721dsiHsp/yuTvNPF6DNsgA3U=
zettelstore.de/c v0.10.0/go.mod h1:+SoneUhKQ81A2Id/bC6FdDYYQAHYfVryh7wHFnnklew=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5 h1:ug4hohM6pK28M8Uo0o3+XvjBure2wfEtuCnHVIdqBZY=
t73f.de/r/sx v0.0.0-20240814083626-4df0ec6454b5/go.mod h1:VRvsWoBErPKvMieDMMk1hsh1tb9sA4ijEQWGw/TbtQ0=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f h1:VJ4S7YWy9tCJuFz5MckqUjjktPaf0kpnTkNBVRVXpo4=
t73f.de/r/sxwebs v0.0.0-20241031144449-53c3b2ed1a6f/go.mod h1:IaM+U+LvYTYeuiIS5cwZW6kcEpdwoKBYVCU7LZr4Sgk=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd h1:+7cqJonXKDso+uPvsvOPl7BiLkhj8VQT/Has8qC5VIQ=
t73f.de/r/webs v0.0.0-20241031141359-cd4f76a622cd/go.mod h1:NSoOON8be62MfQZzlCApK27Jt2zhIa6Vrmo9RJ4tOnQ=
t73f.de/r/zsc v0.0.0-20241213141447-7902e0d34990 h1:iUDHrc4tjfq3UZT3HLrqx2HMN4fZpFCdpNqgr73vTOw=
t73f.de/r/zsc v0.0.0-20241213141447-7902e0d34990/go.mod h1:sQWKzNp0I18aSFnKJjAyhL1zTWITIj1v2acWv4GvuIY=

Deleted input/entity.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































































































































































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

import (
	"html"
	"unicode"
)

// ScanEntity scans either a named or a numbered entity and returns it as a string.
//
// For numbered entities (like &#123; or &#x123;) html.UnescapeString returns
// sometimes other values as expected, if the number is not well-formed. This
// may happen because of some strange HTML parsing rules. But these do not
// apply to Zettelmarkup. Therefore, I parse the number here in the code.
func (inp *Input) ScanEntity() (res string, success bool) {
	if inp.Ch != '&' {
		return "", false
	}
	pos := inp.Pos
	inp.Next()
	if inp.Ch == '#' {
		inp.Next()
		if inp.Ch == 'x' || inp.Ch == 'X' {
			return inp.scanEntityBase16()
		}
		return inp.scanEntityBase10()
	}
	return inp.scanEntityNamed(pos)
}

func (inp *Input) scanEntityBase16() (string, bool) {
	inp.Next()
	if inp.Ch == ';' {
		return "", false
	}
	code := 0
	for {
		switch ch := inp.Ch; ch {
		case ';':
			inp.Next()
			if r := rune(code); isValidEntity(r) {
				return string(r), true
			}
			return "", false
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			code = 16*code + int(ch-'0')
		case 'a', 'b', 'c', 'd', 'e', 'f':
			code = 16*code + int(ch-'a'+10)
		case 'A', 'B', 'C', 'D', 'E', 'F':
			code = 16*code + int(ch-'A'+10)
		default:
			return "", false
		}
		if code > unicode.MaxRune {
			return "", false
		}
		inp.Next()
	}
}

func (inp *Input) scanEntityBase10() (string, bool) {
	// Base 10 code
	if inp.Ch == ';' {
		return "", false
	}
	code := 0
	for {
		switch ch := inp.Ch; ch {
		case ';':
			inp.Next()
			if r := rune(code); isValidEntity(r) {
				return string(r), true
			}
			return "", false
		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			code = 10*code + int(ch-'0')
		default:
			return "", false
		}
		if code > unicode.MaxRune {
			return "", false
		}
		inp.Next()
	}
}
func (inp *Input) scanEntityNamed(pos int) (string, bool) {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r', '&':
			return "", false
		case ';':
			inp.Next()
			es := string(inp.Src[pos:inp.Pos])
			ues := html.UnescapeString(es)
			if es == ues {
				return "", false
			}
			return ues, true
		default:
			inp.Next()
		}
	}
}

// isValidEntity checks if the given code is valid for an entity.
//
// According to https://html.spec.whatwg.org/multipage/syntax.html#character-references
// ""The numeric character reference forms described above are allowed to reference any code point
// excluding U+000D CR, noncharacters, and controls other than ASCII whitespace.""
func isValidEntity(r rune) bool {
	// No C0 control and no "code point in the range U+007F DELETE to U+009F APPLICATION PROGRAM COMMAND, inclusive."
	if r < ' ' || ('\u007f' <= r && r <= '\u009f') {
		return false
	}

	// If below any noncharacter code point, return true
	//
	// See: https://infra.spec.whatwg.org/#noncharacter
	if r < '\ufdd0' {
		return true
	}

	// First range of noncharacter code points: "(...) in the range U+FDD0 to U+FDEF, inclusive"
	if r <= '\ufdef' {
		return false
	}

	// Other noncharacter code points:
	switch r {
	case '\uFFFE', '\uFFFF',
		'\U0001FFFE', '\U0001FFFF',
		'\U0002FFFE', '\U0002FFFF',
		'\U0003FFFE', '\U0003FFFF',
		'\U0004FFFE', '\U0004FFFF',
		'\U0005FFFE', '\U0005FFFF',
		'\U0006FFFE', '\U0006FFFF',
		'\U0007FFFE', '\U0007FFFF',
		'\U0008FFFE', '\U0008FFFF',
		'\U0009FFFE', '\U0009FFFF',
		'\U000AFFFE', '\U000AFFFF',
		'\U000BFFFE', '\U000BFFFF',
		'\U000CFFFE', '\U000CFFFF',
		'\U000DFFFE', '\U000DFFFF',
		'\U000EFFFE', '\U000EFFFF',
		'\U000FFFFE', '\U000FFFFF',
		'\U0010FFFE', '\U0010FFFF':
		return false
	}
	return true
}

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





























































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

import (
	"testing"

	"zettelstore.de/z/input"
)

func TestScanEntity(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"a", ""},
		{"&amp;", "&"},
		{"&#33;", "!"},
		{"&#x33;", "3"},
		{"&quot;", "\""},
	}
	for id, tc := range testcases {
		inp := input.NewInput([]byte(tc.text))
		got, ok := inp.ScanEntity()
		if !ok {
			if tc.exp != "" {
				t.Errorf("ID=%d, text=%q: expected error, but got %q", id, tc.text, got)
			}
			if inp.Pos != 0 {
				t.Errorf("ID=%d, text=%q: input position advances to %d", id, tc.text, inp.Pos)
			}
			continue
		}
		if tc.exp != got {
			t.Errorf("ID=%d, text=%q: expected %q, but got %q", id, tc.text, tc.exp, got)
		}
	}
}

func TestScanIllegalEntity(t *testing.T) {
	t.Parallel()
	testcases := []string{"", "a", "& Input &rarr;", "&#9;", "&#x1f;"}
	for i, tc := range testcases {
		inp := input.NewInput([]byte(tc))
		got, ok := inp.ScanEntity()
		if ok {
			t.Errorf("%d: scanning %q was unexpected successful, got %q", i, tc, got)
			continue
		}
	}
}

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

























































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 input provides an abstraction for data to be read.
package input

import "unicode/utf8"

// Input is an abstract input source
type Input struct {
	// Read-only, will never change
	Src []byte // The source string

	// Read-only, will change
	Ch      rune // current character
	Pos     int  // character position in src
	readPos int  // reading position (position after current character)
}

// NewInput creates a new input source.
func NewInput(src []byte) *Input {
	inp := &Input{Src: src}
	inp.Next()
	return inp
}

// EOS = End of source
const EOS = rune(-1)

// Next reads the next rune into inp.Ch and returns it too.
func (inp *Input) Next() rune {
	if inp.readPos >= len(inp.Src) {
		inp.Pos = len(inp.Src)
		inp.Ch = EOS
		return EOS
	}
	inp.Pos = inp.readPos
	r, w := rune(inp.Src[inp.readPos]), 1
	if r >= utf8.RuneSelf {
		r, w = utf8.DecodeRune(inp.Src[inp.readPos:])
	}
	inp.readPos += w
	inp.Ch = r
	return r
}

// Peek returns the rune following the most recently read rune without
// advancing. If end-of-source was already found peek returns EOS.
func (inp *Input) Peek() rune {
	return inp.PeekN(0)
}

// PeekN returns the n-th rune after the most recently read rune without
// advancing. If end-of-source was already found peek returns EOS.
func (inp *Input) PeekN(n int) rune {
	pos := inp.readPos + n
	if pos < len(inp.Src) {
		r := rune(inp.Src[pos])
		if r >= utf8.RuneSelf {
			r, _ = utf8.DecodeRune(inp.Src[pos:])
		}
		if r == '\t' {
			return ' '
		}
		return r
	}
	return EOS
}

// Accept checks if the given string is a prefix of the text to be parsed.
// If successful, advance position and current character.
// String must only contain bytes < 128.
// If not successful, everything remains as it is.
func (inp *Input) Accept(s string) bool {
	pos := inp.Pos
	remaining := len(inp.Src) - pos
	if s == "" || len(s) > remaining {
		return false
	}
	// According to internal documentation of bytes.Equal, the string() will not allocate any memory.
	if readPos := pos + len(s); s == string(inp.Src[pos:readPos]) {
		inp.readPos = readPos
		inp.Next()
		return true
	}
	return false
}

// IsEOLEOS returns true if char is either EOS or EOL.
func IsEOLEOS(ch rune) bool {
	switch ch {
	case EOS, '\n', '\r':
		return true
	}
	return false
}

// EatEOL transforms both "\r" and "\r\n" into "\n".
func (inp *Input) EatEOL() {
	switch inp.Ch {
	case '\r':
		if inp.Peek() == '\n' {
			inp.Next()
		}
		inp.Ch = '\n'
		inp.Next()
	case '\n':
		inp.Next()
	}
}

// SetPos allows to reset the read position.
func (inp *Input) SetPos(pos int) {
	if inp.Pos != pos {
		inp.readPos = pos
		inp.Next()
	}
}

// SkipToEOL reads until the next end-of-line.
func (inp *Input) SkipToEOL() {
	for {
		switch inp.Ch {
		case EOS, '\n', '\r':
			return
		}
		inp.Next()
	}
}

// ScanLineContent reads the reaining input stream and interprets it as lines of text.
func (inp *Input) ScanLineContent() []byte {
	result := make([]byte, 0, len(inp.Src)-inp.Pos+1)
	for {
		inp.EatEOL()
		posL := inp.Pos
		if inp.Ch == EOS {
			return result
		}
		inp.SkipToEOL()
		if len(result) > 0 {
			result = append(result, '\n')
		}
		result = append(result, inp.Src[posL:inp.Pos]...)
	}
}

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

































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 input_test provides some unit-tests for reading data.
package input_test

import (
	"testing"

	"zettelstore.de/z/input"
)

func TestEatEOL(t *testing.T) {
	t.Parallel()
	inp := input.NewInput(nil)
	inp.EatEOL()
	if inp.Ch != input.EOS {
		t.Errorf("No EOS found: %q", inp.Ch)
	}
	if inp.Pos != 0 {
		t.Errorf("Pos != 0: %d", inp.Pos)
	}

	inp = input.NewInput([]byte("ABC"))
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
	inp.EatEOL()
	if inp.Ch != 'A' {
		t.Errorf("First ch != 'A', got %q", inp.Ch)
	}
}

func TestAccept(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		accept string
		src    string
		acc    bool
		exp    rune
	}{
		{"", "", false, input.EOS},
		{"AB", "abc", false, 'a'},
		{"AB", "ABC", true, 'C'},
		{"AB", "AB", true, input.EOS},
		{"AB", "A", false, 'A'},
	}
	for i, tc := range testcases {
		inp := input.NewInput([]byte(tc.src))
		acc := inp.Accept(tc.accept)
		if acc != tc.acc {
			t.Errorf("%d: %q.Accept(%q) == %v, but got %v", i, tc.src, tc.accept, tc.acc, acc)
		}
		if got := inp.Ch; tc.exp != got {
			t.Errorf("%d: %q.Accept(%q) should result in run %v, but got %v", i, tc.src, tc.accept, tc.exp, got)
		}
	}
}

Deleted input/runes.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 input

import "unicode"

// IsSpace returns true if rune is a whitespace.
func IsSpace(ch rune) bool {
	switch ch {
	case ' ', '\t':
		return true
	case '\n', '\r', EOS:
		return false
	}
	return unicode.IsSpace(ch)
}

Changes to kernel/impl/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
1

2
3
4
5
6
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
// 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

import (
	"errors"
	"sync"

	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type authService struct {
	srvConfig
	mxService     sync.RWMutex
	manager       auth.Manager
	createManager kernel.CreateAuthManagerFunc
68
69
70
71
72
73
74
75

76
77
78
79
80
81
82
71
72
73
74
75
76
77

78
79
80
81
82
83
84
85







-
+







func (as *authService) Start(*myKernel) error {
	as.mxService.Lock()
	defer as.mxService.Unlock()
	readonlyMode := as.GetNextConfig(kernel.AuthReadonly).(bool)
	owner := as.GetNextConfig(kernel.AuthOwner).(id.Zid)
	authMgr, err := as.createManager(readonlyMode, owner)
	if err != nil {
		as.logger.Fatal().Err(err).Msg("Unable to create manager")
		as.logger.Error().Err(err).Msg("Unable to create manager")
		return err
	}
	as.logger.Info().Msg("Start Manager")
	as.manager = authMgr
	return nil
}

Changes to kernel/impl/box.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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

import (
	"context"
	"errors"
78
79
80
81
82
83
84
85

86
87
88
89
90

91
92
93
94
95
96
97
81
82
83
84
85
86
87

88
89
90
91
92

93
94
95
96
97
98
99
100







-
+




-
+







		}
		boxURIs = append(boxURIs, u.(*url.URL))
	}
	ps.mxService.Lock()
	defer ps.mxService.Unlock()
	mgr, err := ps.createManager(boxURIs, kern.auth.manager, &kern.cfg)
	if err != nil {
		ps.logger.Fatal().Err(err).Msg("Unable to create manager")
		ps.logger.Error().Err(err).Msg("Unable to create manager")
		return err
	}
	ps.logger.Info().Str("location", mgr.Location()).Msg("Start Manager")
	if err = mgr.Start(context.Background()); err != nil {
		ps.logger.Fatal().Err(err).Msg("Unable to start manager")
		ps.logger.Error().Err(err).Msg("Unable to start manager")
		return err
	}
	kern.cfg.setBox(mgr)
	ps.manager = mgr
	return nil
}

Changes to kernel/impl/cfg.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
1

2
3
4
5
6
7
8
9
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) 2021-2022 Detlef Stern
// 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

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type configService struct {
	srvConfig
	mxService sync.RWMutex
	orig      *meta.Meta
	manager   box.Manager
}

// Predefined Metadata keys for runtime configuration
// See: https://zettelstore.de/manual/h/00001004020000
const (
	keyDefaultCopyright  = "default-copyright"
	keyDefaultLicense    = "default-license"
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
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







-
+
-
-
-
-
+
+
+





-
+
+
+
+
+


-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+







				case kernel.ConfigZmkHTML:
					return config.ZettelmarkupHTML, nil
				}
				return config.NoHTML, nil
			}),
			true,
		},
		api.KeyLang:              {"Language", parseString, true},
		api.KeyLang:         {"Language", parseString, true},
		config.KeyMarkerExternal: {"Marker external URL", parseString, true},
		keyMaxTransclusions:      {"Maximum transclusions", parseInt64, true},
		keySiteName:              {"Site name", parseString, true},
		keyYAMLHeader:            {"YAML header", parseBool, true},
		keyMaxTransclusions: {"Maximum transclusions", parseInt64, true},
		keySiteName:         {"Site name", parseString, true},
		keyYAMLHeader:       {"YAML header", parseBool, true},
		keyZettelFileSyntax: {
			"Zettel file syntax",
			func(val string) (any, error) { return strings.Fields(val), nil },
			true,
		},
		kernel.ConfigSimpleMode: {"Simple mode", cs.noFrozen(parseBool), true},
		kernel.ConfigSimpleMode:      {"Simple mode", cs.noFrozen(parseBool), true},
		config.KeyShowBackLinks:      {"Show back links", parseString, true},
		config.KeyShowFolgeLinks:     {"Show folge links", parseString, true},
		config.KeyShowSequelLinks:    {"Show sequel links", parseString, true},
		config.KeyShowSuccessorLinks: {"Show successor links", parseString, true},
	}
	cs.next = interfaceMap{
		keyDefaultCopyright:       "",
		keyDefaultLicense:         "",
		keyDefaultVisibility:      meta.VisibilityLogin,
		keyExpertMode:             false,
		config.KeyFooterZettel:    id.Invalid,
		config.KeyHomeZettel:      id.DefaultHomeZid,
		kernel.ConfigInsecureHTML: config.NoHTML,
		api.KeyLang:               api.ValueLangEN,
		keyDefaultCopyright:          "",
		keyDefaultLicense:            "",
		keyDefaultVisibility:         meta.VisibilityLogin,
		keyExpertMode:                false,
		config.KeyFooterZettel:       id.Invalid,
		config.KeyHomeZettel:         id.DefaultHomeZid,
		kernel.ConfigInsecureHTML:    config.NoHTML,
		api.KeyLang:                  api.ValueLangEN,
		config.KeyMarkerExternal:  "&#10138;",
		keyMaxTransclusions:       int64(1024),
		keySiteName:               "Zettelstore",
		keyYAMLHeader:             false,
		keyZettelFileSyntax:       nil,
		kernel.ConfigSimpleMode:   false,
		keyMaxTransclusions:          int64(1024),
		keySiteName:                  "Zettelstore",
		keyYAMLHeader:                false,
		keyZettelFileSyntax:          nil,
		kernel.ConfigSimpleMode:      false,
		config.KeyShowBackLinks:      "",
		config.KeyShowFolgeLinks:     "",
		config.KeyShowSequelLinks:    "",
		config.KeyShowSuccessorLinks: "",
	}
}
func (cs *configService) GetLogger() *logger.Logger { return cs.logger }

func (cs *configService) Start(*myKernel) error {
	cs.logger.Info().Msg("Start Service")
	data := meta.New(id.ConfigurationZid)
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
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







+








+
+
+

-
+


-
-
+
+




+















-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+







	return cs.orig != nil
}

func (cs *configService) Stop(*myKernel) {
	cs.logger.Info().Msg("Stop Service")
	cs.mxService.Lock()
	cs.orig = nil
	cs.manager = nil
	cs.mxService.Unlock()
}

func (*configService) GetStatistics() []kernel.KeyValue {
	return nil
}

func (cs *configService) setBox(mgr box.Manager) {
	cs.mxService.Lock()
	cs.manager = mgr
	cs.mxService.Unlock()
	mgr.RegisterObserver(cs.observe)
	cs.doUpdate(mgr)
	cs.observe(box.UpdateInfo{Box: mgr, Reason: box.OnZettel, Zid: id.ConfigurationZid})
}

func (cs *configService) doUpdate(p box.Box) error {
	m, err := p.GetMeta(context.Background(), cs.orig.Zid)
func (cs *configService) doUpdate(p box.BaseBox) error {
	z, err := p.GetZettel(context.Background(), id.ConfigurationZid)
	cs.logger.Trace().Err(err).Msg("got config meta")
	if err != nil {
		return err
	}
	m := z.Meta
	cs.mxService.Lock()
	for _, pair := range cs.orig.Pairs() {
		key := pair.Key
		if val, ok := m.Get(key); ok {
			cs.SetConfig(key, val)
		} else if defVal, defFound := cs.orig.Get(key); defFound {
			cs.SetConfig(key, defVal)
		}
	}
	cs.mxService.Unlock()
	cs.SwitchNextToCur() // Poor man's restart
	return nil
}

func (cs *configService) observe(ci box.UpdateInfo) {
	if ci.Reason == box.OnReload {
		cs.logger.Debug().Msg("reload")
		go func() { cs.doUpdate(ci.Box) }()
	} else if ci.Zid == id.ConfigurationZid {
	if (ci.Reason != box.OnZettel && ci.Reason != box.OnDelete) || ci.Zid == id.ConfigurationZid {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() {
			cs.mxService.RLock()
			mgr := cs.manager
			cs.mxService.RUnlock()
			if mgr != nil {
				cs.doUpdate(mgr)
			} else {
		cs.logger.Debug().Uint("reason", uint64(ci.Reason)).Zid(ci.Zid).Msg("observe")
		go func() { cs.doUpdate(ci.Box) }()
				cs.doUpdate(ci.Box)
			}
		}()
	}
}

// --- config.Config

func (cs *configService) Get(ctx context.Context, m *meta.Meta, key string) string {
	if m != nil {

Changes to kernel/impl/cmd.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
1

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

import (
	"fmt"
	"io"
	"os"
	"runtime/metrics"
	"sort"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/c/maps"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
)

type cmdSession struct {
	w        io.Writer
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
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







-
+














-
+














-
+



















-
+







	"bye": {
		"end this session",
		func(*cmdSession, string, []string) bool { return false },
	},
	"config": {"show configuration keys", cmdConfig},
	"crlf": {
		"toggle crlf mode",
		func(sess *cmdSession, cmd string, args []string) bool {
		func(sess *cmdSession, _ string, _ []string) bool {
			if len(sess.eol) == 1 {
				sess.eol = []byte{'\r', '\n'}
				sess.println("crlf is on")
			} else {
				sess.eol = []byte{'\n'}
				sess.println("crlf is off")
			}
			return true
		},
	},
	"dump-index":   {"writes the content of the index", cmdDumpIndex},
	"dump-recover": {"show data of last recovery", cmdDumpRecover},
	"echo": {
		"toggle echo mode",
		func(sess *cmdSession, cmd string, args []string) bool {
		func(sess *cmdSession, _ string, _ []string) bool {
			sess.echo = !sess.echo
			if sess.echo {
				sess.println("echo is on")
			} else {
				sess.println("echo is off")
			}
			return true
		},
	},
	"end-profile": {"stop profiling", cmdEndProfile},
	"env":         {"show environment values", cmdEnvironment},
	"get-config":  {"show current configuration data", cmdGetConfig},
	"header": {
		"toggle table header",
		func(sess *cmdSession, cmd string, args []string) bool {
		func(sess *cmdSession, _ string, _ []string) bool {
			sess.header = !sess.header
			if sess.header {
				sess.println("header are on")
			} else {
				sess.println("header are off")
			}
			return true
		},
	},
	"log-level":   {"get/set log level", cmdLogLevel},
	"metrics":     {"show Go runtime metrics", cmdMetrics},
	"next-config": {"show next configuration data", cmdNextConfig},
	"profile":     {"start profiling", cmdProfile},
	"refresh":     {"refresh box data", cmdRefresh},
	"restart":     {"restart service", cmdRestart},
	"services":    {"show available services", cmdServices},
	"set-config":  {"set next configuration data", cmdSetConfig},
	"shutdown": {
		"shutdown Zettelstore",
		func(sess *cmdSession, cmd string, args []string) bool { sess.kern.Shutdown(false); return false },
		func(sess *cmdSession, _ string, _ []string) bool { sess.kern.Shutdown(false); return false },
	},
	"start": {"start service", cmdStart},
	"stat":  {"show service statistics", cmdStat},
	"stop":  {"stop service", cmdStop},
}

func cmdHelp(sess *cmdSession, _ string, _ []string) bool {
235
236
237
238
239
240
241
242

243
244
245
246
247
248
249
238
239
240
241
242
243
244

245
246
247
248
249
250
251
252







-
+







	listConfig func(*cmdSession, service), getConfig func(service, string) interface{}) {

	if len(args) == 0 {
		keys := make([]int, 0, len(sess.kern.srvs))
		for k := range sess.kern.srvs {
			keys = append(keys, int(k))
		}
		sort.Ints(keys)
		slices.Sort(keys)
		for i, k := range keys {
			if i > 0 {
				sess.println()
			}
			srvD := sess.kern.srvs[kernel.Service(k)]
			sess.println("%% Service", srvD.name)
			listConfig(sess, srvD.srv)
334
335
336
337
338
339
340
341

342
343
344
345

346
347
348
349
350
351
352
353
354
337
338
339
340
341
342
343

344




345


346
347
348
349
350
351
352







-
+
-
-
-
-
+
-
-







	if err != nil {
		sess.println(err.Error())
	}
	return true
}

func cmdStop(sess *cmdSession, cmd string, args []string) bool {
	srvnum, ok := lookupService(sess, cmd, args)
	if srvnum, ok := lookupService(sess, cmd, args); ok {
	if !ok {
		return true
	}
	err := sess.kern.doStopService(srvnum)
		sess.kern.doStopService(srvnum)
	if err != nil {
		sess.println(err.Error())
	}
	return true
}

func cmdStat(sess *cmdSession, cmd string, args []string) bool {
	if len(args) == 0 {
		sess.usage(cmd, "SERVICE")
538
539
540
541
542
543
544
545

546
547
548
549
550
551
552
536
537
538
539
540
541
542

543
544
545
546
547
548
549
550







-
+







		workDir = err.Error()
	}
	execName, err := os.Executable()
	if err != nil {
		execName = err.Error()
	}
	envs := os.Environ()
	sort.Strings(envs)
	slices.Sort(envs)

	table := [][]string{
		{"Key", "Value"},
		{"workdir", workDir},
		{"executable", execName},
	}
	for _, env := range envs {

Changes to kernel/impl/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
1

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

import (
	"errors"
	"fmt"
	"sort"
	"slices"
	"strconv"
	"strings"
	"sync"

	"zettelstore.de/c/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

type parseFunc func(string) (any, error)
type configDescription struct {
	text    string
	parse   parseFunc
	canList bool
194
195
196
197
198
199
200
201

202
203
204
205
206
207
208
197
198
199
200
201
202
203

204
205
206
207
208
209
210
211







-
+







				if val == nil {
					break
				}
				keys = append(keys, key)
			}
		}
	}
	sort.Strings(keys)
	slices.Sort(keys)
	return keys
}

func (cfg *srvConfig) Freeze() {
	cfg.mxConfig.Lock()
	cfg.frozen = true
	cfg.mxConfig.Unlock()
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
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







-
+
+

-
-

+



-
+
+

-
-

+






	case '0', 'f', 'F', 'n', 'N':
		return false, nil
	}
	return true, nil
}

func parseInt64(val string) (any, error) {
	if u64, err := strconv.ParseInt(val, 10, 64); err == nil {
	u64, err := strconv.ParseInt(val, 10, 64)
	if err == nil {
		return u64, nil
	} else {
		return nil, err
	}
	return nil, err
}

func parseZid(val string) (any, error) {
	if zid, err := id.Parse(val); err == nil {
	zid, err := id.Parse(val)
	if err == nil {
		return zid, nil
	} else {
		return id.Invalid, err
	}
	return id.Invalid, err
}

func parseInvalidZid(val string) (any, error) {
	zid, _ := id.Parse(val)
	return zid, nil
}

Changes to kernel/impl/core.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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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

import (
	"fmt"
	"net"
	"os"
	"runtime"
	"sync"
	"time"

	"zettelstore.de/c/maps"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/id"
)

type coreService struct {
	srvConfig
	started bool

	mxRecover  sync.RWMutex
77
78
79
80
81
82
83
84

85
86
87
88
89
90
91
80
81
82
83
84
85
86

87
88
89
90
91
92
93
94







-
+







	cs.next = interfaceMap{
		kernel.CoreDebug:     false,
		kernel.CoreGoArch:    runtime.GOARCH,
		kernel.CoreGoOS:      runtime.GOOS,
		kernel.CoreGoVersion: runtime.Version(),
		kernel.CoreHostname:  "*unknown host*",
		kernel.CorePort:      0,
		kernel.CoreStarted:   time.Now().Local().Format(id.ZidLayout),
		kernel.CoreStarted:   time.Now().Local().Format(id.TimestampLayout),
		kernel.CoreVerbose:   false,
	}
	if hn, err := os.Hostname(); err == nil {
		cs.next[kernel.CoreHostname] = hn
	}
}

Changes to kernel/impl/impl.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 the kernel implementation.
package impl

import (
	"errors"
23
24
25
26
27
28
29
30
31
32



33
34
35
36
37
38
39
26
27
28
29
30
31
32



33
34
35
36
37
38
39
40
41
42







-
-
-
+
+
+







	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"
	"time"

	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
)

// myKernel is the main internal kernel.
type myKernel struct {
	logWriter *kernelLogWriter
	logger    *logger.Logger
	wg        sync.WaitGroup
67
68
69
70
71
72
73
74

75
76
77
78
79
80
81
70
71
72
73
74
75
76

77
78
79
80
81
82
83
84







-
+







	srv    service
	srvnum kernel.Service
}
type serviceDependency map[kernel.Service][]kernel.Service

const (
	defaultNormalLogLevel = logger.InfoLevel
	defaultSimpleLogLevel = logger.WarnLevel
	defaultSimpleLogLevel = logger.ErrorLevel
)

// create a new kernel.
func init() {
	kernel.Main = createKernel()
}

95
96
97
98
99
100
101
102

103
104
105
106
107
108
109
98
99
100
101
102
103
104

105
106
107
108
109
110
111
112







-
+







		kernel.AuthService:   {&kern.auth, "auth", defaultNormalLogLevel},
		kernel.BoxService:    {&kern.box, "box", defaultNormalLogLevel},
		kernel.WebService:    {&kern.web, "web", defaultNormalLogLevel},
	}
	kern.srvNames = make(map[string]serviceData, len(kern.srvs))
	for key, srvD := range kern.srvs {
		if _, ok := kern.srvNames[srvD.name]; ok {
			kern.logger.Panic().Str("service", srvD.name).Msg("Service data already set")
			kern.logger.Error().Str("service", srvD.name).Msg("Service data already set, ignore")
		}
		kern.srvNames[srvD.name] = serviceData{srvD.srv, key}
		l := logger.New(lw, strings.ToUpper(srvD.name)).SetLevel(srvD.logLevel)
		kern.logger.Debug().Str("service", srvD.name).Msg("Initialize")
		srvD.srv.Initialize(l)
	}
	kern.depStart = serviceDependency{
122
123
124
125
126
127
128
129

130
131
132

133
134
135
136
137
138
139
125
126
127
128
129
130
131

132
133
134

135
136
137
138
139
140
141
142







-
+


-
+







	}
	return kern
}

func (kern *myKernel) Setup(progname, version string, versionTime time.Time) {
	kern.SetConfig(kernel.CoreService, kernel.CoreProgname, progname)
	kern.SetConfig(kernel.CoreService, kernel.CoreVersion, version)
	kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.ZidLayout))
	kern.SetConfig(kernel.CoreService, kernel.CoreVTime, versionTime.Local().Format(id.TimestampLayout))
}

func (kern *myKernel) Start(headline, lineServer bool) {
func (kern *myKernel) Start(headline, lineServer bool, configFilename string) {
	for _, srvD := range kern.srvs {
		srvD.srv.Freeze()
	}
	if kern.cfg.GetCurConfig(kernel.ConfigSimpleMode).(bool) {
		kern.SetLogLevel(defaultSimpleLogLevel.String())
	}
	kern.wg.Add(1)
156
157
158
159
160
161
162





163
164
165
166



167
168
169
170
171
172
173
159
160
161
162
163
164
165
166
167
168
169
170
171



172
173
174
175
176
177
178
179
180
181







+
+
+
+
+

-
-
-
+
+
+







			kern.core.GetCurConfig(kernel.CoreProgname),
			kern.core.GetCurConfig(kernel.CoreVersion),
			kern.core.GetCurConfig(kernel.CoreGoVersion),
			kern.core.GetCurConfig(kernel.CoreGoOS),
			kern.core.GetCurConfig(kernel.CoreGoArch),
		))
		logger.Mandatory().Msg("Licensed under the latest version of the EUPL (European Union Public License)")
		if configFilename != "" {
			logger.Mandatory().Str("filename", configFilename).Msg("Configuration file found")
		} else {
			logger.Mandatory().Msg("No configuration file found / used")
		}
		if kern.core.GetCurConfig(kernel.CoreDebug).(bool) {
			logger.Warn().Msg("----------------------------------------")
			logger.Warn().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
			logger.Warn().Msg("----------------------------------------")
			logger.Info().Msg("----------------------------------------")
			logger.Info().Msg("DEBUG MODE, DO NO USE THIS IN PRODUCTION")
			logger.Info().Msg("----------------------------------------")
		}
		if kern.auth.GetCurConfig(kernel.AuthReadonly).(bool) {
			logger.Info().Msg("Read-only mode")
		}
	}
	if lineServer {
		port := kern.core.GetNextConfig(kernel.CorePort).(int)
269
270
271
272
273
274
275
276

277
278
279
280
281
282
283
277
278
279
280
281
282
283

284
285
286
287
288
289
290
291







-
+








// LogRecover outputs some information about the previous panic.
func (kern *myKernel) LogRecover(name string, recoverInfo interface{}) bool {
	return kern.doLogRecover(name, recoverInfo)
}
func (kern *myKernel) doLogRecover(name string, recoverInfo interface{}) bool {
	stack := debug.Stack()
	kern.logger.Fatal().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name)
	kern.logger.Error().Str("recovered_from", fmt.Sprint(recoverInfo)).Bytes("stack", stack).Msg(name)
	kern.core.updateRecoverInfo(name, recoverInfo, stack)
	return true
}

// --- Profiling ---------------------------------------------------------

var errProfileInWork = errors.New("already profiling")
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
442
443
444
445
446
447
448

449
450
451

452
453
454

455
456
457
458

459
460
461
462
463
464
465







-
+


-
+


-
+



-







			return err
		}
		srv.SwitchNextToCur()
	}
	return nil
}

func (kern *myKernel) StopService(srvnum kernel.Service) error {
func (kern *myKernel) StopService(srvnum kernel.Service) {
	kern.mx.Lock()
	defer kern.mx.Unlock()
	return kern.doStopService(srvnum)
	kern.doStopService(srvnum)
}

func (kern *myKernel) doStopService(srvnum kernel.Service) error {
func (kern *myKernel) doStopService(srvnum kernel.Service) {
	for _, srv := range kern.sortDependency(srvnum, kern.depStop, false) {
		srv.Stop(kern)
	}
	return nil
}

func (kern *myKernel) sortDependency(
	srvnum kernel.Service,
	srvdeps serviceDependency,
	isStarted bool,
) []service {
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560














547
548
549
550
551
552
553














554
555
556
557
558
559
560
561
562
563
564
565
566
567







-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+

// --- The kernel as a service -------------------------------------------

type kernelService struct {
	kernel *myKernel
}

func (*kernelService) Initialize(*logger.Logger)                        {}
func (ks *kernelService) GetLogger() *logger.Logger                     { return ks.kernel.logger }
func (*kernelService) ConfigDescriptions() []serviceConfigDescription   { return nil }
func (*kernelService) SetConfig(key, value string) error                { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(key string) interface{}              { return nil }
func (*kernelService) GetNextConfig(key string) interface{}             { return nil }
func (*kernelService) GetCurConfigList(all bool) []kernel.KeyDescrValue { return nil }
func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue        { return nil }
func (*kernelService) GetStatistics() []kernel.KeyValue                 { return nil }
func (*kernelService) Freeze()                                          {}
func (*kernelService) Start(*myKernel) error                            { return nil }
func (*kernelService) SwitchNextToCur()                                 {}
func (*kernelService) IsStarted() bool                                  { return true }
func (*kernelService) Stop(*myKernel)                                   {}
func (*kernelService) Initialize(*logger.Logger)                      {}
func (ks *kernelService) GetLogger() *logger.Logger                   { return ks.kernel.logger }
func (*kernelService) ConfigDescriptions() []serviceConfigDescription { return nil }
func (*kernelService) SetConfig(string, string) error                 { return errAlreadyFrozen }
func (*kernelService) GetCurConfig(string) interface{}                { return nil }
func (*kernelService) GetNextConfig(string) interface{}               { return nil }
func (*kernelService) GetCurConfigList(bool) []kernel.KeyDescrValue   { return nil }
func (*kernelService) GetNextConfigList() []kernel.KeyDescrValue      { return nil }
func (*kernelService) GetStatistics() []kernel.KeyValue               { return nil }
func (*kernelService) Freeze()                                        {}
func (*kernelService) Start(*myKernel) error                          { return nil }
func (*kernelService) SwitchNextToCur()                               {}
func (*kernelService) IsStarted() bool                                { return true }
func (*kernelService) Stop(*myKernel)                                 {}

Changes to kernel/impl/log.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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

import (
	"os"
	"sync"
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
75
76
77
78
79
80
81



82
83
84
85
86
87
88







-
-
-







	}
	buf = append(buf, msg...)
	buf = append(buf, details...)
	buf = append(buf, '\n')
	_, err := os.Stdout.Write(buf)

	klw.mx.Unlock()
	if level == logger.PanicLevel {
		panic(err)
	}
	return err
}

func addTimestamp(buf *[]byte, ts time.Time) {
	year, month, day := ts.Date()
	itoa(buf, year, 4)
	*buf = append(*buf, '-')
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
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







-
+










-
+







	defer klw.mx.RUnlock()

	if !klw.full {
		if klw.writePos == 0 {
			return nil
		}
		result := make([]kernel.LogEntry, klw.writePos)
		for i := 0; i < klw.writePos; i++ {
		for i := range klw.writePos {
			copyE2E(&result[i], &klw.data[i])
		}
		return result
	}
	result := make([]kernel.LogEntry, cap(klw.data))
	pos := 0
	for j := klw.writePos; j < cap(klw.data); j++ {
		copyE2E(&result[pos], &klw.data[j])
		pos++
	}
	for j := 0; j < klw.writePos; j++ {
	for j := range klw.writePos {
		copyE2E(&result[pos], &klw.data[j])
		pos++
	}
	return result
}

func (klw *kernelLogWriter) getLastLogTime() time.Time {

Changes to kernel/impl/server.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
1

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

-
+






+
+
+












-
+










-
-
+
+








-
+










-
-
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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

import (
	"bufio"
	"net"
)

func startLineServer(kern *myKernel, listenAddr string) error {
	ln, err := net.Listen("tcp", listenAddr)
	if err != nil {
		kern.logger.Fatal().Err(err).Msg("Unable to start administration console")
		kern.logger.Error().Err(err).Msg("Unable to start administration console")
		return err
	}
	kern.logger.Mandatory().Str("listen", listenAddr).Msg("Start administration console")
	go func() { lineServer(ln, kern) }()
	return nil
}

func lineServer(ln net.Listener, kern *myKernel) {
	// Something may panic. Ensure a running line service.
	defer func() {
		if r := recover(); r != nil {
			kern.doLogRecover("Line", r)
		if ri := recover(); ri != nil {
			kern.doLogRecover("Line", ri)
			go lineServer(ln, kern)
		}
	}()

	for {
		conn, err := ln.Accept()
		if err != nil {
			// handle error
			kern.logger.IfErr(err).Msg("Unable to accept connection")
			kern.logger.Error().Err(err).Msg("Unable to accept connection")
			break
		}
		go handleLineConnection(conn, kern)
	}
	ln.Close()
}

func handleLineConnection(conn net.Conn, kern *myKernel) {
	// Something may panic. Ensure a running connection.
	defer func() {
		if r := recover(); r != nil {
			kern.doLogRecover("LineConn", r)
		if ri := recover(); ri != nil {
			kern.doLogRecover("LineConn", ri)
			go handleLineConnection(conn, kern)
		}
	}()

	kern.logger.Mandatory().Str("from", conn.RemoteAddr().String()).Msg("Start session on administration console")
	cmds := cmdSession{}
	cmds.initialize(conn, kern)

Changes to kernel/impl/web.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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

import (
	"errors"
	"net"
40
41
42
43
44
45
46

47

48
49
50
51

52
53
54
55
56
57
58
43
44
45
46
47
48
49
50

51
52


53
54
55
56
57
58
59
60
61







+
-
+

-
-

+







func (ws *webService) Initialize(logger *logger.Logger) {
	ws.logger = logger
	ws.descr = descriptionMap{
		kernel.WebAssetDir: {
			"Asset file  directory",
			func(val string) (any, error) {
				val = filepath.Clean(val)
				finfo, err := os.Stat(val)
				if finfo, err := os.Stat(val); err == nil && finfo.IsDir() {
				if err == nil && finfo.IsDir() {
					return val, nil
				} else {
					return nil, err
				}
				return nil, err
			},
			true,
		},
		kernel.WebBaseURL: {
			"Base URL",
			func(val string) (any, error) {
				if _, err := url.Parse(val); err != nil {
75
76
77
78
79
80
81

82
83
84
85
86
87
88
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92







+







					return "", err
				}
				return ap.String(), nil
			},
			true},
		kernel.WebMaxRequestSize:   {"Max Request Size", parseInt64, true},
		kernel.WebPersistentCookie: {"Persistent cookie", parseBool, true},
		kernel.WebProfiling:        {"Runtime profiling", parseBool, true},
		kernel.WebSecureCookie:     {"Secure cookie", parseBool, true},
		kernel.WebTokenLifetimeAPI: {
			"Token lifetime API",
			makeDurationParser(10*time.Minute, 0, 1*time.Hour),
			true,
		},
		kernel.WebTokenLifetimeHTML: {
104
105
106
107
108
109
110

111
112
113
114
115
116
117
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122







+







	ws.next = interfaceMap{
		kernel.WebAssetDir:          "",
		kernel.WebBaseURL:           "http://127.0.0.1:23123/",
		kernel.WebListenAddress:     "127.0.0.1:23123",
		kernel.WebMaxRequestSize:    int64(16 * 1024 * 1024),
		kernel.WebPersistentCookie:  false,
		kernel.WebSecureCookie:      true,
		kernel.WebProfiling:         false,
		kernel.WebTokenLifetimeAPI:  1 * time.Hour,
		kernel.WebTokenLifetimeHTML: 10 * time.Minute,
		kernel.WebURLPrefix:         "/",
	}
}

func makeDurationParser(defDur, minDur, maxDur time.Duration) parseFunc {
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
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







+






-
+





-
+


+
+
+
+
+
+
+
+
+
+
+
-
+


-
+






-
+








func (ws *webService) Start(kern *myKernel) error {
	baseURL := ws.GetNextConfig(kernel.WebBaseURL).(string)
	listenAddr := ws.GetNextConfig(kernel.WebListenAddress).(string)
	urlPrefix := ws.GetNextConfig(kernel.WebURLPrefix).(string)
	persistentCookie := ws.GetNextConfig(kernel.WebPersistentCookie).(bool)
	secureCookie := ws.GetNextConfig(kernel.WebSecureCookie).(bool)
	profile := ws.GetNextConfig(kernel.WebProfiling).(bool)
	maxRequestSize := ws.GetNextConfig(kernel.WebMaxRequestSize).(int64)
	if maxRequestSize < 1024 {
		maxRequestSize = 1024
	}

	if !strings.HasSuffix(baseURL, urlPrefix) {
		ws.logger.Fatal().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
		ws.logger.Error().Str("base-url", baseURL).Str("url-prefix", urlPrefix).Msg(
			"url-prefix is not a suffix of base-url")
		return errWrongBasePrefix
	}

	if lap := netip.MustParseAddrPort(listenAddr); !kern.auth.manager.WithAuth() && !lap.Addr().IsLoopback() {
		ws.logger.Warn().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
		ws.logger.Info().Str("listen", listenAddr).Msg("service may be reached from outside, but authentication is not enabled")
	}

	sd := impl.ServerData{
		Log:              ws.logger,
		ListenAddr:       listenAddr,
		BaseURL:          baseURL,
		URLPrefix:        urlPrefix,
		MaxRequestSize:   maxRequestSize,
		Auth:             kern.auth.manager,
		PersistentCookie: persistentCookie,
		SecureCookie:     secureCookie,
		Profiling:        profile,
	}
	srvw := impl.New(ws.logger, listenAddr, baseURL, urlPrefix, persistentCookie, secureCookie, maxRequestSize, kern.auth.manager)
	srvw := impl.New(sd)
	err := kern.web.setupServer(srvw, kern.box.manager, kern.auth.manager, &kern.cfg)
	if err != nil {
		ws.logger.Fatal().Err(err).Msg("Unable to create")
		ws.logger.Error().Err(err).Msg("Unable to create")
		return err
	}
	if kern.core.GetNextConfig(kernel.CoreDebug).(bool) {
		srvw.SetDebug()
	}
	if err = srvw.Run(); err != nil {
		ws.logger.Fatal().Err(err).Msg("Unable to start")
		ws.logger.Error().Err(err).Msg("Unable to start")
		return err
	}
	ws.logger.Info().Str("listen", listenAddr).Str("base-url", baseURL).Msg("Start Service")
	ws.mxService.Lock()
	ws.srvw = srvw
	ws.mxService.Unlock()

Changes to kernel/kernel.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24



25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44

-
+






+
+
+













-
-
-
+
+
+









-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 kernel provides the main kernel service.
package kernel

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

	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
)

// Kernel is the main internal service.
type Kernel interface {
	// Setup sets the most basic data of a software: its name, its version,
	// and when the version was created.
	Setup(progname, version string, versionTime time.Time)

	// Start the service.
	Start(headline bool, lineServer bool)
	Start(headline bool, lineServer bool, configFile string)

	// WaitForShutdown blocks the call until Shutdown is called.
	WaitForShutdown()

	// Shutdown the service. Waits for all concurrent activities to stop.
	Shutdown(silent bool)

87
88
89
90
91
92
93
94

95
96
97
98
99
100
101
90
91
92
93
94
95
96

97
98
99
100
101
102
103
104







-
+







	// StartService start the given service.
	StartService(Service) error

	// RestartService stops and restarts the given service, while maintaining service dependencies.
	RestartService(Service) error

	// StopService stop the given service.
	StopService(Service) error
	StopService(Service)

	// GetServiceStatistics returns a key/value list with statistical data.
	GetServiceStatistics(Service) []KeyValue

	// DumpIndex writes some data about the internal index into a writer.
	DumpIndex(io.Writer)

186
187
188
189
190
191
192

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







+








// Constants for web service keys.
const (
	WebAssetDir          = "asset-dir"
	WebBaseURL           = "base-url"
	WebListenAddress     = "listen"
	WebPersistentCookie  = "persistent"
	WebProfiling         = "profiling"
	WebMaxRequestSize    = "max-request-size"
	WebSecureCookie      = "secure"
	WebTokenLifetimeAPI  = "api-lifetime"
	WebTokenLifetimeHTML = "html-lifetime"
	WebURLPrefix         = "prefix"
)

Changes to logger/logger.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34

35

36


37
38
39
40
41
42
43
44

45

46


47
48
49
50
51
52
53
54

55

56


57
58
59
60
61
62
63

-
+






+
+
+












-
+










-

-

-
-








-

-

-
-








-

-

-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 logger implements a logging package for use in the Zettelstore.
package logger

import (
	"context"
	"strconv"
	"strings"
	"sync/atomic"
	"time"

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

// Level defines the possible log levels
type Level uint8

// Constants for Level
const (
	NoLevel        Level = iota // the absent log level
	TraceLevel                  // Log most internal activities
	DebugLevel                  // Log most data updates
	SenseLevel                  // Log activities of minor interest
	InfoLevel                   // Log normal activities
	WarnLevel                   // Log event that can be easily recovered
	ErrorLevel                  // Log (persistent) errors
	FatalLevel                  // Log event that cannot be recovered within an internal acitivty
	PanicLevel                  // Log event that must stop the software
	MandatoryLevel              // Log only mandatory events
	NeverLevel                  // Logging is disabled
)

var logLevel = [...]string{
	"     ",
	"TRACE",
	"DEBUG",
	"SENSE",
	"INFO ",
	"WARN ",
	"ERROR",
	"FATAL",
	"PANIC",
	">>>>>",
	"NEVER",
}

var strLevel = [...]string{
	"",
	"trace",
	"debug",
	"sense",
	"info",
	"warn",
	"error",
	"fatal",
	"panic",
	"mandatory",
	"disabled",
}

// IsValid returns true, if the level is a valid level
func (l Level) IsValid() bool { return TraceLevel <= l && l <= NeverLevel }

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
158
159
160
161
162
163
164



165
166
167



168
169
170














171
172
173
174
175
176
177







-
-
-



-
-
-



-
-
-
-
-
-
-
-
-
-
-
-
-
-








// Trace creates a tracing message.
func (l *Logger) Trace() *Message { return newMessage(l, TraceLevel) }

// Debug creates a debug message.
func (l *Logger) Debug() *Message { return newMessage(l, DebugLevel) }

// Sense creates a message suitable for sensing data.
func (l *Logger) Sense() *Message { return newMessage(l, SenseLevel) }

// Info creates a message suitable for information data.
func (l *Logger) Info() *Message { return newMessage(l, InfoLevel) }

// Warn creates a message suitable for warning the user.
func (l *Logger) Warn() *Message { return newMessage(l, WarnLevel) }

// Error creates a message suitable for errors.
func (l *Logger) Error() *Message { return newMessage(l, ErrorLevel) }

// IfErr creates an error message and sets the go error, if there is an error.
func (l *Logger) IfErr(err error) *Message {
	if err != nil {
		return newMessage(l, ErrorLevel).Err(err)
	}
	return nil
}

// Fatal creates a message suitable for fatal errors.
func (l *Logger) Fatal() *Message { return newMessage(l, FatalLevel) }

// Panic creates a message suitable for panicing.
func (l *Logger) Panic() *Message { return newMessage(l, PanicLevel) }

// Mandatory creates a message that will always logged, except when logging
// is disabled.
func (l *Logger) Mandatory() *Message { return newMessage(l, MandatoryLevel) }

// Clone creates a message to clone the logger.
func (l *Logger) Clone() *Message {
	msg := newMessage(l, NeverLevel)

Changes to logger/logger_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 logger_test

import (
	"fmt"
	"os"
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
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







-

-
-














-
+



















-
+






-
+






-
+



	testcases := []struct {
		text string
		exp  logger.Level
	}{
		{"tra", logger.TraceLevel},
		{"deb", logger.DebugLevel},
		{"info", logger.InfoLevel},
		{"warn", logger.WarnLevel},
		{"err", logger.ErrorLevel},
		{"fata", logger.FatalLevel},
		{"pan", logger.PanicLevel},
		{"manda", logger.MandatoryLevel},
		{"dis", logger.NeverLevel},
		{"d", logger.Level(0)},
	}
	for i, tc := range testcases {
		got := logger.ParseLevel(tc.text)
		if got != tc.exp {
			t.Errorf("%d: ParseLevel(%q) == %q, but got %q", i, tc.text, tc.exp, got)
		}
	}
}

func BenchmarkDisabled(b *testing.B) {
	log := logger.New(&stderrLogWriter{}, "").SetLevel(logger.NeverLevel)
	for n := 0; n < b.N; n++ {
	for range b.N {
		log.Info().Str("key", "val").Msg("Benchmark")
	}
}

type stderrLogWriter struct{}

func (*stderrLogWriter) WriteMessage(level logger.Level, ts time.Time, prefix, msg string, details []byte) error {
	fmt.Fprintf(os.Stderr, "%v %v %v %v %v\n", level.Format(), ts, prefix, msg, string(details))
	return nil
}

type testLogWriter struct{}

func (*testLogWriter) WriteMessage(logger.Level, time.Time, string, string, []byte) error {
	return nil
}

func BenchmarkStrMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "")
	for n := 0; n < b.N; n++ {
	for range b.N {
		log.Info().Str("key", "val").Msg("Benchmark")
	}
}

func BenchmarkMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "")
	for n := 0; n < b.N; n++ {
	for range b.N {
		log.Info().Msg("Benchmark")
	}
}

func BenchmarkCloneStrMessage(b *testing.B) {
	log := logger.New(&testLogWriter{}, "").Clone().Str("sss", "ttt").Child()
	for n := 0; n < b.N; n++ {
	for range b.N {
		log.Info().Msg("123456789")
	}
}

Changes to logger/message.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
1

2
3
4
5
6
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
// 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 logger

import (
	"context"
	"net/http"
	"strconv"
	"sync"

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

// Message presents a message to log.
type Message struct {
	logger *Logger
	level  Level
	buf    []byte

Changes to parser/blob/blob.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
1

2
3
4
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-2023 Detlef Stern
// 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 blob provides a parser of binary data.
package blob

import (
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxGif,
		AltNames:      nil,
		IsASTParser:   false,

Changes to parser/cleaner/cleaner.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

2
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
// 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 cleaner provides functions to clean up the parsed AST.
package cleaner

import (
	"bytes"
	"strconv"
	"strings"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/strfun"
)

// CleanBlockSlice cleans the given block list.
133
134
135
136
137
138
139
140
141


142
143
144
145

146
147
148
149
150
151
152
136
137
138
139
140
141
142


143
144
145
146
147

148
149
150
151
152
153
154
155







-
-
+
+



-
+







}

func (cv *cleanVisitor) visitHeading(hn *ast.HeadingNode) {
	if cv.doMark || hn == nil || len(hn.Inlines) == 0 {
		return
	}
	if hn.Slug == "" {
		var buf bytes.Buffer
		_, err := cv.textEnc.WriteInlines(&buf, &hn.Inlines)
		var sb strings.Builder
		_, err := cv.textEnc.WriteInlines(&sb, &hn.Inlines)
		if err != nil {
			return
		}
		hn.Slug = strfun.Slugify(buf.String())
		hn.Slug = strfun.Slugify(sb.String())
	}
	if hn.Slug != "" {
		hn.Fragment = cv.addIdentifier(hn.Slug, hn)
	}
}

func (cv *cleanVisitor) visitMark(mn *ast.MarkNode) {

Changes to parser/draw/canvas.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
1

2
3
4
5
6
7
8
9
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import (
	"bytes"
	"fmt"
	"image"
	"sort"
	"slices"
	"unicode/utf8"
)

// newCanvas returns a new Canvas, initialized from the provided data. If tabWidth is set to a non-negative
// value, that value will be used to convert tabs to spaces within the grid. Creation of the Canvas
// can fail if the diagram contains invalid UTF-8 sequences.
func newCanvas(data []byte) (*canvas, error) {
87
88
89
90
91
92
93
94

95
96
97
98
99
100

101
102

103
104
105
106
107
108
109
90
91
92
93
94
95
96

97
98
99
100
101
102

103
104

105
106
107
108
109
110
111
112







-
+





-
+

-
+







// size returns the visual dimensions of the Canvas.
func (c *canvas) size() image.Point { return c.siz }

// findObjects finds all objects (lines, polygons, and text) within the underlying grid.
func (c *canvas) findObjects() {
	c.findPaths()
	c.findTexts()
	sort.Sort(c.objs)
	slices.SortFunc(c.objs, compare)
}

// findPaths by starting with a point that wasn't yet visited, beginning at the top
// left of the grid.
func (c *canvas) findPaths() {
	for y := 0; y < c.siz.Y; y++ {
	for y := range c.siz.Y {
		p := point{y: y}
		for x := 0; x < c.siz.X; x++ {
		for x := range c.siz.X {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if !ch.isPathStart() {
				continue
123
124
125
126
127
128
129
130

131
132
133

134
135
136
137
138
139
140
126
127
128
129
130
131
132

133
134
135

136
137
138
139
140
141
142
143







-
+


-
+







			c.objs = append(c.objs, objs...)
		}
	}
}

// findTexts with a second pass through the grid attempts to identify any text within the grid.
func (c *canvas) findTexts() {
	for y := 0; y < c.siz.Y; y++ {
	for y := range c.siz.Y {
		p := point{}
		p.y = y
		for x := 0; x < c.siz.X; x++ {
		for x := range c.siz.X {
			p.x = x
			if c.isVisited(p) {
				continue
			}
			ch := c.at(p)
			if !ch.isTextStart() {
				continue

Changes to parser/draw/canvas_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
1

2
3
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import (
	"reflect"
	"strings"
654
655
656
657
658
659
660
661

662
663
664
665
666
667
668
657
658
659
660
661
662
663

664
665
666
667
668
669
670
671







-
+







		"             |     |       |         |   |",
		"             +-----+-------+---------+---+",
		"",
		"",
	}
	chunk := []byte(strings.Join(data, "\n"))
	input := make([]byte, 0, len(chunk)*b.N)
	for i := 0; i < b.N; i++ {
	for range b.N {
		input = append(input, chunk...)
	}
	expected := 30 * b.N
	b.ResetTimer()
	c, err := newCanvas(input)
	if err != nil {
		b.Fatalf("Error creating canvas: %s", err)

Changes to parser/draw/char.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
1

2
3
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import "unicode"

type char rune

Changes to parser/draw/draw.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22





23
24
25
26
27
28
29
30
31
32
33
34

-
+






+
+
+











-
-
-
-
-
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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 draw provides a parser to create SVG from ASCII drawing.
//
// It is not a parser registered by the general parser framework (directed by
// metadata "syntax" of a zettel). It will be used when a zettel is evaluated.
package draw

import (
	"strconv"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxDraw,
		AltNames:      []string{},
		IsASTParser:   true,
121
122
123
124
125
126
127
128

129
130
131
132

133
124
125
126
127
128
129
130

131
132
133
134

135
136







-
+



-
+

			return n
		}
	}
	return defVal
}

func canvasErrMsg(err error) ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("Error:", err.Error())
	return ast.InlineSlice{&ast.TextNode{Text: "Error: " + err.Error()}}
}

func noSVGErrMsg() ast.InlineSlice {
	return ast.CreateInlineSliceFromWords("NO", "IMAGE")
	return ast.InlineSlice{&ast.TextNode{Text: "NO IMAGE"}}
}

Changes to parser/draw/draw_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
1

2
3
4
5
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) 2022 Detlef Stern
// 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 draw_test

import (
	"testing"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.ParseBlocks(inp, nil, meta.SyntaxDraw, config.NoHTML)

Changes to parser/draw/object.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
1

2
3
4
5
6
7
8
9
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import "fmt"
import (
	"cmp"
	"fmt"
)

// object represents one of an open path, a closed path, or text.
type object struct {
	// points always starts with the top most, then left most point, proceeding to the right.
	points   []point
	text     []rune
	corners  []point
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
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







-
-
-
+
-
-


-
-



-
+




-
+

-
+







		o.text[i] = rune(ch)
	}
}

// objects implements a sortable collection of Object interfaces.
type objects []*object

func (o objects) Len() int      { return len(o) }
func (o objects) Swap(i, j int) { o[i], o[j] = o[j], o[i] }

func compare(l, r *object) int {
// Less returns in order top most, then left most.
func (o objects) Less(i, j int) bool {
	// TODO(dhobsd): This doesn't catch every z-index case we could possibly want. We should
	// support z-indexing of objects through an a2s tag.
	l := o[i]
	r := o[j]
	lt := l.isJustText()
	rt := r.isJustText()
	if lt != rt {
		return rt
		return 1
	}
	lp := l.Points()[0]
	rp := r.Points()[0]
	if lp.y != rp.y {
		return lp.y < rp.y
		return cmp.Compare(lp.y, rp.y)
	}
	return lp.x < rp.x
	return cmp.Compare(lp.x, rp.x)
}

const (
	dirNone = iota // No directionality
	dirH           // Horizontal
	dirV           // Vertical
	dirSE          // South-East

Changes to parser/draw/point.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
1

2
3
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import "fmt"

// A renderHint suggests ways the SVG renderer may appropriately represent this point.

Changes to parser/draw/svg.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
1

2
3
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import (
	"bytes"
	"fmt"
159
160
161
162
163
164
165
166
167
168



169
170
171
172
173
174
175
162
163
164
165
166
167
168



169
170
171
172
173
174
175
176
177
178







-
-
-
+
+
+







	}
	if !first {
		io.WriteString(w, "</g>")
	}
}

func escape(s string) string {
	b := bytes.Buffer{}
	strfun.XMLEscape(&b, s)
	return b.String()
	var sb strings.Builder
	strfun.XMLEscape(&sb, s)
	return sb.String()
}

type scaledPoint struct {
	X    float64
	Y    float64
	Hint renderHint
}

Changes to parser/draw/svg_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
1

2
3
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) 2022 Detlef Stern
// 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.
//
// This file was originally created by the ASCIIToSVG contributors under an MIT
// license, but later changed to fulfil the needs of Zettelstore. The following
// statements affects the original code as found on
// https://github.com/asciitosvg/asciitosvg (Commit:
// ca82a5ce41e2190a05e07af6e8b3ea4e3256a283, 2020-11-20):
//
// Copyright 2012 - 2018 The ASCIIToSVG Contributors
// All rights reserved.
//
// SPDX-License-Identifier: EUPL-1.2
// SPDX-FileCopyrightText: 2022-present Detlef Stern
//-----------------------------------------------------------------------------

package draw

import (
	"strings"
	"testing"

Changes to parser/markdown/markdown.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
1

2
3
4
5
6
7
8
9
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) 2020-2022 Detlef Stern
// 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 markdown provides a parser for markdown.
package markdown

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"

	gm "github.com/yuin/goldmark"
	gmAst "github.com/yuin/goldmark/ast"
	gmText "github.com/yuin/goldmark/text"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxMarkdown,
		AltNames:      []string{meta.SyntaxMD},
		IsASTParser:   true,
144
145
146
147
148
149
150
151

152
153
154
155
156
157
158
148
149
150
151
152
153
154

155
156
157
158
159
160
161
162







-
+







		Content: p.acceptRawText(node),
	}
}

func (p *mdP) acceptRawText(node gmAst.Node) []byte {
	lines := node.Lines()
	result := make([]byte, 0, 512)
	for i := 0; i < lines.Len(); i++ {
	for i := range lines.Len() {
		s := lines.At(i)
		line := s.Value(p.source)
		if l := len(line); l > 0 {
			if l > 1 && line[l-2] == '\r' && line[l-1] == '\n' {
				line = line[0 : l-2]
			} else if line[l-1] == '\n' || line[l-1] == '\r' {
				line = line[0 : l-1]
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
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







+
+
+
+

-
+

-
-
+
-
-
+
-
-
-
+
-





-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-













-
+







-
-
+
+






-
-
+
+





-
+

-
+



+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+




-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







		return p.acceptRawHTML(n)
	}
	panic(fmt.Sprintf("Unhandled inline node %v", node.Kind()))
}

func (p *mdP) acceptText(node *gmAst.Text) ast.InlineSlice {
	segment := node.Segment
	text := segment.Value(p.source)
	if text == nil {
		return nil
	}
	if node.IsRaw() {
		return splitText(string(segment.Value(p.source)))
		return ast.InlineSlice{&ast.TextNode{Text: string(text)}}
	}
	ins := splitText(string(segment.Value(p.source)))
	result := make(ast.InlineSlice, 0, len(ins)+1)
	result := make(ast.InlineSlice, 0, 2)
	for _, in := range ins {
		if tn, ok := in.(*ast.TextNode); ok {
	in := &ast.TextNode{Text: cleanText(text, true)}
			tn.Text = cleanText([]byte(tn.Text), true)
		}
		result = append(result, in)
	result = append(result, in)
	}
	if node.HardLineBreak() {
		result = append(result, &ast.BreakNode{Hard: true})
	} else if node.SoftLineBreak() {
		result = append(result, &ast.BreakNode{Hard: false})
	}
	return result
}

// splitText transform the text into a sequence of TextNode and SpaceNode
func splitText(text string) ast.InlineSlice {
	if text == "" {
		return nil
	}
	result := make(ast.InlineSlice, 0, 1)

	state := 0 // 0=unknown,1=non-spaces,2=spaces
	lastPos := 0
	for pos, ch := range text {
		if input.IsSpace(ch) {
			if state == 1 {
				result = append(result, &ast.TextNode{Text: text[lastPos:pos]})
				lastPos = pos
			}
			state = 2
		} else {
			if state == 2 {
				result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:pos]})
				lastPos = pos
			}
			state = 1
		}
	}
	switch state {
	case 1:
		result = append(result, &ast.TextNode{Text: text[lastPos:]})
	case 2:
		result = append(result, &ast.SpaceNode{Lexeme: text[lastPos:]})
	default:
		panic(fmt.Sprintf("Unexpected state %v", state))
	}
	return result
}

var ignoreAfterBS = map[byte]struct{}{
	'!': {}, '"': {}, '#': {}, '$': {}, '%': {}, '&': {}, '\'': {}, '(': {},
	')': {}, '*': {}, '+': {}, ',': {}, '-': {}, '.': {}, '/': {}, ':': {},
	';': {}, '<': {}, '=': {}, '>': {}, '?': {}, '@': {}, '[': {}, '\\': {},
	']': {}, '^': {}, '_': {}, '`': {}, '{': {}, '|': {}, '}': {}, '~': {},
}

// cleanText removes backslashes from TextNodes and expands entities
func cleanText(text []byte, cleanBS bool) string {
	lastPos := 0
	var buf bytes.Buffer
	var sb strings.Builder
	for pos, ch := range text {
		if pos < lastPos {
			continue
		}
		if ch == '&' {
			inp := input.NewInput([]byte(text[pos:]))
			if s, ok := inp.ScanEntity(); ok {
				buf.Write(text[lastPos:pos])
				buf.WriteString(s)
				sb.Write(text[lastPos:pos])
				sb.WriteString(s)
				lastPos = pos + inp.Pos
			}
			continue
		}
		if cleanBS && ch == '\\' && pos < len(text)-1 {
			if _, found := ignoreAfterBS[text[pos+1]]; found {
				buf.Write(text[lastPos:pos])
				buf.WriteByte(text[pos+1])
				sb.Write(text[lastPos:pos])
				sb.WriteByte(text[pos+1])
				lastPos = pos + 2
			}
		}
	}
	if lastPos < len(text) {
		buf.Write(text[lastPos:])
		sb.Write(text[lastPos:])
	}
	return buf.String()
	return sb.String()
}

func (p *mdP) acceptCodeSpan(node *gmAst.CodeSpan) ast.InlineSlice {
	var segBuf bytes.Buffer
	for c := node.FirstChild(); c != nil; c = c.NextSibling() {
		segment := c.(*gmAst.Text).Segment
		segBuf.Write(segment.Value(p.source))
	}
	content := segBuf.Bytes()

	// Clean code span
	if len(content) == 0 {
		content = nil
	} else {
		lastPos := 0
		var buf bytes.Buffer
		for pos, ch := range content {
			if ch == '\n' {
				buf.Write(content[lastPos:pos])
				if pos < len(content)-1 {
					buf.WriteByte(' ')
				}
				lastPos = pos + 1
			}
		}
		buf.Write(content[lastPos:])
		content = buf.Bytes()
	}

	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralProg,
			Attrs:   nil, //TODO
			Content: cleanCodeSpan(node.Text(p.source)),
			Content: content,
		},
	}
}

func cleanCodeSpan(text []byte) []byte {
	if len(text) == 0 {
		return nil
	}
	lastPos := 0
	var buf bytes.Buffer
	for pos, ch := range text {
		if ch == '\n' {
			buf.Write(text[lastPos:pos])
			if pos < len(text)-1 {
				buf.WriteByte(' ')
			}
			lastPos = pos + 1
		}
	}
	buf.Write(text[lastPos:])
	return buf.Bytes()
}

func (p *mdP) acceptEmphasis(node *gmAst.Emphasis) ast.InlineSlice {
	kind := ast.FormatEmph
	if node.Level == 2 {
		kind = ast.FormatStrong
	}
	return ast.InlineSlice{
		&ast.FormatNode{
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
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







-
-
+
+



-
+


-
+



















-
+











			Attrs:   a,
		},
	}
}

func (p *mdP) flattenInlineSlice(node gmAst.Node) ast.InlineSlice {
	is := p.acceptInlineChildren(node)
	var buf bytes.Buffer
	_, err := p.textEnc.WriteInlines(&buf, &is)
	var sb strings.Builder
	_, err := p.textEnc.WriteInlines(&sb, &is)
	if err != nil {
		panic(err)
	}
	if buf.Len() == 0 {
	if sb.Len() == 0 {
		return nil
	}
	return ast.InlineSlice{&ast.TextNode{Text: buf.String()}}
	return ast.InlineSlice{&ast.TextNode{Text: sb.String()}}
}

func (p *mdP) acceptAutoLink(node *gmAst.AutoLink) ast.InlineSlice {
	u := node.URL(p.source)
	if node.AutoLinkType == gmAst.AutoLinkEmail &&
		!bytes.HasPrefix(bytes.ToLower(u), []byte("mailto:")) {
		u = append([]byte("mailto:"), u...)
	}
	return ast.InlineSlice{
		&ast.LinkNode{
			Ref:     ast.ParseReference(cleanText(u, false)),
			Inlines: nil,
			Attrs:   nil, // TODO
		},
	}
}

func (p *mdP) acceptRawHTML(node *gmAst.RawHTML) ast.InlineSlice {
	segs := make([][]byte, 0, node.Segments.Len())
	for i := 0; i < node.Segments.Len(); i++ {
	for i := range node.Segments.Len() {
		segment := node.Segments.At(i)
		segs = append(segs, segment.Value(p.source))
	}
	return ast.InlineSlice{
		&ast.LiteralNode{
			Kind:    ast.LiteralHTML,
			Attrs:   nil, // TODO: add HTML as language
			Content: bytes.Join(segs, nil),
		},
	}
}

Deleted parser/markdown/markdown_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




















































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

import (
	"bytes"
	"testing"

	"zettelstore.de/z/ast"
)

func TestSplitText(t *testing.T) {
	t.Parallel()
	var testcases = []struct {
		text string
		exp  string
	}{
		{"", ""},
		{"abc", "Tabc"},
		{" ", "S "},
		{"abc def", "TabcS Tdef"},
		{"abc def ", "TabcS TdefS "},
		{" abc def ", "S TabcS TdefS "},
	}
	for i, tc := range testcases {
		var buf bytes.Buffer
		for _, in := range splitText(tc.text) {
			switch n := in.(type) {
			case *ast.TextNode:
				buf.WriteByte('T')
				buf.WriteString(n.Text)
			case *ast.SpaceNode:
				buf.WriteByte('S')
				buf.WriteString(n.Lexeme)
			default:
				buf.WriteByte('Q')
			}
		}
		got := buf.String()
		if tc.exp != got {
			t.Errorf("TC=%d, text=%q, exp=%q, got=%q", i, tc.text, tc.exp, got)
		}
	}
}

Changes to parser/none/none.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
1

2
3
4
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
// 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 none provides a none-parser, e.g. for zettel with just metadata.
package none

import (
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxNone,
		AltNames:      []string{},
		IsASTParser:   false,

Changes to parser/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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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 parser provides a generic interface to a range of different parsers.
package parser

import (
	"context"
	"fmt"
	"strings"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser/cleaner"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/parser/cleaner"
)

// Info describes a single parser.
//
// Before ParseBlocks() or ParseInlines() is called, ensure the input stream to
// be valid. This can ce achieved on calling inp.Next() after the input stream
// was created.
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
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







-
-
-
-
-
-
-
-
-











-
-
-

















-
-
-
+
+
+
-
-
-
+


+
+
+

+




-
+
-
-
+
+
+

-
-
+
+

-
+



-
+






-
+















-
+



	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsASTParser
}

// IsTextFormat returns whether the given syntax is known to be a text format.
func IsTextFormat(syntax string) bool {
	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsTextFormat
}

// IsImageFormat returns whether the given syntax is known to be an image format.
func IsImageFormat(syntax string) bool {
	pi, ok := registry[syntax]
	if !ok {
		return false
	}
	return pi.IsImageFormat
}

// ParseBlocks parses some input and returns a slice of block nodes.
func ParseBlocks(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parseBlocksAndClean(inp, m, syntax, hi)
}
func parseBlocksAndClean(inp *input.Input, m *meta.Meta, syntax string, hi config.HTMLInsecurity) ast.BlockSlice {
	bs := Get(syntax).ParseBlocks(inp, m, syntax)
	cleaner.CleanBlockSlice(&bs, hi.AllowHTML(syntax))
	return bs
}

// ParseInlines parses some input and returns a slice of inline nodes.
func ParseInlines(inp *input.Input, syntax string) ast.InlineSlice {
	// Do not clean, because we don't know the context where this function will be called.
	return Get(syntax).ParseInlines(inp, syntax)
}

// ParseMetadata parses a string as Zettelmarkup, resulting in an inline slice.
// Typically used to parse the title or other metadata of type Zettelmarkup.
func ParseMetadata(value string) ast.InlineSlice {
	return ParseInlines(input.NewInput([]byte(value)), meta.SyntaxZmk)
}

// ParseMetadataNoLink parses a string as Zettelmarkup, resulting in an inline slice.
// All link and footnote nodes will be removed.
func ParseMetadataNoLink(value string) ast.InlineSlice {
// ParseSpacedText returns an inline slice that consists just of test and space node.
// No Zettelmarkup parsing is done. It is typically used to transform the zettel title into an inline slice.
func ParseSpacedText(s string) ast.InlineSlice {
	in := ParseMetadata(value)
	cleaner.CleanInlineLinks(&in)
	return in
	return ast.InlineSlice{&ast.TextNode{Text: strings.Join(meta.ListFromValue(s), " ")}}
}

// NormalizedSpacedText returns the given string, but normalize multiple spaces to one space.
func NormalizedSpacedText(s string) string { return strings.Join(meta.ListFromValue(s), " ") }

// ParseDescription returns a suitable description stored in the metadata as an inline slice.
// This is done for an image in most cases.
func ParseDescription(m *meta.Meta) ast.InlineSlice {
	if m == nil {
		return nil
	}
	descr, found := m.Get(api.KeySummary)
	if descr, found := m.Get(api.KeySummary); found {
	if !found {
		descr, found = m.Get(api.KeyTitle)
		in := ParseMetadata(descr)
		cleaner.CleanInlineLinks(&in)
		return in
	}
	if !found {
		return nil
	if title, found := m.Get(api.KeyTitle); found {
		return ParseSpacedText(title)
	}
	return ParseMetadataNoLink(descr)
	return ast.InlineSlice{&ast.TextNode{Text: "Zettel without title: " + m.Zid.String()}}
}

// ParseZettel parses the zettel based on the syntax.
func ParseZettel(ctx context.Context, zettel domain.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
func ParseZettel(ctx context.Context, zettel zettel.Zettel, syntax string, rtConfig config.Config) *ast.ZettelNode {
	m := zettel.Meta
	inhMeta := m
	if rtConfig != nil {
		inhMeta = rtConfig.AddDefaultValues(ctx, inhMeta)
	}
	if syntax == "" {
		syntax, _ = inhMeta.Get(api.KeySyntax)
		syntax = inhMeta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
	}
	parseMeta := inhMeta
	if syntax == meta.SyntaxNone {
		parseMeta = m
	}

	hi := config.NoHTML
	if rtConfig != nil {
		hi = rtConfig.GetHTMLInsecurity()
	}
	return &ast.ZettelNode{
		Meta:    m,
		Content: zettel.Content,
		Zid:     m.Zid,
		InhMeta: inhMeta,
		Ast:     parseBlocksAndClean(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi),
		Ast:     ParseBlocks(input.NewInput(zettel.Content.AsBytes()), parseMeta, syntax, hi),
		Syntax:  syntax,
	}
}

Changes to parser/parser_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
1

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

-
+






+
+
+







-
-
-
+
+
+














-


-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+






-
-
-








//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 parser_test

import (
	"testing"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/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 TestParserType(t *testing.T) {
	syntaxSet := strfun.NewSet(parser.GetSyntaxes()...)
	testCases := []struct {
		syntax string
		ast    bool
		text   bool
		image  bool
	}{
		{meta.SyntaxHTML, false, true, false},
		{meta.SyntaxCSS, false, true, false},
		{meta.SyntaxDraw, true, true, false},
		{meta.SyntaxGif, false, false, true},
		{meta.SyntaxJPEG, false, false, true},
		{meta.SyntaxJPG, false, false, true},
		{meta.SyntaxMarkdown, true, true, false},
		{meta.SyntaxMD, true, true, false},
		{meta.SyntaxHTML, false, false},
		{meta.SyntaxCSS, false, false},
		{meta.SyntaxDraw, true, false},
		{meta.SyntaxGif, false, true},
		{meta.SyntaxJPEG, false, true},
		{meta.SyntaxJPG, false, true},
		{meta.SyntaxMarkdown, true, false},
		{meta.SyntaxMD, true, false},
		{meta.SyntaxMustache, false, true, false},
		{meta.SyntaxNone, false, false, false},
		{meta.SyntaxPlain, false, true, false},
		{meta.SyntaxPNG, false, false, true},
		{meta.SyntaxSVG, false, true, true},
		{meta.SyntaxText, false, true, false},
		{meta.SyntaxTxt, false, true, false},
		{meta.SyntaxWebp, false, false, true},
		{meta.SyntaxZmk, true, true, false},
		{meta.SyntaxNone, false, false},
		{meta.SyntaxPlain, false, false},
		{meta.SyntaxPNG, false, true},
		{meta.SyntaxSVG, false, true},
		{meta.SyntaxSxn, false, false},
		{meta.SyntaxText, false, false},
		{meta.SyntaxTxt, false, false},
		{meta.SyntaxWebp, false, true},
		{meta.SyntaxZmk, true, false},
	}
	for _, tc := range testCases {
		delete(syntaxSet, tc.syntax)
		if got := parser.IsASTParser(tc.syntax); got != tc.ast {
			t.Errorf("Syntax %q is AST: %v, but got %v", tc.syntax, tc.ast, got)
		}
		if got := parser.IsTextFormat(tc.syntax); got != tc.text {
			t.Errorf("Syntax %q is text: %v, but got %v", tc.syntax, tc.text, got)
		}
		if got := parser.IsImageFormat(tc.syntax); got != tc.image {
			t.Errorf("Syntax %q is image: %v, but got %v", tc.syntax, tc.image, got)
		}
	}
	for syntax := range syntaxSet {
		t.Errorf("Forgot to test syntax %q", syntax)
	}
}

Changes to parser/plain/plain.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
1

2
3
4
5
6
7
8
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
// 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 plain provides a parser for plain text data.
package plain

import (
	"strings"
	"bytes"

	"t73f.de/r/sx/sxreader"
	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxTxt,
		AltNames:      []string{meta.SyntaxPlain, meta.SyntaxText},
		IsASTParser:   false,
55
56
57
58
59
60
61
62

63
64
65
66
67
68


69
70
71
72
73
74
75
59
60
61
62
63
64
65

66
67
68
69
70


71
72
73
74
75
76
77
78
79







-
+




-
-
+
+







		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: true,
		ParseBlocks:   parseSVGBlocks,
		ParseInlines:  parseSVGInlines,
	})
	parser.Register(&parser.Info{
		Name:          meta.SyntaxMustache,
		Name:          meta.SyntaxSxn,
		AltNames:      []string{},
		IsASTParser:   false,
		IsTextFormat:  true,
		IsImageFormat: false,
		ParseBlocks:   parseBlocks,
		ParseInlines:  parseInlines,
		ParseBlocks:   parseSxnBlocks,
		ParseInlines:  parseSxnInlines,
	})
}

func parseBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	return doParseBlocks(inp, syntax, ast.VerbatimProg)
}
func parseBlocksHTML(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
116
117
118
119
120
121
122
123
124




125



126

127
128
129
130
131




















132









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







-
-
+
+
+
+

+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
	return ast.InlineSlice{&ast.EmbedBLOBNode{
		Blob:   []byte(svgSrc),
		Syntax: syntax,
	}}
}

func scanSVG(inp *input.Input) string {
	for input.IsSpace(inp.Ch) {
		inp.Next()
	inp.SkipSpace()
	pos := inp.Pos
	if !inp.Accept("<svg") {
		return ""
	}
	ch := inp.Ch
	if input.IsSpace(ch) || input.IsEOLEOS(ch) || ch == '>' {
		// TODO: check proper end </svg>
	svgSrc := string(inp.Src[inp.Pos:])
		return string(inp.Src[pos:])
	if !strings.HasPrefix(svgSrc, "<svg ") {
		return ""
	}
	// TODO: check proper end </svg>
	return svgSrc
	}
	return ""
}

func parseSxnBlocks(inp *input.Input, _ *meta.Meta, syntax string) ast.BlockSlice {
	rd := sxreader.MakeReader(bytes.NewReader(inp.Src))
	_, err := rd.ReadAll()
	result := ast.BlockSlice{
		&ast.VerbatimNode{
			Kind:    ast.VerbatimProg,
			Attrs:   attrs.Attributes{"": syntax},
			Content: inp.ScanLineContent(),
		},
	}
	if err != nil {
		result = append(result, ast.CreateParaNode(&ast.TextNode{
			Text: err.Error(),
		}))
	}
	return result
}

func parseSxnInlines(inp *input.Input, syntax string) ast.InlineSlice {
	inp.SkipToEOL()
	return ast.InlineSlice{&ast.LiteralNode{
		Kind:    ast.LiteralProg,
		Attrs:   attrs.Attributes{"": syntax},
		Content: append([]byte(nil), inp.Src[0:inp.Pos]...),
	}}
}

Added parser/plain/plain_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 plain_test

import (
	"testing"

	"t73f.de/r/zsc/input"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func TestParseSVG(t *testing.T) {
	testCases := []struct {
		name string
		src  string
		exp  string
	}{
		{"common", " <svg bla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg bla\"))"},
		{"inkscape", "<svg\nbla", "(INLINE (EMBED-BLOB () \"svg\" \"<svg\\nbla\"))"},
		{"selfmade", "<svg>", "(INLINE (EMBED-BLOB () \"svg\" \"<svg>\"))"},
		{"error", "<svgbla", "(INLINE)"},
		{"error-", "<svg-bla", "(INLINE)"},
		{"error#", "<svg2bla", "(INLINE)"},
	}
	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			inp := input.NewInput([]byte(tc.src))
			is := parser.ParseInlines(inp, meta.SyntaxSVG)
			trans := szenc.NewTransformer()
			lst := trans.GetSz(&is)
			if got := lst.String(); tc.exp != got {
				t.Errorf("\nexp: %q\ngot: %q", tc.exp, got)
			}
		})
	}
}

Changes to parser/zettelmark/block.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
1

2
3
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
// 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 zettelmark

import (
	"fmt"

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

// parseBlockSlice parses a sequence of blocks.
func (cp *zmkP) parseBlockSlice() ast.BlockSlice {
	inp := cp.inp
	var lastPara *ast.ParaNode
	bs := ast.BlockSlice{}
107
108
109
110
111
112
113
114
115






116






117
118
119
120
121
122
123
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







-

+
+
+
+
+
+
-
+
+
+
+
+
+







}

func startsWithSpaceSoftBreak(pn *ast.ParaNode) bool {
	ins := pn.Inlines
	if len(ins) < 2 {
		return false
	}
	_, isSpace := ins[0].(*ast.SpaceNode)
	_, isBreak := ins[1].(*ast.BreakNode)
	return isBreak && isSpaceText(ins[0])
}
func isSpaceText(node ast.InlineNode) bool {
	if tn, isText := node.(*ast.TextNode); isText {
		for _, ch := range tn.Text {
			if !input.IsSpace(ch) {
	return isSpace && isBreak
				return false
			}
		}
		return true
	}
	return false
}

func (cp *zmkP) cleanupListsAfterEOL() {
	for _, l := range cp.lists {
		if lits := len(l.Items); lits > 0 {
			l.Items[lits-1] = append(l.Items[lits-1], &nullItemNode{})
		}
271
272
273
274
275
276
277

278
279

280
281

282
283
284
285
286
287
288
284
285
286
287
288
289
290
291
292

293
294

295
296
297
298
299
300
301
302







+

-
+

-
+







		if !cont {
			lastPara, _ = bn.(*ast.ParaNode)
		}
	}
}

func (cp *zmkP) parseRegionLastLine(rn *ast.RegionNode) {
	inp := cp.inp
	cp.clearStacked() // remove any lists defined in the region
	cp.skipSpace()
	inp.SkipSpace()
	for {
		switch cp.inp.Ch {
		switch inp.Ch {
		case input.EOS, '\n', '\r':
			return
		}
		in := cp.parseInline()
		if in == nil {
			return
		}
297
298
299
300
301
302
303
304

305
306
307
308
309
310
311
311
312
313
314
315
316
317

318
319
320
321
322
323
324
325







-
+







	if delims < 3 {
		return nil, false
	}
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	inp.SkipSpace()
	if delims > 7 {
		delims = 7
	}
	hn = &ast.HeadingNode{Level: delims - 2, Inlines: nil}
	for {
		if input.IsEOLEOS(inp.Ch) {
			return hn, true
344
345
346
347
348
349
350
351

352
353
354
355
356
357
358
358
359
360
361
362
363
364

365
366
367
368
369
370
371
372







-
+







// parseNestedList parses a list.
func (cp *zmkP) parseNestedList() (res ast.BlockNode, success bool) {
	inp := cp.inp
	kinds := cp.parseNestedListKinds()
	if kinds == nil {
		return nil, false
	}
	cp.skipSpace()
	inp.SkipSpace()
	if kinds[len(kinds)-1] != ast.NestedListQuote && input.IsEOLEOS(inp.Ch) {
		return nil, false
	}

	if len(kinds) < len(cp.lists) {
		cp.lists = cp.lists[:len(kinds)]
	}
404
405
406
407
408
409
410
411

412
413
414
415
416
417
418
418
419
420
421
422
423
424

425
426
427
428
429
430
431
432







-
+







		}
	}
	return ln, newLnCount
}

func (cp *zmkP) cleanupParsedNestedList(newLnCount int) (res ast.BlockNode, success bool) {
	listDepth := len(cp.lists)
	for i := 0; i < newLnCount; i++ {
	for i := range newLnCount {
		childPos := listDepth - i - 1
		parentPos := childPos - 1
		if parentPos < 0 {
			return cp.lists[0], true
		}
		if prevItems := cp.lists[parentPos].Items; len(prevItems) > 0 {
			lastItem := len(prevItems) - 1
429
430
431
432
433
434
435
436

437
438
439
440
441
442
443
443
444
445
446
447
448
449

450
451
452
453
454
455
456
457







-
+







func (cp *zmkP) parseDefTerm() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	inp.SkipSpace()
	descrl := cp.descrl
	if descrl == nil {
		descrl = &ast.DescriptionListNode{}
		cp.descrl = descrl
	}
	descrl.Descriptions = append(descrl.Descriptions, ast.Description{})
	defPos := len(descrl.Descriptions) - 1
463
464
465
466
467
468
469
470

471
472
473
474
475
476
477
477
478
479
480
481
482
483

484
485
486
487
488
489
490
491







-
+







func (cp *zmkP) parseDefDescr() (res ast.BlockNode, success bool) {
	inp := cp.inp
	inp.Next()
	if inp.Ch != ' ' {
		return nil, false
	}
	inp.Next()
	cp.skipSpace()
	inp.SkipSpace()
	descrl := cp.descrl
	if descrl == nil || len(descrl.Descriptions) == 0 {
		return nil, false
	}
	defPos := len(descrl.Descriptions) - 1
	if len(descrl.Descriptions[defPos].Term) == 0 {
		return nil, false

Changes to parser/zettelmark/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
1

2
3
4
5
6
7
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
// 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 zettelmark

import (
	"bytes"
	"fmt"
	"strings"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/zettel/meta"
)

// parseInlineSlice parses a sequence of Inlines until EOS.
func (cp *zmkP) parseInlineSlice() (ins ast.InlineSlice) {
	inp := cp.inp
	for inp.Ch != input.EOS {
		in := cp.parseInline()
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
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







-
-



















-
+







		var in ast.InlineNode
		success := false
		switch inp.Ch {
		case input.EOS:
			return nil
		case '\n', '\r':
			return cp.parseSoftBreak()
		case ' ', '\t':
			return cp.parseSpace()
		case '[':
			inp.Next()
			switch inp.Ch {
			case '[':
				in, success = cp.parseLink()
			case '@':
				in, success = cp.parseCite()
			case '^':
				in, success = cp.parseFootnote()
			case '!':
				in, success = cp.parseMark()
			}
		case '{':
			inp.Next()
			if inp.Ch == '{' {
				in, success = cp.parseEmbed()
			}
		case '%':
			in, success = cp.parseComment()
		case '_', '*', '>', '~', '^', ',', '"', ':':
		case '_', '*', '>', '~', '^', ',', '"', '#', ':':
			in, success = cp.parseFormat()
		case '@', '\'', '`', '=', runeModGrave:
			in, success = cp.parseLiteral()
		case '$':
			in, success = cp.parseLiteralMath()
		case '\\':
			return cp.parseBackslash()
98
99
100
101
102
103
104
105

106
107
108
109
110
111
112
99
100
101
102
103
104
105

106
107
108
109
110
111
112
113







-
+







		return cp.parseBackslashRest()
	}
	for {
		inp.Next()
		switch inp.Ch {
		// The following case must contain all runes that occur in parseInline!
		// Plus the closing brackets ] and } and ) and the middle |
		case input.EOS, '\n', '\r', ' ', '\t', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
		case input.EOS, '\n', '\r', '[', ']', '{', '}', '(', ')', '|', '%', '_', '*', '>', '~', '^', ',', '"', '#', ':', '\'', '@', '`', runeModGrave, '$', '=', '\\', '-', '&':
			return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseBackslash() ast.InlineNode {
	inp := cp.inp
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
131
132
133
134
135
136
137













138
139
140
141
142
143
144







-
-
-
-
-
-
-
-
-
-
-
-
-







		return &ast.TextNode{Text: "\u00a0"}
	}
	pos := inp.Pos
	inp.Next()
	return &ast.TextNode{Text: string(inp.Src[pos:inp.Pos])}
}

func (cp *zmkP) parseSpace() *ast.SpaceNode {
	inp := cp.inp
	pos := inp.Pos
	for {
		inp.Next()
		switch inp.Ch {
		case ' ', '\t':
		default:
			return &ast.SpaceNode{Lexeme: string(inp.Src[pos:inp.Pos])}
		}
	}
}

func (cp *zmkP) parseSoftBreak() *ast.BreakNode {
	cp.inp.EatEOL()
	return &ast.BreakNode{}
}

func (cp *zmkP) parseLink() (*ast.LinkNode, bool) {
	if ref, is, ok := cp.parseReference('[', ']'); ok {
169
170
171
172
173
174
175
176

177
178
179
180
181
182
183
157
158
159
160
161
162
163

164
165
166
167
168
169
170
171







-
+







func hasQueryPrefix(src []byte) bool {
	return len(src) > len(ast.QueryPrefix) && string(src[:len(ast.QueryPrefix)]) == ast.QueryPrefix
}

func (cp *zmkP) parseReference(openCh, closeCh rune) (ref string, is ast.InlineSlice, _ bool) {
	inp := cp.inp
	inp.Next()
	cp.skipSpace()
	inp.SkipSpace()
	if inp.Ch == openCh {
		// Additional opening chars result in a fail
		return "", nil, false
	}
	pos := inp.Pos
	if !hasQueryPrefix(inp.Src[pos:]) {
		hasSpace, ok := cp.readReferenceToSep(closeCh)
202
203
204
205
206
207
208
209

210
211
212
213
214
215
216
190
191
192
193
194
195
196

197
198
199
200
201
202
203
204







-
+







			if hasSpace {
				return "", nil, false
			}
			inp.SetPos(pos)
		}
	}

	cp.skipSpace()
	inp.SkipSpace()
	pos = inp.Pos
	if !cp.readReferenceToClose(closeCh) {
		return "", nil, false
	}
	ref = strings.TrimSpace(string(inp.Src[pos:inp.Pos]))
	inp.Next()
	if inp.Ch != closeCh {
321
322
323
324
325
326
327

328

329
330
331
332
333
334
335
336
337
309
310
311
312
313
314
315
316

317
318

319
320
321
322
323
324
325







+
-
+

-







		return nil, false
	}
	attrs := cp.parseInlineAttributes()
	return &ast.FootnoteNode{Inlines: ins, Attrs: attrs}, true
}

func (cp *zmkP) parseLinkLikeRest() (ast.InlineSlice, bool) {
	inp := cp.inp
	cp.skipSpace()
	inp.SkipSpace()
	ins := ast.InlineSlice{}
	inp := cp.inp
	for inp.Ch != ']' {
		in := cp.parseInline()
		if in == nil {
			return nil, false
		}
		ins = append(ins, in)
		if _, ok := in.(*ast.BreakNode); ok && input.IsEOLEOS(inp.Ch) {
392
393
394
395
396
397
398
399

400
401
402
403
404
405
406
380
381
382
383
384
385
386

387
388
389
390
391
392
393
394







-
+







	if inp.Ch != '%' {
		return nil, false
	}
	for inp.Ch == '%' {
		inp.Next()
	}
	attrs := cp.parseInlineAttributes()
	cp.skipSpace()
	inp.SkipSpace()
	pos := inp.Pos
	for {
		if input.IsEOLEOS(inp.Ch) {
			return &ast.LiteralNode{
				Kind:    ast.LiteralComment,
				Attrs:   attrs,
				Content: append([]byte(nil), inp.Src[pos:inp.Pos]...),
414
415
416
417
418
419
420

421
422
423
424
425
426
427
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416







+







	'_': ast.FormatEmph,
	'*': ast.FormatStrong,
	'>': ast.FormatInsert,
	'~': ast.FormatDelete,
	'^': ast.FormatSuper,
	',': ast.FormatSub,
	'"': ast.FormatQuote,
	'#': ast.FormatMark,
	':': ast.FormatSpan,
}

func (cp *zmkP) parseFormat() (res ast.InlineNode, success bool) {
	inp := cp.inp
	fch := inp.Ch
	kind, ok := mapRuneFormat[fch]

Changes to parser/zettelmark/node.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 zettelmark

import "zettelstore.de/z/ast"

// Internal nodes for parsing zettelmark. These will be removed in

Changes to parser/zettelmark/post-processor.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 zettelmark

import (
	"strings"

69
70
71
72
73
74
75

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

83
84
85
86
87
88
89







+



-








func (pp *postProcessor) visitRegion(rn *ast.RegionNode) {
	oldVerse := pp.inVerse
	if rn.Kind == ast.RegionVerse {
		pp.inVerse = true
	}
	pp.visitBlockSlice(&rn.Blocks)
	pp.inVerse = oldVerse
	if len(rn.Inlines) > 0 {
		pp.visitInlineSlice(&rn.Inlines)
	}
	pp.inVerse = oldVerse
}

func (pp *postProcessor) visitNestedList(ln *ast.NestedListNode) {
	for i, item := range ln.Items {
		ln.Items[i] = pp.processItemSlice(item)
	}
	if ln.Kind != ast.NestedListQuote {
127
128
129
130
131
132
133
134

135
136
137
138
139
140
141
130
131
132
133
134
135
136

137
138
139
140
141
142
143
144







-
+







		}
	}
}

func (pp *postProcessor) visitTable(tn *ast.TableNode) {
	width := tableWidth(tn)
	tn.Align = make([]ast.Alignment, width)
	for i := 0; i < width; i++ {
	for i := range width {
		tn.Align[i] = ast.AlignDefault
	}
	if len(tn.Rows) > 0 && isHeaderRow(tn.Rows[0]) {
		tn.Header = tn.Rows[0]
		tn.Rows = tn.Rows[1:]
		pp.visitTableHeader(tn)
	}
361
362
363
364
365
366
367
368
369


370
371
372
373






374
375
376
377
378
379
380
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







-
-
+
+



-
+
+
+
+
+
+







}

// processInlineSliceHead removes leading spaces and empty text.
func (pp *postProcessor) processInlineSliceHead(is *ast.InlineSlice) {
	ins := *is
	for i, in := range ins {
		switch in := in.(type) {
		case *ast.SpaceNode:
			if pp.inVerse {
		case *ast.TextNode:
			if pp.inVerse && len(in.Text) > 0 {
				*is = ins[i:]
				return
			}
		case *ast.TextNode:
			for len(in.Text) > 0 {
				if ch := in.Text[0]; ch != ' ' && ch != '\t' {
					break
				}
				in.Text = in.Text[1:]
			}
			if len(in.Text) > 0 {
				*is = ins[i:]
				return
			}
		default:
			*is = ins[i:]
			return
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










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







+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+

-










-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-






+




-








+
+
+
+
+
+
+
+
+
+
	ins := *is
	fromPos, toPos := 0, 0
	for fromPos < maxPos {
		ins[toPos] = ins[fromPos]
		fromPos++
		switch in := ins[toPos].(type) {
		case *ast.TextNode:
			// Merge following TextNodes
			for fromPos < maxPos {
				if tn, ok := ins[fromPos].(*ast.TextNode); ok {
					in.Text = in.Text + tn.Text
			fromPos = processTextNode(ins, maxPos, in, fromPos)
		case *ast.SpaceNode:
					fromPos++
				} else {
					break
				}
			}
			if in.Text == "" {
				continue
			}
			if ch := in.Text[len(in.Text)-1]; ch == ' ' && fromPos < maxPos {
				switch nn := ins[fromPos].(type) {
				case *ast.BreakNode:
					nn.Hard = true
					in.Text = removeTrailingSpaces(in.Text)
				case *ast.LiteralNode:
					if nn.Kind == ast.LiteralComment {
						in.Text = removeTrailingSpaces(in.Text)
					}
				}
			}
			if pp.inVerse {
				in.Lexeme = strings.Repeat("\u00a0", in.Count())
				in.Text = strings.ReplaceAll(in.Text, " ", "\u00a0")
			}
			fromPos = processSpaceNode(ins, maxPos, in, toPos, fromPos)
		case *ast.BreakNode:
			if pp.inVerse {
				in.Hard = true
			}
		}
		toPos++
	}
	return toPos
}

func processTextNode(ins ast.InlineSlice, maxPos int, in *ast.TextNode, fromPos int) int {
	for fromPos < maxPos {
		if tn, ok := ins[fromPos].(*ast.TextNode); ok {
			in.Text = in.Text + tn.Text
			fromPos++
		} else {
			break
		}
	}
	return fromPos
}

func processSpaceNode(ins ast.InlineSlice, maxPos int, in *ast.SpaceNode, toPos, fromPos int) int {
	if fromPos < maxPos {
		switch nn := ins[fromPos].(type) {
		case *ast.BreakNode:
			if in.Count() > 1 {
				nn.Hard = true
				ins[toPos] = nn
				fromPos++
			}
		case *ast.LiteralNode:
			if nn.Kind == ast.LiteralComment {
				ins[toPos] = ins[fromPos]
				fromPos++
			}
		}
	}
	return fromPos
}

// processInlineSliceTail removes empty text nodes, breaks and spaces at the end.
func (*postProcessor) processInlineSliceTail(is *ast.InlineSlice, toPos int) int {
	ins := *is
	for toPos > 0 {
		switch n := ins[toPos-1].(type) {
		case *ast.TextNode:
			n.Text = removeTrailingSpaces(n.Text)
			if len(n.Text) > 0 {
				return toPos
			}
		case *ast.BreakNode:
		case *ast.SpaceNode:
		default:
			return toPos
		}
		toPos--
		ins[toPos] = nil // Kill node to enable garbage collection
	}
	return toPos
}

func removeTrailingSpaces(s string) string {
	for len(s) > 0 {
		if ch := s[len(s)-1]; ch != ' ' && ch != '\t' {
			return s
		}
		s = s[0 : len(s)-1]
	}
	return ""
}

Changes to parser/zettelmark/zettelmark.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
1

2
3
4
5
6
7
8
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
// 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 zettelmark provides a parser for zettelmarkup.
package zettelmark

import (
	"strings"
	"unicode"

	"zettelstore.de/c/attrs"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func init() {
	parser.Register(&parser.Info{
		Name:          meta.SyntaxZmk,
		AltNames:      nil,
		IsASTParser:   true,
149
150
151
152
153
154
155
156

157
158
159
160
161
162
163
152
153
154
155
156
157
158

159
160
161
162
163
164
165
166







-
+







		inp.Next()
	}
	if pos < inp.Pos {
		return attrs.Attributes{"": string(inp.Src[pos:inp.Pos])}
	}

	// No immediate name: skip spaces
	cp.skipSpace()
	inp.SkipSpace()
	return cp.parseInlineAttributes()
}

func (cp *zmkP) parseInlineAttributes() attrs.Attributes {
	inp := cp.inp
	pos := inp.Pos
	if attrs, success := cp.doParseAttributes(); success {
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
236
237
238
239
240
241
242






243
244
245







-
-
-
-
-
-



			inp.EatEOL()
		default:
			return
		}
	}
}

func (cp *zmkP) skipSpace() {
	for inp := cp.inp; inp.Ch == ' '; {
		inp.Next()
	}
}

func isNameRune(ch rune) bool {
	return unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '-' || ch == '_'
}

Changes to parser/zettelmark/zettelmark_fuzz_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
1

2
3
4
5
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) 2022 Detlef Stern
// 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 zettelmark_test

import (
	"testing"

	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

func FuzzParseBlocks(f *testing.F) {
	f.Fuzz(func(t *testing.T, src []byte) {
		t.Parallel()
		inp := input.NewInput(src)
		parser.ParseBlocks(inp, nil, meta.SyntaxZmk, config.NoHTML)

Changes to parser/zettelmark/zettelmark_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
1

2
3
4
5
6
7
8
9
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
// 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 zettelmark_test provides some tests for the zettelmarkup parser.
package zettelmark_test

import (
	"bytes"
	"fmt"
	"strings"
	"testing"

	"zettelstore.de/c/attrs"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/parser"
)

type TestCase struct{ source, want string }
type TestCases []TestCase

func replace(s string, tcs TestCases) TestCases {
	var testCases TestCases
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
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







-
+














-
+







	})
}

func TestText(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"abcd", "(PARA abcd)"},
		{"ab cd", "(PARA ab SP cd)"},
		{"ab cd", "(PARA ab cd)"},
		{"abcd ", "(PARA abcd)"},
		{" abcd", "(PARA abcd)"},
		{"\\", "(PARA \\)"},
		{"\\\n", ""},
		{"\\\ndef", "(PARA HB def)"},
		{"\\\r", ""},
		{"\\\rdef", "(PARA HB def)"},
		{"\\\r\n", ""},
		{"\\\r\ndef", "(PARA HB def)"},
		{"\\a", "(PARA a)"},
		{"\\aa", "(PARA aa)"},
		{"a\\a", "(PARA aa)"},
		{"\\+", "(PARA +)"},
		{"\\ ", "(PARA \u00a0)"},
		{"http://a, http://b", "(PARA http://a, SP http://b)"},
		{"http://a, http://b", "(PARA http://a, http://b)"},
	})
}

func TestSpace(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{" ", ""},
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
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







-
+


-
+












-
+







		{"[", "(PARA [)"},
		{"[[", "(PARA [[)"},
		{"[[|", "(PARA [[|)"},
		{"[[]", "(PARA [[])"},
		{"[[|]", "(PARA [[|])"},
		{"[[]]", "(PARA [[]])"},
		{"[[|]]", "(PARA [[|]])"},
		{"[[ ]]", "(PARA [[ SP ]])"},
		{"[[ ]]", "(PARA [[ ]])"},
		{"[[\n]]", "(PARA [[ SB ]])"},
		{"[[ a]]", "(PARA (LINK a))"},
		{"[[a ]]", "(PARA [[a SP ]])"},
		{"[[a ]]", "(PARA [[a ]])"},
		{"[[a\n]]", "(PARA [[a SB ]])"},
		{"[[a]]", "(PARA (LINK a))"},
		{"[[12345678901234]]", "(PARA (LINK 12345678901234))"},
		{"[[a]", "(PARA [[a])"},
		{"[[|a]]", "(PARA [[|a]])"},
		{"[[b|]]", "(PARA [[b|]])"},
		{"[[b|a]]", "(PARA (LINK a b))"},
		{"[[b| a]]", "(PARA (LINK a b))"},
		{"[[b%c|a]]", "(PARA (LINK a b%c))"},
		{"[[b%%c|a]]", "(PARA [[b {% c|a]]})"},
		{"[[b|a]", "(PARA [[b|a])"},
		{"[[b\nc|a]]", "(PARA (LINK a b SB c))"},
		{"[[b c|a#n]]", "(PARA (LINK a#n b SP c))"},
		{"[[b c|a#n]]", "(PARA (LINK a#n b c))"},
		{"[[a]]go", "(PARA (LINK a) go)"},
		{"[[b|a]]{go}", "(PARA (LINK a b)[ATTR go])"},
		{"[[[[a]]|b]]", "(PARA [[ (LINK a) |b]])"},
		{"[[a[b]c|d]]", "(PARA (LINK d a[b]c))"},
		{"[[[b]c|d]]", "(PARA [ (LINK d b]c))"},
		{"[[a[]c|d]]", "(PARA (LINK d a[]c))"},
		{"[[a[b]|d]]", "(PARA (LINK d a[b]))"},
176
177
178
179
180
181
182
183

184
185
186
187
188
189
190
178
179
180
181
182
183
184

185
186
187
188
189
190
191
192







-
+








func TestCite(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[@", "(PARA [@)"},
		{"[@]", "(PARA [@])"},
		{"[@a]", "(PARA (CITE a))"},
		{"[@ a]", "(PARA [@ SP a])"},
		{"[@ a]", "(PARA [@ a])"},
		{"[@a ]", "(PARA (CITE a))"},
		{"[@a\n]", "(PARA (CITE a))"},
		{"[@a\nx]", "(PARA (CITE a SB x))"},
		{"[@a\n\n]", "(PARA [@a)(PARA ])"},
		{"[@a,\n]", "(PARA (CITE a))"},
		{"[@a,n]", "(PARA (CITE a n))"},
		{"[@a| n]", "(PARA (CITE a n))"},
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
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







-
+

-
+











-
+







		{"{", "(PARA {)"},
		{"{{", "(PARA {{)"},
		{"{{|", "(PARA {{|)"},
		{"{{}", "(PARA {{})"},
		{"{{|}", "(PARA {{|})"},
		{"{{}}", "(PARA {{}})"},
		{"{{|}}", "(PARA {{|}})"},
		{"{{ }}", "(PARA {{ SP }})"},
		{"{{ }}", "(PARA {{ }})"},
		{"{{\n}}", "(PARA {{ SB }})"},
		{"{{a }}", "(PARA {{a SP }})"},
		{"{{a }}", "(PARA {{a }})"},
		{"{{a\n}}", "(PARA {{a SB }})"},
		{"{{a}}", "(PARA (EMBED a))"},
		{"{{12345678901234}}", "(PARA (EMBED 12345678901234))"},
		{"{{ a}}", "(PARA (EMBED a))"},
		{"{{a}", "(PARA {{a})"},
		{"{{|a}}", "(PARA {{|a}})"},
		{"{{b|}}", "(PARA {{b|}})"},
		{"{{b|a}}", "(PARA (EMBED a b))"},
		{"{{b| a}}", "(PARA (EMBED a b))"},
		{"{{b|a}", "(PARA {{b|a})"},
		{"{{b\nc|a}}", "(PARA (EMBED a b SB c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b SP c))"},
		{"{{b c|a#n}}", "(PARA (EMBED a#n b c))"},
		{"{{a}}{go}", "(PARA (EMBED a)[ATTR go])"},
		{"{{{{a}}|b}}", "(PARA {{ (EMBED a) |b}})"},
		{"{{\\|}}", "(PARA (EMBED %5C%7C))"},
		{"{{\\||a}}", "(PARA (EMBED a |))"},
		{"{{b\\||a}}", "(PARA (EMBED a b|))"},
		{"{{b\\|c|a}}", "(PARA (EMBED a b|c))"},
		{"{{\\}}}", "(PARA (EMBED %5C%7D))"},
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
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







-
+


-
+






-
+







func TestMark(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"[!", "(PARA [!)"},
		{"[!\n", "(PARA [!)"},
		{"[!]", "(PARA (MARK #*))"},
		{"[!][!]", "(PARA (MARK #*) (MARK #*-1))"},
		{"[! ]", "(PARA [! SP ])"},
		{"[! ]", "(PARA [! ])"},
		{"[!a]", "(PARA (MARK \"a\" #a))"},
		{"[!a][!a]", "(PARA (MARK \"a\" #a) (MARK \"a\" #a-1))"},
		{"[!a ]", "(PARA [!a SP ])"},
		{"[!a ]", "(PARA [!a ])"},
		{"[!a_]", "(PARA (MARK \"a_\" #a))"},
		{"[!a_][!a]", "(PARA (MARK \"a_\" #a) (MARK \"a\" #a-1))"},
		{"[!a-b]", "(PARA (MARK \"a-b\" #a-b))"},
		{"[!a|b]", "(PARA (MARK \"a\" #a b))"},
		{"[!a|]", "(PARA (MARK \"a\" #a))"},
		{"[!|b]", "(PARA (MARK #* b))"},
		{"[!|b c]", "(PARA (MARK #* b SP c))"},
		{"[!|b c]", "(PARA (MARK #* b c))"},
	})
}

func TestComment(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"%", "(PARA %)"},
290
291
292
293
294
295
296
297

298
299
300
301
302
303
304
305

306
307
308
309
310
311
312
292
293
294
295
296
297
298

299
300
301
302
303
304
305
306

307
308
309
310
311
312
313
314







-
+







-
+







		{"100%", "(PARA 100%)"},
	})
}

func TestFormat(t *testing.T) {
	t.Parallel()
	// Not for Insert / '>', because collision with quoted list
	for _, ch := range []string{"_", "*", "~", "^", ",", "\"", ":"} {
	for _, ch := range []string{"_", "*", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$", "(PARA $)"},
			{"$$", "(PARA $$)"},
			{"$$$", "(PARA $$$)"},
			{"$$$$", "(PARA {$})"},
		}))
	}
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", ":"} {
	for _, ch := range []string{"_", "*", ">", "~", "^", ",", "\"", "#", ":"} {
		checkTcs(t, replace(ch, TestCases{
			{"$$a$$", "(PARA {$ a})"},
			{"$$a$$$", "(PARA {$ a} $)"},
			{"$$$a$$", "(PARA {$ $a})"},
			{"$$$a$$$", "(PARA {$ $a} $)"},
			{"$\\$", "(PARA $$)"},
			{"$\\$$", "(PARA $$$)"},
411
412
413
414
415
416
417
418

419
420
421
422
423
424
425
413
414
415
416
417
418
419

420
421
422
423
424
425
426
427







-
+







		// Good cases
		{"&lt;", "(PARA <)"},
		{"&#48;", "(PARA 0)"},
		{"&#x4A;", "(PARA J)"},
		{"&#X4a;", "(PARA J)"},
		{"&hellip;", "(PARA \u2026)"},
		{"&nbsp;", "(PARA \u00a0)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: SP &,?;c.)"},
		{"E: &amp;,&#63;;&#x63;.", "(PARA E: &,?;c.)"},
	})
}

func TestVerbatimZettel(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"@@@\n@@@", "(ZETTEL)"},
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
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







-
+

-
+














-
-
+
+


-
-
+
+







	}))
}

func TestHeading(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"=h", "(PARA =h)"},
		{"= h", "(PARA = SP h)"},
		{"= h", "(PARA = h)"},
		{"==h", "(PARA ==h)"},
		{"== h", "(PARA == SP h)"},
		{"== h", "(PARA == h)"},
		{"===h", "(PARA ===h)"},
		{"=== h", "(H1 h #h)"},
		{"===  h", "(H1 h #h)"},
		{"==== h", "(H2 h #h)"},
		{"===== h", "(H3 h #h)"},
		{"====== h", "(H4 h #h)"},
		{"======= h", "(H5 h #h)"},
		{"======== h", "(H5 h #h)"},
		{"=", "(PARA =)"},
		{"=== h=__=a__", "(H1 h= {_ =a} #h-a)"},
		{"=\n", "(PARA =)"},
		{"a=", "(PARA a=)"},
		{" =", "(PARA =)"},
		{"=== h\na", "(H1 h #h)(PARA a)"},
		{"=== h i {-}", "(H1 h SP i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h SP (EMBED a) #h)"},
		{"=== h i {-}", "(H1 h i #h-i)[ATTR -]"},
		{"=== h {{a}}", "(H1 h  (EMBED a) #h)"},
		{"=== h{{a}}", "(H1 h (EMBED a) #h)"},
		{"=== {{a}}", "(H1 (EMBED a))"},
		{"=== h {{a}}{-}", "(H1 h SP (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H1 h SP (EMBED a) #h)[ATTR -]"},
		{"=== h {{a}}{-}", "(H1 h  (EMBED a)[ATTR -] #h)"},
		{"=== h {{a}} {-}", "(H1 h  (EMBED a) #h)[ATTR -]"},
		{"=== h {-}{{a}}", "(H1 h #h)[ATTR -]"},
		{"=== h{id=abc}", "(H1 h #h)[ATTR id=abc]"},
		{"=== h\n=== h", "(H1 h #h)(H1 h #h-1)"},
	})
}

func TestHRule(t *testing.T) {
615
616
617
618
619
620
621
622

623
624
625
626
627
628
629
617
618
619
620
621
622
623

624
625
626
627
628
629
630
631







-
+







		{">", "(QL {})"},
	})
}

func TestQuoteList(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"> w1 w2", "(QL {(PARA w1 SP w2)})"},
		{"> w1 w2", "(QL {(PARA w1 w2)})"},
		{"> w1\n> w2", "(QL {(PARA w1 SB w2)})"},
		{"> w1\n>\n>w2", "(QL {(PARA w1)} {})(PARA >w2)"},
	})
}

func TestEnumAfterPara(t *testing.T) {
	t.Parallel()
640
641
642
643
644
645
646
647

648
649
650
651
652
653
654
642
643
644
645
646
647
648

649
650
651
652
653
654
655
656







-
+







		{"; ", "(PARA ;)"},
		{"; abc", "(DL (DT abc))"},
		{"; abc\ndef", "(DL (DT abc))(PARA def)"},
		{"; abc\n def", "(DL (DT abc))(PARA def)"},
		{"; abc\n  def", "(DL (DT abc SB def))"},
		{":", "(PARA :)"},
		{": ", "(PARA :)"},
		{": abc", "(PARA : SP abc)"},
		{": abc", "(PARA : abc)"},
		{"; abc\n: def", "(DL (DT abc) (DD (PARA def)))"},
		{"; abc\n: def\nghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n ghi", "(DL (DT abc) (DD (PARA def)))(PARA ghi)"},
		{"; abc\n: def\n  ghi", "(DL (DT abc) (DD (PARA def SB ghi)))"},
		{"; abc\n: def\n\n  ghi", "(DL (DT abc) (DD (PARA def)(PARA ghi)))"},
		{"; abc\n:", "(DL (DT abc))(PARA :)"},
		{"; abc\n: def\n: ghi", "(DL (DT abc) (DD (PARA def)) (DD (PARA ghi)))"},
744
745
746
747
748
749
750
751

752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769

770
771
772

773
774
775
776
777
778
779

780
781

782
783
784
785
786
787

788
789
790


791
792

793
794
795
796
797
798
799

800
801

802
803
804
805

806
807

808
809

810
811
812

813
814
815
816


817
818

819
820
821

822
823
824

825
826

827
828

829
830

831
832

833
834

835
836

837
838

839
840

841
842
843

844
845

846
847

848
849
850


851
852

853
854

855
856
857

858
859

860
861
862

863
864
865


866
867

868
869

870
871
872

873
874

875
876
877
878
879



880
881

882
883
884
885
886
887
888
889
890

891
892

893
894
895

896
897

898
899
900

901
902
903
904

905
906
907
908
909

910
911
912
913

914
915
916

917
918

919
920
921

922
923
924
925



926
927
928
929


930
931
932
933
934

935
936

937
938

939
940
941
942
943
944
945
946


947
948
949


950
951

952
953
954
955
956
957
958
746
747
748
749
750
751
752

753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770

771
772
773

774
775
776
777
778
779
780

781
782

783
784
785
786
787
788

789
790


791
792
793

794
795
796
797
798
799
800

801
802

803
804
805
806

807
808

809
810

811
812
813

814
815
816


817
818
819

820
821
822

823
824
825

826
827

828
829

830
831

832
833

834
835

836
837

838
839

840
841

842
843
844

845
846

847
848

849
850


851
852
853

854
855

856
857
858

859
860

861
862
863

864
865


866
867
868

869
870

871
872
873

874
875

876
877
878



879
880
881
882

883






884
885

886
887

888
889
890

891
892

893
894
895

896
897
898
899

900
901
902
903
904

905
906
907
908

909
910
911

912
913

914
915
916

917
918



919
920
921
922
923


924
925
926
927
928
929

930
931

932
933

934
935
936
937
938
939
940


941
942
943


944
945
946

947
948
949
950
951
952
953
954







-
+

















-
+


-
+






-
+

-
+





-
+

-
-
+
+

-
+






-
+

-
+



-
+

-
+

-
+


-
+


-
-
+
+

-
+


-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+

-
+


-
+

-
+

-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
+

-
-
+
+

-
+

-
+


-
+

-
+


-
-
-
+
+
+

-
+
-
-
-
-
-
-


-
+

-
+


-
+

-
+


-
+



-
+




-
+



-
+


-
+

-
+


-
+

-
-
-
+
+
+


-
-
+
+




-
+

-
+

-
+






-
-
+
+

-
-
+
+

-
+







	})
	checkTcs(t, replace("\"", TestCases{
		{"::a::{py=3}", "(PARA {: a}[ATTR py=3])"},
		{"::a::{py=$2 3$}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{py=$2\\$3$}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=2$3}", "(PARA {: a}[ATTR py=2$3])"},
		{"::a::{py=$2\n3$}", "(PARA {: a}[ATTR py=$2\n3$])"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 SP 3})"},
		{"::a::{py=$2 3}", "(PARA {: a} {py=$2 3})"},

		{"::a::{py=2 py=3}", "(PARA {: a}[ATTR py=$2 3$])"},
		{"::a::{.go .py}", "(PARA {: a}[ATTR class=$go py$])"},
	}))
}

func TestTemp(t *testing.T) {
	t.Parallel()
	checkTcs(t, TestCases{
		{"", ""},
	})
}

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

// TestVisitor serializes the abstract syntax tree to a string.
type TestVisitor struct {
	buf bytes.Buffer
	sb strings.Builder
}

func (tv *TestVisitor) String() string { return tv.buf.String() }
func (tv *TestVisitor) String() string { return tv.sb.String() }

func (tv *TestVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		tv.visitInlineSlice(n)
	case *ast.ParaNode:
		tv.buf.WriteString("(PARA")
		tv.sb.WriteString("(PARA")
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.VerbatimNode:
		code, ok := mapVerbatimKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown verbatim code %v", n.Kind))
		}
		tv.buf.WriteString(code)
		tv.sb.WriteString(code)
		if len(n.Content) > 0 {
			tv.buf.WriteByte('\n')
			tv.buf.Write(n.Content)
			tv.sb.WriteByte('\n')
			tv.sb.Write(n.Content)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.RegionNode:
		code, ok := mapRegionKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("Unknown region code %v", n.Kind))
		}
		tv.buf.WriteString(code)
		tv.sb.WriteString(code)
		if len(n.Blocks) > 0 {
			tv.buf.WriteByte(' ')
			tv.sb.WriteByte(' ')
			ast.Walk(tv, &n.Blocks)
		}
		if len(n.Inlines) > 0 {
			tv.buf.WriteString(" (LINE")
			tv.sb.WriteString(" (LINE")
			ast.Walk(tv, &n.Inlines)
			tv.buf.WriteByte(')')
			tv.sb.WriteByte(')')
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HeadingNode:
		fmt.Fprintf(&tv.buf, "(H%d", n.Level)
		fmt.Fprintf(&tv.sb, "(H%d", n.Level)
		ast.Walk(tv, &n.Inlines)
		if n.Fragment != "" {
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.HRuleNode:
		tv.buf.WriteString("(HR)")
		tv.sb.WriteString("(HR)")
		tv.visitAttributes(n.Attrs)
	case *ast.NestedListNode:
		tv.buf.WriteString(mapNestedListKind[n.Kind])
		tv.sb.WriteString(mapNestedListKind[n.Kind])
		for _, item := range n.Items {
			tv.buf.WriteString(" {")
			tv.sb.WriteString(" {")
			ast.WalkItemSlice(tv, item)
			tv.buf.WriteByte('}')
			tv.sb.WriteByte('}')
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.DescriptionListNode:
		tv.buf.WriteString("(DL")
		tv.sb.WriteString("(DL")
		for _, def := range n.Descriptions {
			tv.buf.WriteString(" (DT")
			tv.sb.WriteString(" (DT")
			ast.Walk(tv, &def.Term)
			tv.buf.WriteByte(')')
			tv.sb.WriteByte(')')
			for _, b := range def.Descriptions {
				tv.buf.WriteString(" (DD ")
				tv.sb.WriteString(" (DD ")
				ast.WalkDescriptionSlice(tv, b)
				tv.buf.WriteByte(')')
				tv.sb.WriteByte(')')
			}
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.TableNode:
		tv.buf.WriteString("(TAB")
		tv.sb.WriteString("(TAB")
		if len(n.Header) > 0 {
			tv.buf.WriteString(" (TR")
			tv.sb.WriteString(" (TR")
			for _, cell := range n.Header {
				tv.buf.WriteString(" (TH")
				tv.buf.WriteString(alignString[cell.Align])
				tv.sb.WriteString(" (TH")
				tv.sb.WriteString(alignString[cell.Align])
				ast.Walk(tv, &cell.Inlines)
				tv.buf.WriteString(")")
				tv.sb.WriteString(")")
			}
			tv.buf.WriteString(")")
			tv.sb.WriteString(")")
		}
		if len(n.Rows) > 0 {
			tv.buf.WriteString(" ")
			tv.sb.WriteString(" ")
			for _, row := range n.Rows {
				tv.buf.WriteString("(TR")
				tv.sb.WriteString("(TR")
				for i, cell := range row {
					if i == 0 {
						tv.buf.WriteString(" ")
						tv.sb.WriteString(" ")
					}
					tv.buf.WriteString("(TD")
					tv.buf.WriteString(alignString[cell.Align])
					tv.sb.WriteString("(TD")
					tv.sb.WriteString(alignString[cell.Align])
					ast.Walk(tv, &cell.Inlines)
					tv.buf.WriteString(")")
					tv.sb.WriteString(")")
				}
				tv.buf.WriteString(")")
				tv.sb.WriteString(")")
			}
		}
		tv.buf.WriteString(")")
		tv.sb.WriteString(")")
	case *ast.TranscludeNode:
		fmt.Fprintf(&tv.buf, "(TRANSCLUDE %v)", n.Ref)
		fmt.Fprintf(&tv.sb, "(TRANSCLUDE %v)", n.Ref)
		tv.visitAttributes(n.Attrs)
	case *ast.BLOBNode:
		tv.buf.WriteString("(BLOB ")
		tv.buf.WriteString(n.Syntax)
		tv.buf.WriteString(")")
		tv.sb.WriteString("(BLOB ")
		tv.sb.WriteString(n.Syntax)
		tv.sb.WriteString(")")
	case *ast.TextNode:
		tv.buf.WriteString(n.Text)
		tv.sb.WriteString(n.Text)
	case *ast.SpaceNode:
		if l := n.Count(); l == 1 {
			tv.buf.WriteString("SP")
		} else {
			fmt.Fprintf(&tv.buf, "SP%d", l)
		}
	case *ast.BreakNode:
		if n.Hard {
			tv.buf.WriteString("HB")
			tv.sb.WriteString("HB")
		} else {
			tv.buf.WriteString("SB")
			tv.sb.WriteString("SB")
		}
	case *ast.LinkNode:
		fmt.Fprintf(&tv.buf, "(LINK %v", n.Ref)
		fmt.Fprintf(&tv.sb, "(LINK %v", n.Ref)
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedRefNode:
		fmt.Fprintf(&tv.buf, "(EMBED %v", n.Ref)
		fmt.Fprintf(&tv.sb, "(EMBED %v", n.Ref)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.EmbedBLOBNode:
		panic("TODO: zmktest blob")
	case *ast.CiteNode:
		fmt.Fprintf(&tv.buf, "(CITE %s", n.Key)
		fmt.Fprintf(&tv.sb, "(CITE %s", n.Key)
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.FootnoteNode:
		tv.buf.WriteString("(FN")
		tv.sb.WriteString("(FN")
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
		tv.visitAttributes(n.Attrs)
	case *ast.MarkNode:
		tv.buf.WriteString("(MARK")
		tv.sb.WriteString("(MARK")
		if n.Mark != "" {
			tv.buf.WriteString(" \"")
			tv.buf.WriteString(n.Mark)
			tv.buf.WriteByte('"')
			tv.sb.WriteString(" \"")
			tv.sb.WriteString(n.Mark)
			tv.sb.WriteByte('"')
		}
		if n.Fragment != "" {
			tv.buf.WriteString(" #")
			tv.buf.WriteString(n.Fragment)
			tv.sb.WriteString(" #")
			tv.sb.WriteString(n.Fragment)
		}
		if len(n.Inlines) > 0 {
			ast.Walk(tv, &n.Inlines)
		}
		tv.buf.WriteByte(')')
		tv.sb.WriteByte(')')
	case *ast.FormatNode:
		fmt.Fprintf(&tv.buf, "{%c", mapFormatKind[n.Kind])
		fmt.Fprintf(&tv.sb, "{%c", mapFormatKind[n.Kind])
		ast.Walk(tv, &n.Inlines)
		tv.buf.WriteByte('}')
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	case *ast.LiteralNode:
		code, ok := mapLiteralKind[n.Kind]
		if !ok {
			panic(fmt.Sprintf("No element for code %v", n.Kind))
		}
		tv.buf.WriteByte('{')
		tv.buf.WriteRune(code)
		tv.sb.WriteByte('{')
		tv.sb.WriteRune(code)
		if len(n.Content) > 0 {
			tv.buf.WriteByte(' ')
			tv.buf.Write(n.Content)
			tv.sb.WriteByte(' ')
			tv.sb.Write(n.Content)
		}
		tv.buf.WriteByte('}')
		tv.sb.WriteByte('}')
		tv.visitAttributes(n.Attrs)
	default:
		return tv
	}
	return nil
}

987
988
989
990
991
992
993

994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008

1009
1010
1011
1012
1013
1014
1015
1016
1017

1018
1019
1020
1021


1022
1023
1024

1025
1026
1027
1028



1029
1030

1031
1032
1033
1034
1035

1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004

1005
1006
1007
1008
1009
1010
1011
1012
1013

1014
1015
1016


1017
1018
1019
1020

1021
1022



1023
1024
1025
1026

1027
1028
1029
1030
1031

1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042







+














-
+








-
+


-
-
+
+


-
+

-
-
-
+
+
+

-
+




-
+










	ast.FormatEmph:   '_',
	ast.FormatStrong: '*',
	ast.FormatInsert: '>',
	ast.FormatDelete: '~',
	ast.FormatSuper:  '^',
	ast.FormatSub:    ',',
	ast.FormatQuote:  '"',
	ast.FormatMark:   '#',
	ast.FormatSpan:   ':',
}

var mapLiteralKind = map[ast.LiteralKind]rune{
	ast.LiteralZettel:  '@',
	ast.LiteralProg:    '`',
	ast.LiteralInput:   '\'',
	ast.LiteralOutput:  '=',
	ast.LiteralComment: '%',
	ast.LiteralMath:    '$',
}

func (tv *TestVisitor) visitInlineSlice(is *ast.InlineSlice) {
	for _, in := range *is {
		tv.buf.WriteByte(' ')
		tv.sb.WriteByte(' ')
		ast.Walk(tv, in)
	}
}

func (tv *TestVisitor) visitAttributes(a attrs.Attributes) {
	if a.IsEmpty() {
		return
	}
	tv.buf.WriteString("[ATTR")
	tv.sb.WriteString("[ATTR")

	for _, k := range a.Keys() {
		tv.buf.WriteByte(' ')
		tv.buf.WriteString(k)
		tv.sb.WriteByte(' ')
		tv.sb.WriteString(k)
		v := a[k]
		if len(v) > 0 {
			tv.buf.WriteByte('=')
			tv.sb.WriteByte('=')
			if quoteString(v) {
				tv.buf.WriteByte('"')
				tv.buf.WriteString(v)
				tv.buf.WriteByte('"')
				tv.sb.WriteByte('"')
				tv.sb.WriteString(v)
				tv.sb.WriteByte('"')
			} else {
				tv.buf.WriteString(v)
				tv.sb.WriteString(v)
			}
		}
	}

	tv.buf.WriteByte(']')
	tv.sb.WriteByte(']')
}

func quoteString(s string) bool {
	for _, ch := range s {
		if ch <= ' ' {
			return true
		}
	}
	return false
}

Added query/compiled.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 query

import (
	"math/rand/v2"
	"slices"

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

// Compiled is a compiled query, to be used in a Box
type Compiled struct {
	hasQuery bool
	seed     int
	pick     int
	order    []sortOrder
	offset   int // <= 0: no offset
	limit    int // <= 0: no limit

	startMeta []*meta.Meta
	PreMatch  MetaMatchFunc // Precondition for Match and Retrieve
	Terms     []CompiledTerm

	sortFunc sortFunc
}

// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
type MetaMatchFunc func(*meta.Meta) bool

func matchAlways(*meta.Meta) bool { return true }
func matchNever(*meta.Meta) bool  { return false }

// CompiledTerm is the preprocessed sequence of conjugated search terms.
type CompiledTerm struct {
	Match    MetaMatchFunc     // Match on metadata
	Retrieve RetrievePredicate // Retrieve from full-text search
}

// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool

// AlwaysIncluded is a RetrievePredicate that always returns true.
func AlwaysIncluded(id.Zid) bool { return true }
func neverIncluded(id.Zid) bool  { return false }

func (c *Compiled) isDeterministic() bool { return c.seed > 0 }

// Result returns a result of the compiled search, that is achievable without iterating through a box.
func (c *Compiled) Result() []*meta.Meta {
	if len(c.startMeta) == 0 {
		// nil -> no directive
		// empty slice -> nothing found
		return c.startMeta
	}
	result := make([]*meta.Meta, 0, len(c.startMeta))
	for _, m := range c.startMeta {
		for _, term := range c.Terms {
			if term.Match(m) && term.Retrieve(m.Zid) {
				result = append(result, m)
				break
			}
		}
	}
	result = c.pickElements(result)
	c.ensureSortFunc()
	result = c.sortElements(result)
	result = c.offsetElements(result)
	return limitElements(result, c.limit)
}

func (c *Compiled) ensureSortFunc() {
	if c.sortFunc == nil {
		c.sortFunc = buildSortFunc(c.order)
	}
}

// AfterSearch applies all terms to the metadata list that was searched.
//
// This includes sorting, offset, limit, and picking.
func (c *Compiled) AfterSearch(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if !c.hasQuery {
		slices.SortFunc(metaList, defaultMetaSort)
		return metaList
	}

	if c.isDeterministic() {
		// We need to sort to make it deterministic
		if len(c.order) == 0 || c.order[0].isRandom() {
			slices.SortFunc(metaList, defaultMetaSort)
		} else {
			c.ensureSortFunc()
			slices.SortFunc(metaList, c.sortFunc)
		}
	}
	metaList = c.pickElements(metaList)
	if c.isDeterministic() {
		if len(c.order) > 0 && c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		}
	} else {
		metaList = c.sortElements(metaList)
	}
	metaList = c.offsetElements(metaList)
	return limitElements(metaList, c.limit)
}

func (c *Compiled) sortElements(metaList []*meta.Meta) []*meta.Meta {
	if len(c.order) > 0 {
		if c.order[0].isRandom() {
			metaList = c.sortRandomly(metaList)
		} else {
			c.ensureSortFunc()
			slices.SortFunc(metaList, c.sortFunc)
		}
	}
	return metaList
}

func (c *Compiled) offsetElements(metaList []*meta.Meta) []*meta.Meta {
	if c.offset == 0 {
		return metaList
	}
	if c.offset > len(metaList) {
		return nil
	}
	return metaList[c.offset:]
}

func (c *Compiled) pickElements(metaList []*meta.Meta) []*meta.Meta {
	count := c.pick
	if count <= 0 || count >= len(metaList) {
		return metaList
	}
	if limit := c.limit; limit > 0 && limit < count {
		count = limit
		c.limit = 0
	}

	order := make([]int, len(metaList))
	for i := range len(metaList) {
		order[i] = i
	}
	rnd := c.newRandom()
	picked := make([]int, count)
	for i := range count {
		last := len(order) - i
		n := rnd.IntN(last)
		picked[i] = order[n]
		order[n] = order[last-1]
	}
	order = nil
	slices.Sort(picked)
	result := make([]*meta.Meta, count)
	for i, p := range picked {
		result[i] = metaList[p]
	}
	return result
}

func (c *Compiled) sortRandomly(metaList []*meta.Meta) []*meta.Meta {
	rnd := c.newRandom()
	rnd.Shuffle(
		len(metaList),
		func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] },
	)
	return metaList
}

func (c *Compiled) newRandom() *rand.Rand {
	seed := c.seed
	if seed <= 0 {
		seed = rand.IntN(10000) + 10001
	}
	return rand.New(rand.NewPCG(uint64(seed), uint64(seed)))
}

func limitElements(metaList []*meta.Meta, limit int) []*meta.Meta {
	if limit > 0 && limit < len(metaList) {
		return metaList[:limit]
	}
	return metaList
}

Added query/context.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 query

import (
	"container/heap"
	"context"
	"math"

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

// ContextSpec contains all specification values for calculating a context.
type ContextSpec struct {
	Direction ContextDirection
	MaxCost   int
	MaxCount  int
	Full      bool
}

// ContextDirection specifies the direction a context should be calculated.
type ContextDirection uint8

// Constants for ContextDirection.
const (
	ContextDirBoth ContextDirection = iota
	ContextDirForward
	ContextDirBackward
)

// ContextPort is the collection of box methods needed by this directive.
type ContextPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *Query) ([]*meta.Meta, error)
}

// Print the spec on the given print environment.
func (spec *ContextSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ContextDirective)
	if spec.Full {
		pe.printSpace()
		pe.writeString(api.FullDirective)
	}
	switch spec.Direction {
	case ContextDirBackward:
		pe.printSpace()
		pe.writeString(api.BackwardDirective)
	case ContextDirForward:
		pe.printSpace()
		pe.writeString(api.ForwardDirective)
	}
	pe.printPosInt(api.CostDirective, spec.MaxCost)
	pe.printPosInt(api.MaxDirective, spec.MaxCount)
}

// Execute the specification.
func (spec *ContextSpec) Execute(ctx context.Context, startSeq []*meta.Meta, port ContextPort) []*meta.Meta {
	maxCost := float64(spec.MaxCost)
	if maxCost <= 0 {
		maxCost = 17
	}
	maxCount := spec.MaxCount
	if maxCount <= 0 {
		maxCount = 200
	}
	tasks := newQueue(startSeq, maxCost, maxCount, port)
	isBackward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirBackward
	isForward := spec.Direction == ContextDirBoth || spec.Direction == ContextDirForward
	result := []*meta.Meta{}
	for {
		m, cost := tasks.next()
		if m == nil {
			break
		}
		result = append(result, m)

		for _, p := range m.ComputedPairsRest() {
			tasks.addPair(ctx, p.Key, p.Value, cost, isBackward, isForward)
		}
		if !spec.Full {
			continue
		}
		if tags, found := m.GetList(api.KeyTags); found {
			tasks.addTags(ctx, tags, cost)
		}
	}
	return result
}

type ztlCtxItem struct {
	cost float64
	meta *meta.Meta
}
type ztlCtxQueue []ztlCtxItem

func (q ztlCtxQueue) Len() int           { return len(q) }
func (q ztlCtxQueue) Less(i, j int) bool { return q[i].cost < q[j].cost }
func (q ztlCtxQueue) Swap(i, j int)      { q[i], q[j] = q[j], q[i] }
func (q *ztlCtxQueue) Push(x any)        { *q = append(*q, x.(ztlCtxItem)) }
func (q *ztlCtxQueue) Pop() any {
	old := *q
	n := len(old)
	item := old[n-1]
	old[n-1].meta = nil // avoid memory leak
	*q = old[0 : n-1]
	return item
}

type contextTask struct {
	port     ContextPort
	seen     *id.Set
	queue    ztlCtxQueue
	maxCost  float64
	limit    int
	tagMetas map[string][]*meta.Meta
	tagZids  map[string]*id.Set    // just the zids of tagMetas
	metaZid  map[id.Zid]*meta.Meta // maps zid to meta for all meta retrieved with tags
}

func newQueue(startSeq []*meta.Meta, maxCost float64, limit int, port ContextPort) *contextTask {
	result := &contextTask{
		port:     port,
		seen:     id.NewSet(),
		maxCost:  maxCost,
		limit:    limit,
		tagMetas: make(map[string][]*meta.Meta),
		tagZids:  make(map[string]*id.Set),
		metaZid:  make(map[id.Zid]*meta.Meta),
	}

	queue := make(ztlCtxQueue, 0, len(startSeq))
	for _, m := range startSeq {
		queue = append(queue, ztlCtxItem{cost: 1, meta: m})
	}
	heap.Init(&queue)
	result.queue = queue
	return result
}

func (ct *contextTask) addPair(ctx context.Context, key, value string, curCost float64, isBackward, isForward bool) {
	if key == api.KeyBack {
		return
	}
	newCost := curCost + contextCost(key)
	if key == api.KeyBackward {
		if isBackward {
			ct.addIDSet(ctx, newCost, value)
		}
		return
	}
	if key == api.KeyForward {
		if isForward {
			ct.addIDSet(ctx, newCost, value)
		}
		return
	}
	hasInverse := meta.Inverse(key) != ""
	if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
		return
	}
	if t := meta.Type(key); t == meta.TypeID {
		ct.addID(ctx, newCost, value)
	} else if t == meta.TypeIDSet {
		ct.addIDSet(ctx, newCost, value)
	}
}

func contextCost(key string) float64 {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 0.1
	case api.KeySequel, api.KeyPrequel:
		return 1.0
	case api.KeySuccessors, api.KeyPredecessor:
		return 7
	}
	return 2
}

func (ct *contextTask) addID(ctx context.Context, newCost float64, value string) {
	if zid, errParse := id.Parse(value); errParse == nil {
		if m, errGetMeta := ct.port.GetMeta(ctx, zid); errGetMeta == nil {
			ct.addMeta(m, newCost)
		}
	}
}

func (ct *contextTask) addMeta(m *meta.Meta, newCost float64) {
	// If len(zc.seen) <= 1, the initial zettel is processed. In this case allow all
	// other zettel that are directly reachable, without taking the cost into account.
	// Of course, the limit ist still relevant.
	if !ct.hasLimit() && (ct.seen.Length() <= 1 || ct.maxCost == 0 || newCost <= ct.maxCost) {
		if !ct.seen.Contains(m.Zid) {
			heap.Push(&ct.queue, ztlCtxItem{cost: newCost, meta: m})
		}
	}
}

func (ct *contextTask) addIDSet(ctx context.Context, newCost float64, value string) {
	elems := meta.ListFromValue(value)
	refCost := referenceCost(newCost, len(elems))
	for _, val := range elems {
		ct.addID(ctx, refCost, val)
	}
}

func referenceCost(baseCost float64, numReferences int) float64 {
	nRefs := float64(numReferences)
	return nRefs*math.Log2(nRefs+1) + baseCost
}

func (ct *contextTask) addTags(ctx context.Context, tags []string, baseCost float64) {
	var zidSet *id.Set
	for _, tag := range tags {
		zs := ct.updateTagData(ctx, tag)
		zidSet = zidSet.IUnion(zs)
	}
	zidSet.ForEach(func(zid id.Zid) {
		minCost := math.MaxFloat64
		costFactor := 1.1
		for _, tag := range tags {
			tagZids := ct.tagZids[tag]
			if tagZids.Contains(zid) {
				cost := tagCost(baseCost, tagZids.Length())
				if cost < minCost {
					minCost = cost
				}
				costFactor /= 1.1
			}
		}
		ct.addMeta(ct.metaZid[zid], minCost*costFactor)
	})
}

func (ct *contextTask) updateTagData(ctx context.Context, tag string) *id.Set {
	if _, found := ct.tagMetas[tag]; found {
		return ct.tagZids[tag]
	}
	q := Parse(api.KeyTags + api.SearchOperatorHas + tag + " ORDER REVERSE " + api.KeyID)
	ml, err := ct.port.SelectMeta(ctx, nil, q)
	if err != nil {
		ml = nil
	}
	ct.tagMetas[tag] = ml
	zids := id.NewSetCap(len(ml))
	for _, m := range ml {
		zid := m.Zid
		zids = zids.Add(zid)
		if _, found := ct.metaZid[zid]; !found {
			ct.metaZid[zid] = m
		}
	}
	ct.tagZids[tag] = zids
	return zids
}

func tagCost(baseCost float64, numTags int) float64 {
	nTags := float64(numTags)
	return nTags*math.Log2(nTags+1) + baseCost
}

func (ct *contextTask) next() (*meta.Meta, float64) {
	if ct.hasLimit() {
		return nil, -1
	}
	for len(ct.queue) > 0 {
		item := heap.Pop(&ct.queue).(ztlCtxItem)
		m := item.meta
		zid := m.Zid
		if ct.seen.Contains(zid) {
			continue
		}
		ct.seen.Add(zid)
		return m, item.cost
	}
	return nil, -1
}

func (ct *contextTask) hasLimit() bool {
	limit := ct.limit
	return limit > 0 && ct.seen.Length() >= limit
}

Changes to query/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
1

2
3
4
5
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) 2022 Detlef Stern
// 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 query

import (
	"strconv"

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

// Parse the query specification and return a Query object.
func Parse(spec string) (q *Query) { return q.Parse(spec) }

// Parse the query string and update the Query object.
func (q *Query) Parse(spec string) *Query {
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
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







+
-
+





+
-
-
+
+






-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-




+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+







-
+







-
+
+
+
+
+
+
+
+






-
+






-
+






-
+







+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+








type parserState struct {
	inp *input.Input
}

func (ps *parserState) mustStop() bool { return ps.inp.Ch == input.EOS }
func (ps *parserState) acceptSingleKw(s string) bool {
	inp := ps.inp
	if ps.inp.Accept(s) && (ps.isSpace() || ps.mustStop()) {
	if inp.Accept(s) && (inp.IsSpace() || ps.isActionSep() || ps.mustStop()) {
		return true
	}
	return false
}
func (ps *parserState) acceptKwArgs(s string) bool {
	inp := ps.inp
	if ps.inp.Accept(s) && ps.isSpace() {
		ps.skipSpace()
	if inp.Accept(s) && inp.IsSpace() {
		inp.SkipSpace()
		return true
	}
	return false
}

const (
	actionSeparatorChar      = '|'
	existOperatorChar        = '?'
	searchOperatorNotChar    = '!'
	searchOperatorHasChar    = ':'
	searchOperatorPrefixChar = '>'
	searchOperatorSuffixChar = '<'
	searchOperatorMatchChar  = '~'

	actionSeparatorChar       = '|'
	existOperatorChar         = '?'
	searchOperatorNotChar     = '!'
	searchOperatorEqualChar   = '='
	searchOperatorHasChar     = ':'
	searchOperatorPrefixChar  = '['
	searchOperatorSuffixChar  = ']'
	searchOperatorMatchChar   = '~'
	searchOperatorLessChar    = '<'
	kwLimit   = "LIMIT"
	kwOffset  = "OFFSET"
	kwOr      = "OR"
	searchOperatorGreaterChar = '>'
	kwOrder   = "ORDER"
	kwRandom  = "RANDOM"
	kwReverse = "REVERSE"
)

func (ps *parserState) parse(q *Query) *Query {
	inp := ps.inp
	inp.SkipSpace()
	if ps.mustStop() {
		return q
	}
	firstPos := inp.Pos
	zidSet := id.NewSet()
	for {
		pos := inp.Pos
		zid, found := ps.scanZid()
		if !found {
			inp.SetPos(pos)
			break
		}
		if !zidSet.Contains(zid) {
			zidSet.Add(zid)
			q = createIfNeeded(q)
			q.zids = append(q.zids, zid)
		}
		ps.skipSpace()
		inp.SkipSpace()
		if ps.mustStop() {
			q.zids = nil
			break
		}
	}

	hasContext := false
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.ContextDirective) {
			if hasContext {
				inp.SetPos(pos)
				break
			}
			q = ps.parseContext(q)
			hasContext = true
			continue
		}
		inp.SetPos(pos)
		if q == nil || len(q.zids) == 0 {
			break
		}
		if ps.acceptSingleKw(api.IdentDirective) {
			q.directives = append(q.directives, &IdentSpec{})
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ItemsDirective) {
			q.directives = append(q.directives, &ItemsSpec{})
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.UnlinkedDirective) {
			q = ps.parseUnlinked(q)
			continue
		}
		inp.SetPos(pos)
		break
	}
	if q != nil && len(q.directives) == 0 {
		inp.SetPos(firstPos) // No directive -> restart at beginning
		q.zids = nil
	}

	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(kwOr) {
		if ps.acceptSingleKw(api.OrDirective) {
			q = createIfNeeded(q)
			if !q.terms[len(q.terms)-1].isEmpty() {
				q.terms = append(q.terms, conjTerms{})
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(kwRandom) {
		if ps.acceptSingleKw(api.RandomDirective) {
			q = createIfNeeded(q)
			if len(q.order) == 0 {
				q.order = []sortOrder{{"", false}}
			}
			continue
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(kwOrder) {
		if ps.acceptKwArgs(api.PickDirective) {
			if s, ok := ps.parsePick(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.OrderDirective) {
			if s, ok := ps.parseOrder(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(kwOffset) {
		if ps.acceptKwArgs(api.OffsetDirective) {
			if s, ok := ps.parseOffset(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(kwLimit) {
		if ps.acceptKwArgs(api.LimitDirective) {
			if s, ok := ps.parseLimit(q); ok {
				q = s
				continue
			}
		}
		inp.SetPos(pos)
		if isActionSep(inp.Ch) {
		if ps.isActionSep() {
			q = ps.parseActions(q)
			break
		}
		q = ps.parseText(q)
	}
	return q
}

func (ps *parserState) parseContext(q *Query) *Query {
	inp := ps.inp
	spec := &ContextSpec{}
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptSingleKw(api.FullDirective) {
			spec.Full = true
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.BackwardDirective) {
			spec.Direction = ContextDirBackward
			continue
		}
		inp.SetPos(pos)
		if ps.acceptSingleKw(api.ForwardDirective) {
			spec.Direction = ContextDirForward
			continue
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.CostDirective) {
			if ps.parseCost(spec) {
				continue
			}
		}
		inp.SetPos(pos)
		if ps.acceptKwArgs(api.MaxDirective) {
			if ps.parseCount(spec) {
				continue
			}
		}

		inp.SetPos(pos)
		break
	}
	q = createIfNeeded(q)
	q.directives = append(q.directives, spec)
	return q
}
func (ps *parserState) parseCost(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MaxCost == 0 || spec.MaxCost >= num {
		spec.MaxCost = num
	}
	return true
}
func (ps *parserState) parseCount(spec *ContextSpec) bool {
	num, ok := ps.scanPosInt()
	if !ok {
		return false
	}
	if spec.MaxCount == 0 || spec.MaxCount >= num {
		spec.MaxCount = num
	}
	return true
}

func (ps *parserState) parseUnlinked(q *Query) *Query {
	inp := ps.inp

	spec := &UnlinkedSpec{}
	for {
		inp.SkipSpace()
		if ps.mustStop() {
			break
		}
		pos := inp.Pos
		if ps.acceptKwArgs(api.PhraseDirective) {
			if word := ps.scanWord(); len(word) > 0 {
				spec.words = append(spec.words, string(word))
				continue
			}
		}

		inp.SetPos(pos)
		break
	}
	q.directives = append(q.directives, spec)
	return q
}

func (ps *parserState) parsePick(q *Query) (*Query, bool) {
	num, ok := ps.scanPosInt()
	if !ok {
		return q, false
	}
	q = createIfNeeded(q)
	if q.pick == 0 || q.pick >= num {
		q.pick = num
	}
	return q, true
}

func (ps *parserState) parseOrder(q *Query) (*Query, bool) {
	reverse := false
	if ps.acceptKwArgs(kwReverse) {
	if ps.acceptKwArgs(api.ReverseDirective) {
		reverse = true
	}
	word := ps.scanWord()
	if len(word) == 0 {
		return q, false
	}
	if sWord := string(word); meta.KeyIsValid(sWord) {
165
166
167
168
169
170
171

172

173
174
175

176
177
178
179
180
181
182
342
343
344
345
346
347
348
349

350
351
352

353
354
355
356
357
358
359
360







+
-
+


-
+







	if q.limit == 0 || q.limit >= num {
		q.limit = num
	}
	return q, true
}

func (ps *parserState) parseActions(q *Query) *Query {
	inp := ps.inp
	ps.inp.Next()
	inp.Next()
	var words []string
	for {
		ps.skipSpace()
		inp.SkipSpace()
		word := ps.scanWord()
		if len(word) == 0 {
			break
		}
		words = append(words, string(word))
	}
	if len(words) > 0 {
196
197
198
199
200
201
202
203

204
205
206
207
208
209
210
374
375
376
377
378
379
380

381
382
383
384
385
386
387
388







-
+







	}
	text, key := ps.scanSearchTextOrKey(hasOp)
	if len(key) > 0 {
		// Assert: hasOp == false
		op, hasOp = ps.scanSearchOp()
		// Assert hasOp == true
		if op == cmpExist || op == cmpNotExist {
			if ps.isSpace() || isActionSep(inp.Ch) || ps.mustStop() {
			if inp.IsSpace() || ps.isActionSep() || ps.mustStop() {
				return q.addKey(string(key), op)
			}
			ps.inp.SetPos(pos)
			hasOp = false
			text = ps.scanWord()
			key = nil
		} else {
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
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

















-
+


-
-
+
+
+
+














-
+






-
-
-
-
-
-
-
-
-










+
+
+
+
+
+
+
+
+
+
+
+
+














+
+
+












+
+
+
+
+
+












-
+
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
}

func (ps *parserState) scanSearchTextOrKey(hasOp bool) ([]byte, []byte) {
	inp := ps.inp
	pos := inp.Pos
	allowKey := !hasOp

	for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
	for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
		if allowKey {
			switch inp.Ch {
			case searchOperatorNotChar, existOperatorChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar:
			case searchOperatorNotChar, existOperatorChar,
				searchOperatorEqualChar, searchOperatorHasChar,
				searchOperatorPrefixChar, searchOperatorSuffixChar, searchOperatorMatchChar,
				searchOperatorLessChar, searchOperatorGreaterChar:
				allowKey = false
				if key := inp.Src[pos:inp.Pos]; meta.KeyIsValid(string(key)) {
					return nil, key
				}
			}
		}
		inp.Next()
	}
	return inp.Src[pos:inp.Pos], nil
}

func (ps *parserState) scanWord() []byte {
	inp := ps.inp
	pos := inp.Pos
	for !ps.isSpace() && !isActionSep(inp.Ch) && !ps.mustStop() {
	for !inp.IsSpace() && !ps.isActionSep() && !ps.mustStop() {
		inp.Next()
	}
	return inp.Src[pos:inp.Pos]
}

func (ps *parserState) scanPosInt() (int, bool) {
	inp := ps.inp
	ch := inp.Ch
	if ch == '0' {
		ch = inp.Next()
		if isSpace(ch) || isActionSep(inp.Ch) || ps.mustStop() {
			return 0, true
		}
		return 0, false
	}
	word := ps.scanWord()
	if len(word) == 0 {
		return 0, false
	}
	uval, err := strconv.ParseUint(string(word), 10, 63)
	if err != nil {
		return 0, false
	}
	return int(uval), true
}

func (ps *parserState) scanZid() (id.Zid, bool) {
	word := ps.scanWord()
	if len(word) == 0 {
		return id.Invalid, false
	}
	uval, err := id.ParseUint(string(word))
	if err != nil {
		return id.Invalid, false
	}
	zid := id.Zid(uval)
	return zid, zid.IsValid()
}

func (ps *parserState) scanSearchOp() (compareOp, bool) {
	inp := ps.inp
	ch := inp.Ch
	negate := false
	if ch == searchOperatorNotChar {
		ch = inp.Next()
		negate = true
	}
	op := cmpUnknown
	switch ch {
	case existOperatorChar:
		inp.Next()
		op = cmpExist
	case searchOperatorEqualChar:
		inp.Next()
		op = cmpEqual
	case searchOperatorHasChar:
		inp.Next()
		op = cmpHas
	case searchOperatorSuffixChar:
		inp.Next()
		op = cmpSuffix
	case searchOperatorPrefixChar:
		inp.Next()
		op = cmpPrefix
	case searchOperatorMatchChar:
		inp.Next()
		op = cmpMatch
	case searchOperatorLessChar:
		inp.Next()
		op = cmpLess
	case searchOperatorGreaterChar:
		inp.Next()
		op = cmpGreater
	default:
		if negate {
			return cmpNoMatch, true
		}
		return cmpUnknown, false
	}
	if negate {
		return op.negate(), true
	}
	return op, true
}

func (ps *parserState) isSpace() bool {
func (ps *parserState) isActionSep() bool {
	return isSpace(ps.inp.Ch)
}

func isSpace(ch rune) bool {
	switch ch {
	case input.EOS:
		return false
	return ps.inp.Ch == actionSeparatorChar
	case ' ', '\t', '\n', '\r':
		return true
	}
}
	return input.IsSpace(ch)
}

func (ps *parserState) skipSpace() {
	for ps.isSpace() {
		ps.inp.Next()
	}
}

func isActionSep(ch rune) bool { return ch == actionSeparatorChar }

Changes to query/parser_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

65
66
67
68


69
70
71
72
73


74
75
76
77
78
79
80


81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

99
100
101

102
103
104
105
106
107
108
109

-
+






+
+
+
















+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+

+

-
-
+
+

+

-
-
+
+

+
+
+

-
-
+
+

+
+



+
+








-
+


-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 query_test

import (
	"testing"

	"zettelstore.de/z/query"
)

func TestParser(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		spec string
		exp  string
	}{
		{"1", "1"}, // Just a number will transform to search for that number in all zettel

		{"1 IDENT", "00000000000001 IDENT"},
		{"IDENT", "IDENT"},
		{"1 IDENT|REINDEX", "00000000000001 IDENT | REINDEX"},

		{"1 ITEMS", "00000000000001 ITEMS"},
		{"ITEMS", "ITEMS"},

		{"CONTEXT", "CONTEXT"}, {"CONTEXT a", "CONTEXT a"},
		{"0 CONTEXT", "0 CONTEXT"}, {"1 CONTEXT", "00000000000001 CONTEXT"},
		{"00000000000001 CONTEXT", "00000000000001 CONTEXT"},
		{"100000000000001 CONTEXT", "100000000000001 CONTEXT"},
		{"1 CONTEXT FULL", "00000000000001 CONTEXT FULL"},
		{"1 CONTEXT BACKWARD", "00000000000001 CONTEXT BACKWARD"},
		{"1 CONTEXT FORWARD", "00000000000001 CONTEXT FORWARD"},
		{"1 CONTEXT COST ", "00000000000001 CONTEXT COST"},
		{"1 CONTEXT COST 3", "00000000000001 CONTEXT COST 3"}, {"1 CONTEXT COST x", "00000000000001 CONTEXT COST x"},
		{"1 CONTEXT MAX 5", "00000000000001 CONTEXT MAX 5"}, {"1 CONTEXT MAX y", "00000000000001 CONTEXT MAX y"},
		{"1 CONTEXT MAX 5 COST 7", "00000000000001 CONTEXT COST 7 MAX 5"},
		{"1 CONTEXT |  N", "00000000000001 CONTEXT | N"},
		{"1 1 CONTEXT", "00000000000001 CONTEXT"},
		{"1 2 CONTEXT", "00000000000001 00000000000002 CONTEXT"},
		{"2 1 CONTEXT", "00000000000002 00000000000001 CONTEXT"},
		{"1 CONTEXT|N", "00000000000001 CONTEXT | N"},

		{"CONTEXT 0", "CONTEXT 0"},

		{"1 UNLINKED", "00000000000001 UNLINKED"},
		{"UNLINKED", "UNLINKED"},
		{"1 UNLINKED PHRASE", "00000000000001 UNLINKED PHRASE"},
		{"1 UNLINKED PHRASE Zettel", "00000000000001 UNLINKED PHRASE Zettel"},

		{"?", "?"}, {"!?", "!?"}, {"?a", "?a"}, {"!?a", "!?a"},
		{"key?", "key?"}, {"key!?", "key!?"},
		{"b key?", "key? b"}, {"b key!?", "key!? b"},
		{"key?a", "key?a"}, {"key!?a", "key!?a"},
		{"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {">", ""}, {"!>", ""}, {"<", ""}, {"!<", ""}, {"~", ""}, {"!~", ""},
		{"", ""}, {"!", ""}, {":", ""}, {"!:", ""}, {"[", ""}, {"![", ""}, {"]", ""}, {"!]", ""}, {"~", ""}, {"!~", ""}, {"<", ""}, {"!<", ""}, {">", ""}, {"!>", ""},
		{`a`, `a`}, {`!a`, `!a`},
		{`=a`, `=a`}, {`!=a`, `!=a`},
		{`:a`, `:a`}, {`!:a`, `!:a`},
		{`>a`, `>a`}, {`!>a`, `!>a`},
		{`<a`, `<a`}, {`!<a`, `!<a`},
		{`[a`, `[a`}, {`![a`, `![a`},
		{`]a`, `]a`}, {`!]a`, `!]a`},
		{`~a`, `a`}, {`!~a`, `!a`},
		{`key=`, `key=`}, {`key!=`, `key!=`},
		{`key:`, `key:`}, {`key!:`, `key!:`},
		{`key>`, `key>`}, {`key!>`, `key!>`},
		{`key<`, `key<`}, {`key!<`, `key!<`},
		{`key[`, `key[`}, {`key![`, `key![`},
		{`key]`, `key]`}, {`key!]`, `key!]`},
		{`key~`, `key~`}, {`key!~`, `key!~`},
		{`key<`, `key<`}, {`key!<`, `key!<`},
		{`key>`, `key>`}, {`key!>`, `key!>`},
		{`key=a`, `key=a`}, {`key!=a`, `key!=a`},
		{`key:a`, `key:a`}, {`key!:a`, `key!:a`},
		{`key>a`, `key>a`}, {`key!>a`, `key!>a`},
		{`key<a`, `key<a`}, {`key!<a`, `key!<a`},
		{`key[a`, `key[a`}, {`key![a`, `key![a`},
		{`key]a`, `key]a`}, {`key!]a`, `key!]a`},
		{`key~a`, `key~a`}, {`key!~a`, `key!~a`},
		{`key<a`, `key<a`}, {`key!<a`, `key!<a`},
		{`key>a`, `key>a`}, {`key!>a`, `key!>a`},
		{`key1:a key2:b`, `key1:a key2:b`},
		{`key1: key2:b`, `key1: key2:b`},
		{"word key:a", "key:a word"},
		{`PICK 3`, `PICK 3`}, {`PICK 9 PICK 11`, `PICK 9`},
		{"PICK a", "PICK a"},
		{`RANDOM`, `RANDOM`}, {`RANDOM a`, `a RANDOM`}, {`a RANDOM`, `a RANDOM`},
		{`RANDOM RANDOM a`, `a RANDOM`},
		{`RANDOMRANDOM a`, `RANDOMRANDOM a`}, {`a RANDOMRANDOM`, `a RANDOMRANDOM`},
		{`ORDER`, `ORDER`}, {"ORDER a b", "b ORDER a"}, {"a ORDER", "a ORDER"}, {"ORDER %", "ORDER %"},
		{"ORDER a %", "% ORDER a"},
		{"ORDER REVERSE", "ORDER REVERSE"}, {"ORDER REVERSE a b", "b ORDER REVERSE a"},
		{"a RANDOM ORDER b", "a ORDER b"}, {"a ORDER b RANDOM", "a ORDER b"},
		{"OFFSET", "OFFSET"}, {"OFFSET a", "OFFSET a"}, {"OFFSET 10 a", "a OFFSET 10"},
		{"OFFSET 01 a", "OFFSET 01 a"}, {"OFFSET 0 a", "a"}, {"a OFFSET 0", "a"},
		{"OFFSET 01 a", "a OFFSET 1"}, {"OFFSET 0 a", "a"}, {"a OFFSET 0", "a"},
		{"OFFSET 4 OFFSET 8", "OFFSET 8"}, {"OFFSET 8 OFFSET 4", "OFFSET 8"},
		{"LIMIT", "LIMIT"}, {"LIMIT a", "LIMIT a"}, {"LIMIT 10 a", "a LIMIT 10"},
		{"LIMIT 01 a", "LIMIT 01 a"}, {"LIMIT 0 a", "a"}, {"a LIMIT 0", "a"},
		{"LIMIT 01 a", "a LIMIT 1"}, {"LIMIT 0 a", "a"}, {"a LIMIT 0", "a"},
		{"LIMIT 4 LIMIT 8", "LIMIT 4"}, {"LIMIT 8 LIMIT 4", "LIMIT 4"},
		{"OR", ""}, {"OR OR", ""}, {"a OR", "a"}, {"OR b", "b"}, {"OR a OR", "a"},
		{"a OR b", "a OR b"},
		{"|", ""}, {" | RANDOM", "| RANDOM"}, {"| RANDOM", "| RANDOM"}, {"a|a b ", "a | a b"},
	}
	for i, tc := range testcases {
		got := query.Parse(tc.spec).String()

Changes to query/print.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
1

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

-
+






+
+
+









-
-
+
+
+



-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+













-
+
+
+
+
+










-
-
-
+
-









+

-
-
+
+



+
-
+






-
+






-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 query

import (
	"io"
	"strconv"
	"strings"

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

var op2string = map[compareOp]string{
	cmpExist:    api.ExistOperator,
	cmpNotExist: api.ExistNotOperator,
	cmpHas:      api.SearchOperatorHas,
	cmpHasNot:   api.SearchOperatorHasNot,
	cmpPrefix:   api.SearchOperatorPrefix,
	cmpNoPrefix: api.SearchOperatorNoPrefix,
	cmpSuffix:   api.SearchOperatorSuffix,
	cmpNoSuffix: api.SearchOperatorNoSuffix,
	cmpMatch:    api.SearchOperatorMatch,
	cmpNoMatch:  api.SearchOperatorNoMatch,
	cmpExist:     api.ExistOperator,
	cmpNotExist:  api.ExistNotOperator,
	cmpEqual:     api.SearchOperatorEqual,
	cmpNotEqual:  api.SearchOperatorNotEqual,
	cmpHas:       api.SearchOperatorHas,
	cmpHasNot:    api.SearchOperatorHasNot,
	cmpPrefix:    api.SearchOperatorPrefix,
	cmpNoPrefix:  api.SearchOperatorNoPrefix,
	cmpSuffix:    api.SearchOperatorSuffix,
	cmpNoSuffix:  api.SearchOperatorNoSuffix,
	cmpMatch:     api.SearchOperatorMatch,
	cmpNoMatch:   api.SearchOperatorNoMatch,
	cmpLess:      api.SearchOperatorLess,
	cmpNoLess:    api.SearchOperatorNotLess,
	cmpGreater:   api.SearchOperatorGreater,
	cmpNoGreater: api.SearchOperatorNotGreater,
}

func (q *Query) String() string {
	var sb strings.Builder
	q.Print(&sb)
	return sb.String()
}

// Print the query in a parseable form.
func (q *Query) Print(w io.Writer) {
	if q == nil {
		return
	}
	env := printEnv{w: w}
	env := PrintEnv{w: w}
	env.printZids(q.zids)
	for _, d := range q.directives {
		d.Print(&env)
	}
	for i, term := range q.terms {
		if i > 0 {
			env.writeString(" OR")
		}
		for _, name := range maps.Keys(term.keys) {
			env.printSpace()
			env.writeString(name)
			if op := term.keys[name]; op == cmpExist || op == cmpNotExist {
				env.writeString(op2string[op])
			} else {
				env.writeString(api.ExistOperator)
				env.printSpace()
				env.writeString(name)
				env.writeStrings(api.ExistOperator, " ", name, api.ExistNotOperator)
				env.writeString(api.ExistNotOperator)
			}
		}
		for _, name := range maps.Keys(term.mvals) {
			env.printExprValues(name, term.mvals[name])
		}
		if len(term.search) > 0 {
			env.printExprValues("", term.search)
		}
	}
	env.printPosInt(api.PickDirective, q.pick)
	env.printOrder(q.order)
	env.printPosInt(kwOffset, q.offset)
	env.printPosInt(kwLimit, q.limit)
	env.printPosInt(api.OffsetDirective, q.offset)
	env.printPosInt(api.LimitDirective, q.limit)
	env.printActions(q.actions)
}

// PrintEnv is an environment where queries are printed.
type printEnv struct {
type PrintEnv struct {
	w     io.Writer
	space bool
}

var bsSpace = []byte{' '}

func (pe *printEnv) printSpace() {
func (pe *PrintEnv) printSpace() {
	if pe.space {
		pe.w.Write(bsSpace)
		return
	}
	pe.space = true
}
func (pe *printEnv) write(ch byte)        { pe.w.Write([]byte{ch}) }
func (pe *printEnv) writeString(s string) { io.WriteString(pe.w, s) }

func (pe *printEnv) printExprValues(key string, values []expValue) {
func (pe *PrintEnv) write(ch byte)        { pe.w.Write([]byte{ch}) }
func (pe *PrintEnv) writeString(s string) { io.WriteString(pe.w, s) }
func (pe *PrintEnv) writeStrings(sSeq ...string) {
	for _, s := range sSeq {
		io.WriteString(pe.w, s)
	}
}

func (pe *PrintEnv) printZids(zids []id.Zid) {
	for i, zid := range zids {
		if i > 0 {
			pe.printSpace()
		}
		pe.writeString(zid.String())
		pe.space = true
	}
}
func (pe *PrintEnv) printExprValues(key string, values []expValue) {
	for _, val := range values {
		pe.printSpace()
		pe.writeString(key)
		switch op := val.op; op {
		case cmpMatch:
			// An empty key signals a full-text search. Since "~" is the default op in this case,
			// it can be ignored. Therefore, print only "~" if there is a key.
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
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







+











-
+
+
+
+
+







		}
		if s := val.value; s != "" {
			pe.writeString(s)
		}
	}
}

// Human returns the query as a human readable string.
func (q *Query) Human() string {
	var sb strings.Builder
	q.PrintHuman(&sb)
	return sb.String()
}

// PrintHuman the query to a writer in a human readable form.
func (q *Query) PrintHuman(w io.Writer) {
	if q == nil {
		return
	}
	env := printEnv{w: w}
	env := PrintEnv{w: w}
	env.printZids(q.zids)
	for _, d := range q.directives {
		d.Print(&env)
	}
	for i, term := range q.terms {
		if i > 0 {
			env.writeString(" OR ")
			env.space = false
		}
		for _, name := range maps.Keys(term.keys) {
			if env.space {
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
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







+

-
-
+
+



-
+










+
+
+
+
















+
+
+
+
+
+
+
+











-
+



-
+
-
-



-
+


-
+






-
+


-
+
-
-



-
+









			}
			env.writeString("ANY")
			env.printHumanSelectExprValues(term.search)
			env.space = true
		}
	}

	env.printPosInt(api.PickDirective, q.pick)
	env.printOrder(q.order)
	env.printPosInt(kwOffset, q.offset)
	env.printPosInt(kwLimit, q.limit)
	env.printPosInt(api.OffsetDirective, q.offset)
	env.printPosInt(api.LimitDirective, q.limit)
	env.printActions(q.actions)
}

func (pe *printEnv) printHumanSelectExprValues(values []expValue) {
func (pe *PrintEnv) printHumanSelectExprValues(values []expValue) {
	if len(values) == 0 {
		pe.writeString(" MATCH ANY")
		return
	}

	for j, val := range values {
		if j > 0 {
			pe.writeString(" AND")
		}
		switch val.op {
		case cmpEqual:
			pe.writeString(" EQUALS ")
		case cmpNotEqual:
			pe.writeString(" EQUALS NOT ")
		case cmpHas:
			pe.writeString(" HAS ")
		case cmpHasNot:
			pe.writeString(" HAS NOT ")
		case cmpPrefix:
			pe.writeString(" PREFIX ")
		case cmpNoPrefix:
			pe.writeString(" NOT PREFIX ")
		case cmpSuffix:
			pe.writeString(" SUFFIX ")
		case cmpNoSuffix:
			pe.writeString(" NOT SUFFIX ")
		case cmpMatch:
			pe.writeString(" MATCH ")
		case cmpNoMatch:
			pe.writeString(" NOT MATCH ")
		case cmpLess:
			pe.writeString(" LESS ")
		case cmpNoLess:
			pe.writeString(" NOT LESS ")
		case cmpGreater:
			pe.writeString(" GREATER ")
		case cmpNoGreater:
			pe.writeString(" NOT GREATER ")
		default:
			pe.writeString(" MaTcH ")
		}
		if val.value == "" {
			pe.writeString("NOTHING")
		} else {
			pe.writeString(val.value)
		}
	}
}

func (pe *printEnv) printOrder(order []sortOrder) {
func (pe *PrintEnv) printOrder(order []sortOrder) {
	for _, o := range order {
		if o.isRandom() {
			pe.printSpace()
			pe.writeString(kwRandom)
			pe.writeString(api.RandomDirective)
			continue
		} else if o.key == api.KeyID && o.descending {
			continue
		}
		pe.printSpace()
		pe.writeString(kwOrder)
		pe.writeString(api.OrderDirective)
		if o.descending {
			pe.printSpace()
			pe.writeString(kwReverse)
			pe.writeString(api.ReverseDirective)
		}
		pe.printSpace()
		pe.writeString(o.key)
	}
}

func (pe *printEnv) printPosInt(key string, val int) {
func (pe *PrintEnv) printPosInt(key string, val int) {
	if val > 0 {
		pe.printSpace()
		pe.writeString(key)
		pe.writeStrings(key, " ", strconv.Itoa(val))
		pe.writeString(" ")
		pe.writeString(strconv.Itoa(val))
	}
}

func (pe *printEnv) printActions(words []string) {
func (pe *PrintEnv) printActions(words []string) {
	if len(words) > 0 {
		pe.printSpace()
		pe.write(actionSeparatorChar)
		for _, word := range words {
			pe.printSpace()
			pe.writeString(word)
		}
	}
}

Changes to query/query.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14

15
16


17
18
19


20
21
22
23
24
25
26

27
28
29
30

31
32
33
34

35
36
37
38

39
40
41
42






43
44
45
46
47
48


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














68
69
70
71
72
73
74
75
76









77
78
79
80
81
82
83
84
85
1

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

-
+






+
+
+






+
-
-
+
+

-
-
+
+






-
+



-
+



-
+



-
+




+
+
+
+
+
+






+
+










-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-

-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 query provides a query for zettel.
package query

import (
	"context"
	"math/rand"
	"sort"
	"math/rand/v2"
	"slices"

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

// Searcher is used to select zettel identifier based on search criteria.
type Searcher interface {
	// Select all zettel that contains the given exact word.
	// The word must be normalized through Unicode NKFD, trimmed and not empty.
	SearchEqual(word string) id.Set
	SearchEqual(word string) *id.Set

	// Select all zettel that have a word with the given prefix.
	// The prefix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchPrefix(prefix string) id.Set
	SearchPrefix(prefix string) *id.Set

	// Select all zettel that have a word with the given suffix.
	// The suffix must be normalized through Unicode NKFD, trimmed and not empty.
	SearchSuffix(suffix string) id.Set
	SearchSuffix(suffix string) *id.Set

	// Select all zettel that contains the given string.
	// The string must be normalized through Unicode NKFD, trimmed and not empty.
	SearchContains(s string) id.Set
	SearchContains(s string) *id.Set
}

// Query specifies a mechanism for querying zettel.
type Query struct {
	// Präfixed zettel identifier.
	zids []id.Zid

	// Querydirectives, like CONTEXT, ...
	directives []Directive

	// Fields to be used for selecting
	preMatch MetaMatchFunc // Match that must be true
	terms    []conjTerms

	// Allow to create predictable randomness
	seed int

	pick int // Randomly pick elements, <= 0: no pick

	// Fields to be used for sorting
	order  []sortOrder
	offset int // <= 0: no offset
	limit  int // <= 0: no limit

	// Execute specification
	actions []string
}

// Compiled is a compiled query, to be used in a Box
type Compiled struct {
	PreMatch MetaMatchFunc // Precondition for Match and Retrieve
	Terms    []CompiledTerm
}

// MetaMatchFunc is a function determine whethe some metadata should be selected or not.
type MetaMatchFunc func(*meta.Meta) bool

// GetZids returns a slide of all specified zettel identifier.
func (q *Query) GetZids() []id.Zid {
	if q == nil || len(q.zids) == 0 {
		return nil
	}
	result := make([]id.Zid, len(q.zids))
	copy(result, q.zids)
	return result
}

// Directive are executed to process the list of metadata.
type Directive interface {
	Print(*PrintEnv)
}
func matchAlways(*meta.Meta) bool { return true }
func matchNever(*meta.Meta) bool  { return false }

// CompiledTerm is the preprocessed sequence of conjugated search terms.
type CompiledTerm struct {
	Match    MetaMatchFunc     // Match on metadata
	Retrieve RetrievePredicate // Retrieve from full-text search
}

// GetDirectives returns the slice of query directives.
func (q *Query) GetDirectives() []Directive {
	if q == nil || len(q.directives) == 0 {
		return nil
	}
	result := make([]Directive, len(q.directives))
	copy(result, q.directives)
	return result
}
// RetrievePredicate returns true, if the given Zid is contained in the (full-text) search.
type RetrievePredicate func(id.Zid) bool

type keyExistMap map[string]compareOp
type expMetaValues map[string][]expValue

type conjTerms struct {
	keys   keyExistMap
	mvals  expMetaValues // Expected values for a meta datum
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
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







+
+
+
+
+
+
+
+
+




















+















+
+








+
+
+
+



-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+





-
-
-
-
-
+
+
+
+
+
+
+
+
















+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+








// Clone the query value.
func (q *Query) Clone() *Query {
	if q == nil {
		return nil
	}
	c := new(Query)
	if len(q.zids) > 0 {
		c.zids = make([]id.Zid, len(q.zids))
		copy(c.zids, q.zids)
	}
	if len(q.directives) > 0 {
		c.directives = make([]Directive, len(q.directives))
		copy(c.directives, q.directives)
	}

	c.preMatch = q.preMatch
	c.terms = make([]conjTerms, len(q.terms))
	for i, term := range q.terms {
		if len(term.keys) > 0 {
			c.terms[i].keys = make(keyExistMap, len(term.keys))
			for k, v := range term.keys {
				c.terms[i].keys[k] = v
			}
		}
		// if len(c.mvals) > 0 {
		c.terms[i].mvals = make(expMetaValues, len(term.mvals))
		for k, v := range term.mvals {
			c.terms[i].mvals[k] = v
		}
		// }
		if len(term.search) > 0 {
			c.terms[i].search = append([]expValue{}, term.search...)
		}
	}
	c.seed = q.seed
	c.pick = q.pick
	if len(q.order) > 0 {
		c.order = append([]sortOrder{}, q.order...)
	}
	c.offset = q.offset
	c.limit = q.limit
	c.actions = q.actions
	return c
}

type compareOp uint8

const (
	cmpUnknown compareOp = iota
	cmpExist
	cmpNotExist
	cmpEqual
	cmpNotEqual
	cmpHas
	cmpHasNot
	cmpPrefix
	cmpNoPrefix
	cmpSuffix
	cmpNoSuffix
	cmpMatch
	cmpNoMatch
	cmpLess
	cmpNoLess
	cmpGreater
	cmpNoGreater
)

var negateMap = map[compareOp]compareOp{
	cmpUnknown:  cmpUnknown,
	cmpExist:    cmpNotExist,
	cmpHas:      cmpHasNot,
	cmpHasNot:   cmpHas,
	cmpPrefix:   cmpNoPrefix,
	cmpNoPrefix: cmpPrefix,
	cmpSuffix:   cmpNoSuffix,
	cmpNoSuffix: cmpSuffix,
	cmpMatch:    cmpNoMatch,
	cmpNoMatch:  cmpMatch,
	cmpUnknown:   cmpUnknown,
	cmpExist:     cmpNotExist,
	cmpEqual:     cmpNotEqual,
	cmpNotEqual:  cmpEqual,
	cmpHas:       cmpHasNot,
	cmpHasNot:    cmpHas,
	cmpPrefix:    cmpNoPrefix,
	cmpNoPrefix:  cmpPrefix,
	cmpSuffix:    cmpNoSuffix,
	cmpNoSuffix:  cmpSuffix,
	cmpMatch:     cmpNoMatch,
	cmpNoMatch:   cmpMatch,
	cmpLess:      cmpNoLess,
	cmpNoLess:    cmpLess,
	cmpGreater:   cmpNoGreater,
	cmpNoGreater: cmpGreater,
}

func (op compareOp) negate() compareOp { return negateMap[op] }

var negativeMap = map[compareOp]bool{
	cmpNotExist: true,
	cmpHasNot:   true,
	cmpNoPrefix: true,
	cmpNoSuffix: true,
	cmpNoMatch:  true,
	cmpNotExist:  true,
	cmpNotEqual:  true,
	cmpHasNot:    true,
	cmpNoPrefix:  true,
	cmpNoSuffix:  true,
	cmpNoMatch:   true,
	cmpNoLess:    true,
	cmpNoGreater: true,
}

func (op compareOp) isNegated() bool { return negativeMap[op] }

type expValue struct {
	value string
	op    compareOp
}

func (q *Query) addSearch(val expValue) { q.terms[len(q.terms)-1].addSearch(val) }

func (q *Query) addKey(key string, op compareOp) *Query {
	q = createIfNeeded(q)
	q.terms[len(q.terms)-1].addKey(key, op)
	return q
}

var missingMap = map[compareOp]bool{
	cmpNotExist: true,
	cmpNotEqual: true,
	cmpHasNot:   true,
	cmpNoMatch:  true,
}

// GetMetaValues returns the slice of all values specified for a given metadata key.
// If `withMissing` is true, all values are returned. Otherwise only those,
// where the comparison operator will positively search for a value.
func (q *Query) GetMetaValues(key string, withMissing bool) (vals []string) {
	if q == nil {
		return nil
	}
	for _, term := range q.terms {
		if mvs, hasMv := term.mvals[key]; hasMv {
			for _, ev := range mvs {
				if withMissing || !missingMap[ev.op] {
					vals = append(vals, ev.value)
				}
			}
		}
	}
	slices.Sort(vals)
	return slices.Compact(vals)
}

// SetPreMatch sets the pre-selection predicate.
func (q *Query) SetPreMatch(preMatch MetaMatchFunc) *Query {
	q = createIfNeeded(q)
	if q.preMatch != nil {
		panic("search PreMatch already set")
	}
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
298
299
300
301
302
303
304


305
306
307


308





309




310

311
312
313
314
315
316
317
318







-
-
+
+

-
-
+
-
-
-
-
-
+
-
-
-
-

-
+







func (q *Query) GetSeed() (int, bool) {
	if q == nil {
		return 0, false
	}
	return q.seed, q.seed > 0
}

// SetLimit sets the given limit of the query object.
func (q *Query) SetLimit(limit int) *Query {
// SetDeterministic signals that the result should be the same if the seed is the same.
func (q *Query) SetDeterministic() *Query {
	q = createIfNeeded(q)
	if limit < 0 {
		limit = 0
	if q.seed <= 0 {
	}
	q.limit = limit
	return q
}

		q.seed = int(rand.IntN(10000) + 1)
// GetLimit returns the current offset value.
func (q *Query) GetLimit() int {
	if q == nil {
		return 0
	}
	return q.limit
	return q
}

// Actions returns the slice of action specifications
func (q *Query) Actions() []string {
	if q == nil {
		return nil
	}
268
269
270
271
272
273
274
275



276
277

278
279
280
281
282
283
284
327
328
329
330
331
332
333
334
335
336
337
338

339
340
341
342
343
344
345
346








+
+
+

-
+







}

// EnrichNeeded returns true, if the query references a metadata key that
// is calculated via metadata enrichments.
func (q *Query) EnrichNeeded() bool {
	if q == nil {
		return false
	}
	if len(q.zids) > 0 {
		return true
	}
	if len(q.actions) > 0 {
		// Unknown, what an action will use. Example: RSS needs api.KeyPublished.
		// Unknown, what an action may use. For examples: KEYS action uses all metadata.
		return true
	}
	for _, term := range q.terms {
		for key := range term.keys {
			if meta.IsProperty(key) {
				return true
			}
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
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







-
+





-
+








+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+

-
+



-
-
-
-
-
-
+
+
+
+
+

-
+









+
+
+
+
+
+
+
+
+
+
+
-
+




+
+
+
+
+
+
+
+
+
+
+
+
+



















-
+

-
+








-
-
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
+
-
-
-



-
-
-
-
-
-
-
-
-
-
-





-
-
-
-
+

		}
	}
	return false
}

// RetrieveAndCompile queries the search index and returns a predicate
// for its results and returns a matching predicate.
func (q *Query) RetrieveAndCompile(searcher Searcher) Compiled {
func (q *Query) RetrieveAndCompile(_ context.Context, searcher Searcher, metaSeq []*meta.Meta) Compiled {
	if q == nil {
		return Compiled{
			PreMatch: matchAlways,
			Terms: []CompiledTerm{{
				Match:    matchAlways,
				Retrieve: alwaysIncluded,
				Retrieve: AlwaysIncluded,
			}}}
	}
	q = q.Clone()

	preMatch := q.preMatch
	if preMatch == nil {
		preMatch = matchAlways
	}

	startSet := metaList2idSet(metaSeq)
	result := Compiled{PreMatch: preMatch}

	result := Compiled{
		hasQuery:  true,
		seed:      q.seed,
		pick:      q.pick,
		order:     q.order,
		offset:    q.offset,
		limit:     q.limit,
		startMeta: metaSeq,
		PreMatch:  preMatch,
		Terms:     []CompiledTerm{},
	}

	for _, term := range q.terms {
		cTerm := term.retrievAndCompileTerm(searcher)
		cTerm := term.retrieveAndCompileTerm(searcher, startSet)
		if cTerm.Retrieve == nil {
			if cTerm.Match == nil {
				// no restriction on match/retrieve -> all will match
				return Compiled{
					PreMatch: preMatch,
					Terms: []CompiledTerm{{
						Match:    matchAlways,
						Retrieve: alwaysIncluded,
					}}}
				result.Terms = []CompiledTerm{{
					Match:    matchAlways,
					Retrieve: AlwaysIncluded,
				}}
				break
			}
			cTerm.Retrieve = alwaysIncluded
			cTerm.Retrieve = AlwaysIncluded
		}
		if cTerm.Match == nil {
			cTerm.Match = matchAlways
		}
		result.Terms = append(result.Terms, cTerm)
	}
	return result
}

func metaList2idSet(ml []*meta.Meta) *id.Set {
	if ml == nil {
		return nil
	}
	result := id.NewSetCap(len(ml))
	for _, m := range ml {
		result = result.Add(m.Zid)
	}
	return result
}

func (ct *conjTerms) retrievAndCompileTerm(searcher Searcher) CompiledTerm {
func (ct *conjTerms) retrieveAndCompileTerm(searcher Searcher, startSet *id.Set) CompiledTerm {
	match := ct.compileMeta() // Match might add some searches
	var pred RetrievePredicate
	if searcher != nil {
		pred = ct.retrieveIndex(searcher)
		if startSet != nil {
			if pred == nil {
				pred = startSet.ContainsOrNil
			} else {
				predSet := id.NewSetCap(startSet.Length())
				startSet.ForEach(func(zid id.Zid) {
					if pred(zid) {
						predSet = predSet.Add(zid)
					}
				})
				pred = predSet.ContainsOrNil
			}
		}
	}
	return CompiledTerm{Match: match, Retrieve: pred}
}

// retrieveIndex and return a predicate to ask for results.
func (ct *conjTerms) retrieveIndex(searcher Searcher) RetrievePredicate {
	if len(ct.search) == 0 {
		return nil
	}
	normCalls, plainCalls, negCalls := prepareRetrieveCalls(searcher, ct.search)
	if hasConflictingCalls(normCalls, plainCalls, negCalls) {
		return neverIncluded
	}

	positives := retrievePositives(normCalls, plainCalls)
	if positives == nil {
		// No positive search for words, must contain only words for a negative search.
		// Otherwise len(search) == 0 (see above)
		negatives := retrieveNegatives(negCalls)
		return func(zid id.Zid) bool { return !negatives.Contains(zid) }
		return func(zid id.Zid) bool { return !negatives.ContainsOrNil(zid) }
	}
	if len(positives) == 0 {
	if positives.IsEmpty() {
		// Positive search didn't found anything. We can omit the negative search.
		return neverIncluded
	}
	if len(negCalls) == 0 {
		// Positive search found something, but there is no negative search.
		return positives.Contains
	}
	negatives := retrieveNegatives(negCalls)
	return func(zid id.Zid) bool {
		return positives.Contains(zid) && !negatives.Contains(zid)
	if negatives == nil {
		return positives.Contains
	}
}

// Sort applies the sorter to the slice of meta data.
func (q *Query) Sort(metaList []*meta.Meta) []*meta.Meta {
	if len(metaList) == 0 {
		return metaList
	}

	if q == nil || len(q.order) == 0 {
		sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
		if q == nil {
			return metaList
		}
	} else if q.order[0].isRandom() {
		metaList = q.sortRandomly(metaList)
	} else {
		sort.Slice(metaList, createSortFunc(q.order, metaList))
	}

	if q.offset > 0 {
		if q.offset > len(metaList) {
			return nil
	return func(zid id.Zid) bool {
		}
		metaList = metaList[q.offset:]
	}
	return q.Limit(metaList)
}

		return positives.Contains(zid) && !negatives.ContainsOrNil(zid)
func (q *Query) setSeed() {
	if q != nil && q.seed <= 0 {
		q.seed = int(rand.Intn(10000) + 1)
	}
}

func (q *Query) sortRandomly(metaList []*meta.Meta) []*meta.Meta {
	q.setSeed()
	sort.Slice(metaList, func(i, j int) bool { return metaList[i].Zid > metaList[j].Zid })
	rnd := rand.New(rand.NewSource(int64(q.seed)))
	rnd.Shuffle(
		len(metaList),
		func(i, j int) { metaList[i], metaList[j] = metaList[j], metaList[i] },
	)
	return metaList
}

// Limit returns only s.GetLimit() elements of the given list.
func (q *Query) Limit(metaList []*meta.Meta) []*meta.Meta {
	if q == nil {
		return metaList
	}
	if q.limit > 0 && q.limit < len(metaList) {
		return metaList[:q.limit]
	}
	return metaList
	return limitElements(metaList, q.limit)
}

Changes to query/retrieve.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
1

2
3
4
5
6
7
8
9
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) 2021-2022 Detlef Stern
// 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 query

// This file contains helper functions to search within the index.

import (
	"fmt"
	"strings"

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

type searchOp struct {
	s  string
	op compareOp
}
type searchFunc func(string) id.Set
type searchFunc func(string) *id.Set
type searchCallMap map[searchOp]searchFunc

var cmpPred = map[compareOp]func(string, string) bool{
	cmpHas:    func(s, t string) bool { return s == t },
	cmpPrefix: strings.HasPrefix,
	cmpSuffix: strings.HasSuffix,
	cmpMatch:  strings.Contains,
	cmpEqual:   stringEqual,
	cmpPrefix:  strings.HasPrefix,
	cmpSuffix:  strings.HasSuffix,
	cmpMatch:   strings.Contains,
	cmpHas:     strings.Contains, // the "has" operator have string semantics here in a index search
	cmpLess:    strings.Contains, // in index search there is no "less", only "has"
	cmpGreater: strings.Contains, // in index search there is no "greater", only "has"
}

func (scm searchCallMap) addSearch(s string, op compareOp, sf searchFunc) {
	pred := cmpPred[op]
	for k := range scm {
		if op == cmpMatch {
			if strings.Contains(k.s, s) {
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
61
62
63
64
65
66
67



68
69
70
71
72
73
74







-
-
-







		if pred(s, k.s) {
			delete(scm, k)
		}
	}
	scm[searchOp{s: s, op: op}] = sf
}

func alwaysIncluded(id.Zid) bool { return true }
func neverIncluded(id.Zid) bool  { return false }

func prepareRetrieveCalls(searcher Searcher, search []expValue) (normCalls, plainCalls, negCalls searchCallMap) {
	normCalls = make(searchCallMap, len(search))
	negCalls = make(searchCallMap, len(search))
	for _, val := range search {
		for _, word := range strfun.NormalizeWords(val.value) {
			if cmpOp := val.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()
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
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







-
+

-
+






-
+

-
+










-
+









-
+











-
-
+
+

-
+






-
+





-
+





		if _, found := plainCalls[val]; found {
			return true
		}
	}
	return false
}

func retrievePositives(normCalls, plainCalls searchCallMap) id.Set {
func retrievePositives(normCalls, plainCalls searchCallMap) *id.Set {
	if isSuperset(normCalls, plainCalls) {
		var normResult id.Set
		var normResult *id.Set
		for c, sf := range normCalls {
			normResult = normResult.IntersectOrSet(sf(c.s))
		}
		return normResult
	}

	type searchResults map[searchOp]id.Set
	type searchResults map[searchOp]*id.Set
	var cache searchResults
	var plainResult id.Set
	var plainResult *id.Set
	for c, sf := range plainCalls {
		result := sf(c.s)
		if _, found := normCalls[c]; found {
			if cache == nil {
				cache = make(searchResults)
			}
			cache[c] = result
		}
		plainResult = plainResult.IntersectOrSet(result)
	}
	var normResult id.Set
	var normResult *id.Set
	for c, sf := range normCalls {
		if cache != nil {
			if result, found := cache[c]; found {
				normResult = normResult.IntersectOrSet(result)
				continue
			}
		}
		normResult = normResult.IntersectOrSet(sf(c.s))
	}
	return normResult.Add(plainResult)
	return normResult.IUnion(plainResult)
}

func isSuperset(normCalls, plainCalls searchCallMap) bool {
	for c := range plainCalls {
		if _, found := normCalls[c]; !found {
			return false
		}
	}
	return true
}

func retrieveNegatives(negCalls searchCallMap) id.Set {
	var negatives id.Set
func retrieveNegatives(negCalls searchCallMap) *id.Set {
	var negatives *id.Set
	for val, sf := range negCalls {
		negatives = negatives.Add(sf(val.s))
		negatives = negatives.IUnion(sf(val.s))
	}
	return negatives
}

func getSearchFunc(searcher Searcher, op compareOp) searchFunc {
	switch op {
	case cmpHas:
	case cmpEqual:
		return searcher.SearchEqual
	case cmpPrefix:
		return searcher.SearchPrefix
	case cmpSuffix:
		return searcher.SearchSuffix
	case cmpMatch:
	case cmpMatch, cmpHas, cmpLess, cmpGreater: // for index search we assume string semantics
		return searcher.SearchContains
	default:
		panic(fmt.Sprintf("Unexpected value of comparison operation: %v", op))
	}
}

Changes to query/select.go.

1
2

3
4
5
6
7
8



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

2
3
4
5
6
7
8
9
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) 2020-2022 Detlef Stern
// 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 query

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"

	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

type matchValueFunc func(value string) bool

func matchValueNever(string) bool { return false }

type matchSpec struct {
	key   string
	match matchValueFunc
}

// compileMeta calculates a selection func based on the given select criteria.
func (ct *conjTerms) compileMeta() MetaMatchFunc {
	for key := range ct.mvals {
		// All queried keys must exist
		ct.addKey(key, cmpExist)
	for key, vals := range ct.mvals {
		// All queried keys must exist, if there is at least one non-negated compare operation
		//
		// This is only an optimization to make selection of metadata faster.
		if countNegatedOps(vals) < len(vals) {
			ct.addKey(key, cmpExist)
		}
	}
	for _, op := range ct.keys {
		if op != cmpExist && op != cmpNotExist {
			return matchNever
		}
	}
	posSpecs, negSpecs := ct.createSelectSpecs()
	if len(posSpecs) > 0 || len(negSpecs) > 0 || len(ct.keys) > 0 {
		return makeSearchMetaMatchFunc(posSpecs, negSpecs, ct.keys)
	}
	return nil
}

func countNegatedOps(vals []expValue) int {
	count := 0
	for _, val := range vals {
		if val.op.isNegated() {
			count++
		}
	}
	return count
}

func (ct *conjTerms) createSelectSpecs() (posSpecs, negSpecs []matchSpec) {
	posSpecs = make([]matchSpec, 0, len(ct.mvals))
	negSpecs = make([]matchSpec, 0, len(ct.mvals))
	for key, values := range ct.mvals {
		if !meta.KeyIsValid(key) {
			continue
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
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







-
+



+
+
+
+



-
-
-
+



-
+



-
+











-
+












+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+







func createMatchFunc(key string, values []expValue, addSearch addSearchFunc) matchValueFunc {
	if len(values) == 0 {
		return nil
	}
	switch meta.Type(key) {
	case meta.TypeCredential:
		return matchValueNever
	case meta.TypeID, meta.TypeTimestamp: // ID and timestamp use the same layout
	case meta.TypeID:
		return createMatchIDFunc(values, addSearch)
	case meta.TypeIDSet:
		return createMatchIDSetFunc(values, addSearch)
	case meta.TypeTimestamp:
		return createMatchTimestampFunc(values, addSearch)
	case meta.TypeNumber:
		return createMatchNumberFunc(values, addSearch)
	case meta.TypeTagSet:
		return createMatchTagSetFunc(values, addSearch)
	case meta.TypeWord:
		return createMatchWordOrStringFunc(values, addSearch)
	case meta.TypeWordSet:
		return createMatchWordSetFunc(values, addSearch)
		return createMatchWordFunc(values, addSearch)
	case meta.TypeZettelmarkup:
		return createMatchZmkFunc(values, addSearch)
	}
	return createMatchWordOrStringFunc(values, addSearch)
	return createMatchStringFunc(values, addSearch)
}

func createMatchIDFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToStringPredicates(values, addSearch)
	preds := valuesToIDPredicates(values, addSearch)
	return func(value string) bool {
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchIDSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToStringSetPredicates(preprocessSet(values), addSearch)
	predList := valuesToSetPredicates(preprocessSet(values), addSearch)
	return func(value string) bool {
		ids := meta.ListFromValue(value)
		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(ids) {
					return false
				}
			}
		}
		return true
	}
}
func createMatchTimestampFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToTimestampPredicates(values, addSearch)
	return func(value string) bool {
		value = meta.ExpandTimestamp(value)
		for _, pred := range preds {
			if !pred(value) {
				return false

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToStringSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
			}
		}
		return true
	}
}

func createMatchNumberFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToNumberPredicates(values, addSearch)
	return func(value string) bool {
		tags := meta.ListFromValue(value)
		// Remove leading '#' from each tag
		for i, tag := range tags {
			tags[i] = meta.CleanTag(tag)
		}
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchTagSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predList := valuesToSetPredicates(processTagSet(preprocessSet(sliceToLower(values))), addSearch)
	return func(value string) bool {
		tags := meta.TagsFromValue(value)
		for _, preds := range predList {
			for _, pred := range preds {
				if !pred(tags) {
					return false
				}
			}
		}
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
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







-
-
+
+











-
-
+
+

-
+
-
-
-
-
+
+
+
-







			}
		}
		result[i] = tags
	}
	return result
}

func createMatchWordOrStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToStringPredicates(sliceToLower(values), addSearch)
func createMatchWordFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToWordPredicates(sliceToLower(values), addSearch)
	return func(value string) bool {
		value = strings.ToLower(value)
		for _, pred := range preds {
			if !pred(value) {
				return false
			}
		}
		return true
	}
}

func createMatchWordSetFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	predsList := valuesToStringSetPredicates(preprocessSet(sliceToLower(values)), addSearch)
func createMatchStringFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	preds := valuesToStringPredicates(sliceToLower(values), addSearch)
	return func(value string) bool {
		words := meta.ListFromValue(value)
		value = strings.ToLower(value)
		for _, preds := range predsList {
			for _, pred := range preds {
				if !pred(words) {
					return false
		for _, pred := range preds {
			if !pred(value) {
				return false
				}
			}
		}
		return true
	}
}

func sliceToLower(sl []expValue) []expValue {
216
217
218
219
220
221
222
223

224
225

226
227
228
229
230
231
232
253
254
255
256
257
258
259

260
261

262
263
264
265
266
267
268
269







-
+

-
+







func createMatchZmkFunc(values []expValue, addSearch addSearchFunc) matchValueFunc {
	normPreds := make([]stringPredicate, 0, len(values))
	negPreds := make([]stringPredicate, 0, len(values))
	for _, v := range values {
		for _, word := range strfun.NormalizeWords(v.value) {
			if cmpOp := v.op; cmpOp.isNegated() {
				cmpOp = cmpOp.negate()
				negPreds = append(negPreds, createStringCompareFunc(word, cmpOp))
				negPreds = append(negPreds, createWordCompareFunc(word, cmpOp))
			} else {
				normPreds = append(normPreds, createStringCompareFunc(word, cmpOp))
				normPreds = append(normPreds, createWordCompareFunc(word, cmpOp))
				addSearch(expValue{word, cmpOp}) // addSearch only for positive selections
			}
		}
	}
	return func(metaValue string) bool {
		temp := strings.Fields(zmk2text(metaValue))
		values := make([]string, 0, len(temp))
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







-
+

















-
-
+
+


-
+







		}
	}
	return true
}

func zmk2text(zmk string) string {
	isASCII, hasUpper, needParse := true, false, false
	for i := 0; i < len(zmk); i++ {
	for i := range len(zmk) {
		ch := zmk[i]
		if ch >= utf8.RuneSelf {
			isASCII = false
			break
		}
		hasUpper = hasUpper || ('A' <= ch && ch <= 'Z')
		needParse = needParse || !(('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('0' <= ch && ch <= '9') || ch == ' ')
	}
	if isASCII {
		if !needParse {
			if !hasUpper {
				return zmk
			}
			return strings.ToLower(zmk)
		}
	}
	is := parser.ParseMetadata(zmk)
	var buf bytes.Buffer
	if _, err := textenc.Create().WriteInlines(&buf, &is); err != nil {
	var sb strings.Builder
	if _, err := textenc.Create().WriteInlines(&sb, &is); err != nil {
		return strings.ToLower(zmk)
	}
	return strings.ToLower(buf.String())
	return strings.ToLower(sb.String())
}

func preprocessSet(set []expValue) [][]expValue {
	result := make([][]expValue, 0, len(set))
	for _, elem := range set {
		splitElems := strings.Split(elem.value, ",")
		valueElems := make([]expValue, 0, len(splitElems))
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
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








+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+




+
+
+
+
+
+
+
+
+
+
+
-
+

-
+

-
+













+
+
+
+
+
+
+
+
+
+







-
+





-
-
+
+

-
-
-
+
+
+


-
+

-
+


-
+

-
+


+
+
-
-
-
+
+
+
+
+
+
+

-
+







-
+
+
+







			result = append(result, valueElems)
		}
	}
	return result
}

type stringPredicate func(string) bool

func valuesToIDPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		value := v.value
		if len(value) > 14 {
			value = value[:14]
		}
		switch op := disambiguatedIDOp(v.op); op {
		case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			if isDigits(value) {
				// Never add the strValue to search.
				// Append enough zeroes to make it comparable as string.
				// (an ID and a timestamp always have 14 digits)
				strValue := value + "00000000000000"[:14-len(value)]
				result[i] = createIDCompareFunc(strValue, op)
				continue
			}
			fallthrough
		default:
			// Otherwise compare as a word.
			if !op.isNegated() {
				addSearch(v) // addSearch only for positive selections
			}
			result[i] = createWordCompareFunc(value, op)
		}
	}
	return result
}

func isDigits(s string) bool {
	for i := range len(s) {
		if ch := s[i]; ch < '0' || '9' < ch {
			return false
		}
	}
	return true
}

func disambiguatedIDOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createIDCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToTimestampPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		value := meta.ExpandTimestamp(v.value)
		switch op := disambiguatedTimestampOp(v.op); op {
		case cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			if isDigits(value) {
				// Never add the value to search.
				result[i] = createTimestampCompareFunc(value, op)
				continue
			}
			fallthrough
		default:
			// Otherwise compare as a word.
			if !op.isNegated() {
				addSearch(v) // addSearch only for positive selections
			}
			result[i] = createWordCompareFunc(value, op)
		}
	}
	return result
}

func disambiguatedTimestampOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createTimestampCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToNumberPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		switch op := disambiguatedNumberOp(v.op); op {
		case cmpEqual, cmpNotEqual, cmpLess, cmpNoLess, cmpGreater, cmpNoGreater:
			iValue, err := strconv.ParseInt(v.value, 10, 64)
			if err == nil {
				// Never add the strValue to search.
				result[i] = createNumberCompareFunc(iValue, op)
				continue
			}
			fallthrough
		default:
			// In all other cases, a number is treated like a word.
			if !op.isNegated() {
				addSearch(v) // addSearch only for positive selections
			}
			result[i] = createWordCompareFunc(v.value, op)
		}
	}
	return result
}

func disambiguatedNumberOp(cmpOp compareOp) compareOp { return disambiguateWordOp(cmpOp) }

func createNumberCompareFunc(cmpVal int64, cmpOp compareOp) stringPredicate {
	var cmpFunc func(int64) bool
	switch cmpOp {
	case cmpEqual:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal == cmpVal }
	case cmpNotEqual:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal != cmpVal }
	case cmpLess:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal < cmpVal }
	case cmpNoLess:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal >= cmpVal }
	case cmpGreater:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal > cmpVal }
	case cmpNoGreater:
		cmpFunc = func(iMetaVal int64) bool { return iMetaVal <= cmpVal }
	default:
		panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
	}
	return func(metaVal string) bool {
		iMetaVal, err := strconv.ParseInt(metaVal, 10, 64)
		if err != nil {
			return false
		}
		return cmpFunc(iMetaVal)
	}
}

func valuesToStringPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		op := disambiguatedStringOp(v.op)
		if !v.op.isNegated() {
		if !op.isNegated() {
			addSearch(v) // addSearch only for positive selections
		}
		result[i] = createStringCompareFunc(v.value, op)
	}
	return result
}

func disambiguatedStringOp(cmpOp compareOp) compareOp {
	switch cmpOp {
	case cmpHas:
		return cmpMatch
	case cmpHasNot:
		return cmpNoMatch
	default:
		return cmpOp
	}
}

func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	return createWordCompareFunc(cmpVal, cmpOp)
}

func valuesToWordPredicates(values []expValue, addSearch addSearchFunc) []stringPredicate {
	result := make([]stringPredicate, len(values))
	for i, v := range values {
		op := disambiguateWordOp(v.op)
		if !op.isNegated() {
			addSearch(v) // addSearch only for positive selections
		}
		result[i] = createStringCompareFunc(v.value, v.op)
		result[i] = createWordCompareFunc(v.value, op)
	}
	return result
}

func disambiguateWordOp(cmpOp compareOp) compareOp {
	switch cmpOp {
	case cmpHas:
		return cmpEqual
	case cmpHasNot:
		return cmpNotEqual
	default:
		return cmpOp
	}
}

func createStringCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
func createWordCompareFunc(cmpVal string, cmpOp compareOp) stringPredicate {
	switch cmpOp {
	case cmpHas:
	case cmpEqual:
		return func(metaVal string) bool { return metaVal == cmpVal }
	case cmpHasNot:
	case cmpNotEqual:
		return func(metaVal string) bool { return metaVal != cmpVal }
	case cmpPrefix:
		return func(metaVal string) bool { return strings.HasPrefix(metaVal, cmpVal) }
	case cmpNoPrefix:
		return func(metaVal string) bool { return !strings.HasPrefix(metaVal, cmpVal) }
	case cmpSuffix:
		return func(metaVal string) bool { return strings.HasSuffix(metaVal, cmpVal) }
	case cmpNoSuffix:
		return func(metaVal string) bool { return !strings.HasSuffix(metaVal, cmpVal) }
	case cmpMatch:
		return func(metaVal string) bool { return strings.Contains(metaVal, cmpVal) }
	case cmpNoMatch:
		return func(metaVal string) bool { return !strings.Contains(metaVal, cmpVal) }
	case cmpLess:
		return func(metaVal string) bool { return metaVal < cmpVal }
	case cmpNoLess:
		return func(metaVal string) bool { return metaVal >= cmpVal }
	case cmpGreater:
		return func(metaVal string) bool { return metaVal > cmpVal }
	case cmpNoGreater:
		return func(metaVal string) bool { return metaVal <= cmpVal }
	case cmpHas, cmpHasNot:
		panic(fmt.Sprintf("operator %d not disambiguated with value %q", cmpOp, cmpVal))
	default:
		panic(fmt.Sprintf("Unknown compare operation %d with value %q", cmpOp, cmpVal))
	}
}

type stringSetPredicate func(value []string) bool

func valuesToStringSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate {
func valuesToSetPredicates(values [][]expValue, addSearch addSearchFunc) [][]stringSetPredicate {
	result := make([][]stringSetPredicate, len(values))
	for i, val := range values {
		elemPreds := make([]stringSetPredicate, len(val))
		for j, v := range val {
			opVal := v.value // loop variable is used in closure --> save needed value
			switch v.op {
			case cmpHas:
			switch op := disambiguateWordOp(v.op); op {
			case cmpEqual:
				addSearch(v) // addSearch only for positive selections
				elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, true)
			case cmpHasNot:
				elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, false)
				fallthrough
			case cmpNotEqual:
				elemPreds[j] = makeStringSetPredicate(opVal, stringEqual, op == cmpEqual)
			case cmpPrefix:
				addSearch(v)
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, true)
				fallthrough
			case cmpNoPrefix:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, false)
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasPrefix, op == cmpPrefix)
			case cmpSuffix:
				addSearch(v)
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, true)
				fallthrough
			case cmpNoSuffix:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, false)
				elemPreds[j] = makeStringSetPredicate(opVal, strings.HasSuffix, op == cmpSuffix)
			case cmpMatch:
				addSearch(v)
				fallthrough
			case cmpNoMatch:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, true)
			case cmpNoMatch:
				elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, false)
				elemPreds[j] = makeStringSetPredicate(opVal, strings.Contains, op == cmpMatch)
			case cmpLess, cmpNoLess:
				elemPreds[j] = makeStringSetPredicate(opVal, stringLess, op == cmpLess)
			case cmpGreater, cmpNoGreater:
				elemPreds[j] = makeStringSetPredicate(opVal, stringGreater, op == cmpGreater)
			case cmpHas, cmpHasNot:
				panic(fmt.Sprintf("operator %d not disambiguated with value %q", op, opVal))
			default:
				panic(fmt.Sprintf("Unknown compare operation %d with value %q", v.op, opVal))
				panic(fmt.Sprintf("Unknown compare operation %d with value %q", op, opVal))
			}
		}
		result[i] = elemPreds
	}
	return result
}

func stringEqual(val1, val2 string) bool { return val1 == val2 }
func stringEqual(val1, val2 string) bool   { return val1 == val2 }
func stringLess(val1, val2 string) bool    { return val1 < val2 }
func stringGreater(val1, val2 string) bool { return val1 > val2 }

type compareStringFunc func(val1, val2 string) bool

func makeStringSetPredicate(neededValue string, compare compareStringFunc, foundResult bool) stringSetPredicate {
	return func(metaVals []string) bool {
		for _, metaVal := range metaVals {
			if compare(metaVal, neededValue) {

Changes to query/select_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13

14
15
16
17
18
19




20
21
22
23
24

25
26
27
28
29
30
31
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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 query_test

import (
	"context"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func TestMatchZidNegate(t *testing.T) {
	q := query.Parse(api.KeyID + api.SearchOperatorHasNot + string(api.ZidVersion) + " " + api.KeyID + api.SearchOperatorHasNot + string(api.ZidLicense))
	compiled := q.RetrieveAndCompile(nil)
	compiled := q.RetrieveAndCompile(context.Background(), nil, nil)

	testCases := []struct {
		zid api.ZettelID
		exp bool
	}{
		{api.ZidVersion, false},
		{api.ZidLicense, false},

Changes to query/sorter.go.

1
2

3
4
5
6
7
8



9
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


































1

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

-
+






+
+
+





+


-
-
+
+


-
+

-
+



-
+






-
+

-



-
+

-
-
+
+

-
-
-
+
-
-
+



-
+
+


-
-
+
+

-
+

+
+
+

-
+

-
+


+
+
-
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+











+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 query

import (
	"cmp"
	"strconv"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel/meta"
)

type sortFunc func(i, j int) bool
type sortFunc func(i, j *meta.Meta) int

func createSortFunc(order []sortOrder, ml []*meta.Meta) sortFunc {
func buildSortFunc(order []sortOrder) sortFunc {
	hasID := false
	sortFuncs := make([]sortFunc, 0, len(order)+1)
	for _, o := range order {
		sortFuncs = append(sortFuncs, createOneSortFunc(o.key, o.descending, ml))
		sortFuncs = append(sortFuncs, o.buildSortfunc())
		if o.key == api.KeyID {
			hasID = true
			break
		}
	}
	if !hasID {
		sortFuncs = append(sortFuncs, func(i, j int) bool { return ml[i].Zid > ml[j].Zid })
		sortFuncs = append(sortFuncs, defaultMetaSort)
	}
	// return sortFuncs[0]
	if len(sortFuncs) == 1 {
		return sortFuncs[0]
	}
	return func(i, j int) bool {
	return func(i, j *meta.Meta) int {
		for _, sf := range sortFuncs {
			if sf(i, j) {
				return true
			if result := sf(i, j); result != 0 {
				return result
			}
			if sf(j, i) {
				return false
			}
		}
		}
		return false
		return 0
	}
}

func createOneSortFunc(key string, descending bool, ml []*meta.Meta) sortFunc {
func (so *sortOrder) buildSortfunc() sortFunc {
	key := so.key
	keyType := meta.Type(key)
	if key == api.KeyID || keyType == meta.TypeCredential {
		if descending {
			return func(i, j int) bool { return ml[i].Zid > ml[j].Zid }
		if so.descending {
			return defaultMetaSort
		}
		return func(i, j int) bool { return ml[i].Zid < ml[j].Zid }
		return func(i, j *meta.Meta) int { return cmp.Compare(i.Zid, j.Zid) }
	}
	if keyType == meta.TypeTimestamp {
		return createSortTimestampFunc(key, so.descending)
	}
	if keyType == meta.TypeNumber {
		return createSortNumberFunc(ml, key, descending)
		return createSortNumberFunc(key, so.descending)
	}
	return createSortStringFunc(ml, key, descending)
	return createSortStringFunc(key, so.descending)
}

func defaultMetaSort(i, j *meta.Meta) int { return cmp.Compare(j.Zid, i.Zid) }

func createSortNumberFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
func createSortTimestampFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := getNum(ml[i], key)
			jVal, jOk := getNum(ml[j], key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := getNum(ml[i], key)
		jVal, jOk := getNum(ml[j], key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
	}
}

func createSortStringFunc(ml []*meta.Meta, key string, descending bool) sortFunc {
		return func(i, j *meta.Meta) int {
			iVal, iOk := i.Get(key)
			jVal, jOk := j.Get(key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(meta.ExpandTimestamp(jVal), meta.ExpandTimestamp(iVal))
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := i.Get(key)
		jVal, jOk := j.Get(key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(meta.ExpandTimestamp(iVal), meta.ExpandTimestamp(jVal))
	}
}

func createSortNumberFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j int) bool {
			iVal, iOk := ml[i].Get(key)
			jVal, jOk := ml[j].Get(key)
			return (iOk && (!jOk || iVal > jVal)) || !jOk
		}
	}
	return func(i, j int) bool {
		iVal, iOk := ml[i].Get(key)
		jVal, jOk := ml[j].Get(key)
		return (iOk && (!jOk || iVal < jVal)) || !jOk
		return func(i, j *meta.Meta) int {
			iVal, iOk := getNum(i, key)
			jVal, jOk := getNum(j, key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(jVal, iVal)
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := getNum(i, key)
		jVal, jOk := getNum(j, key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(iVal, jVal)
	}
}

func getNum(m *meta.Meta, key string) (int64, bool) {
	if s, ok := m.Get(key); ok {
		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
			return i, true
		}
	}
	return 0, false
}

func createSortStringFunc(key string, descending bool) sortFunc {
	if descending {
		return func(i, j *meta.Meta) int {
			iVal, iOk := i.Get(key)
			jVal, jOk := j.Get(key)
			if result := compareFound(jOk, iOk); result != 0 {
				return result
			}
			return cmp.Compare(jVal, iVal)
		}
	}
	return func(i, j *meta.Meta) int {
		iVal, iOk := i.Get(key)
		jVal, jOk := j.Get(key)
		if result := compareFound(iOk, jOk); result != 0 {
			return result
		}
		return cmp.Compare(iVal, jVal)
	}
}

func compareFound(iOk, jOk bool) int {
	if iOk {
		if jOk {
			return 0
		}
		return 1
	}
	if jOk {
		return -1
	}
	return 0
}

Added query/specs.go.



































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 query

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

// IdentSpec contains all specification values to calculate the ident directive.
type IdentSpec struct{}

// Print the spec on the given print environment.
func (spec *IdentSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.IdentDirective)
}

// ItemsSpec contains all specification values to calculate items.
type ItemsSpec struct{}

// Print the spec on the given print environment.
func (spec *ItemsSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.ItemsDirective)
}

Added query/unlinked.go.





















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 query

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

// UnlinkedSpec contains all specification values to calculate unlinked references.
type UnlinkedSpec struct {
	words []string
}

// Print the spec on the given print environment.
func (spec *UnlinkedSpec) Print(pe *PrintEnv) {
	pe.printSpace()
	pe.writeString(api.UnlinkedDirective)
	for _, word := range spec.words {
		pe.writeStrings(" ", api.PhraseDirective, " ", word)
	}
}

// GetWords returns all title words of a given query result.
func (spec *UnlinkedSpec) GetWords(metaSeq []*meta.Meta) []string {
	if words := spec.words; len(words) > 0 {
		result := make([]string, len(words))
		copy(result, words)
		return result
	}
	result := make([]string, 0, len(metaSeq)*4) // Assumption: four words per title
	for _, m := range metaSeq {
		title, hasTitle := m.Get(api.KeyTitle)
		if !hasTitle {
			continue
		}
		result = append(result, strfun.MakeWords(title)...)
	}
	return result
}

Changes to strfun/escape.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 strfun

import "io"

var (
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
51
52
53
54
55
56
57
































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
		}
		io.WriteString(w, s[last:i])
		w.Write(esc)
		last = i + 1
	}
	io.WriteString(w, s[last:])
}

var (
	jsBackslash   = []byte{'\\', '\\'}
	jsDoubleQuote = []byte{'\\', '"'}
	jsNewline     = []byte{'\\', 'n'}
	jsTab         = []byte{'\\', 't'}
	jsCr          = []byte{'\\', 'r'}
	jsUnicode     = []byte{'\\', 'u', '0', '0', '0', '0'}
	jsHex         = []byte("0123456789ABCDEF")
)

// JSONEscape returns the given string as a byte slice, where every non-printable
// rune is made printable.
func JSONEscape(w io.Writer, s string) (int, error) {
	length := 0
	last := 0
	for i, ch := range s {
		var b []byte
		switch ch {
		case '\t':
			b = jsTab
		case '\r':
			b = jsCr
		case '\n':
			b = jsNewline
		case '"':
			b = jsDoubleQuote
		case '\\':
			b = jsBackslash
		default:
			if ch < ' ' {
				b = jsUnicode
				b[2] = '0'
				b[3] = '0'
				b[4] = jsHex[ch>>4]
				b[5] = jsHex[ch&0xF]
			} else {
				continue
			}
		}
		l1, err := io.WriteString(w, s[last:i])
		if err != nil {
			return 0, err
		}
		l2, err := w.Write(b)
		if err != nil {
			return 0, err
		}
		length += l1 + l2
		last = i + 1
	}
	l, err := io.WriteString(w, s[last:])
	if err != nil {
		return 0, err
	}
	return length + l, nil
}

Changes to strfun/set.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 strfun

// Set ist a set of strings.
type Set map[string]struct{}

Changes to strfun/slugify.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 strfun

import (
	"strings"
	"unicode"

Changes to strfun/slugify_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 strfun_test

import (
	"testing"

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







+
+
+

















-
-
-
+
+
+






-
+











		if got := strfun.Slugify(test.in); got != test.exp {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}

func eqStringSlide(got, exp []string) bool {
	if got == nil {
		return exp == nil
	}
	if len(got) != len(exp) {
		return false
	}
	for i, g := range got {
		if g != exp[i] {
			return false
		}
	}
	return true
}

func TestNormalizeWord(t *testing.T) {
	t.Parallel()
	tests := []struct {
		in  string
		exp []string
	}{
		{"", []string{}},
		{" ", []string{}},
		{"Ë‹", []string{}}, // No single diacritic char, such as U+02CB
		{"", nil},
		{" ", nil},
		{"Ë‹", nil}, // No single diacritic char, such as U+02CB
		{"simple test", []string{"simple", "test"}},
		{"I'm a go developer", []string{"i", "m", "a", "go", "developer"}},
		{"-!->simple   test<-!-", []string{"simple", "test"}},
		{"äöüÄÖÜß", []string{"aouaouß"}},
		{"\"aèf", []string{"aef"}},
		{"a#b", []string{"a", "b"}},
		{"*", []string{}},
		{"*", nil},
		{"123", []string{"123"}},
		{"1²3", []string{"123"}},
		{"Period.", []string{"period"}},
		{" WORD  NUMBER ", []string{"word", "number"}},
	}
	for _, test := range tests {
		if got := strfun.NormalizeWords(test.in); !eqStringSlide(got, test.exp) {
			t.Errorf("%q: %q != %q", test.in, got, test.exp)
		}
	}
}

Changes to strfun/strfun.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

2
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) 2021-2022 Detlef Stern
// 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 strfun provides some string functions.
package strfun

import (
	"bytes"
	"strings"
	"unicode"
	"unicode/utf8"
)

// Length returns the number of runes in the given string.
func Length(s string) int {
	return utf8.RuneCountInString(s)
}
32
33
34
35
36
37
38
39

40
41

42
43
44


45
46

47
48
49
50
51
52
53
54








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







-
+

-
+

-
-
+
+

-
+








+
+
+
+
+
+
+
+
		runes = append(runes, r)
	}
	if len(runes) > maxLen {
		runes = runes[:maxLen]
		runes[maxLen-1] = '\u2025'
	}

	var buf bytes.Buffer
	var sb strings.Builder
	for _, r := range runes {
		buf.WriteRune(r)
		sb.WriteRune(r)
	}
	for i := 0; i < maxLen-len(runes); i++ {
		buf.WriteRune(pad)
	for range maxLen - len(runes) {
		sb.WriteRune(pad)
	}
	return buf.String()
	return sb.String()
}

// SplitLines splits the given string into a list of lines.
func SplitLines(s string) []string {
	return strings.FieldsFunc(s, func(r rune) bool {
		return r == '\n' || r == '\r'
	})
}

// MakeWords produces a list of words, i.e. string that were separated by
// control character, space characters, or separator characters.
func MakeWords(text string) []string {
	return strings.FieldsFunc(text, func(r rune) bool {
		return unicode.In(r, unicode.C, unicode.P, unicode.Z)
	})
}

Changes to strfun/strfun_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 strfun_test

import (
	"testing"

Deleted template/LICENSE.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19



















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

Deleted template/mustache.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
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711







































































































































































































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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.
//
// This file was derived from previous work:
// - https://github.com/hoisie/mustache (License: MIT)
//   Copyright (c) 2009 Michael Hoisie
// - https://github.com/cbroglie/mustache (a fork from above code)
//   Starting with commit [f9b4cbf]
//   Does not have an explicit copyright and obviously continues with
//   above MIT license.
// The license text is included in the same directory where this file is
// located. See file LICENSE.
//-----------------------------------------------------------------------------

// Package template implements the Mustache templating language.
package template

import (
	"fmt"
	"io"
	"reflect"
	"regexp"
	"strings"

	"zettelstore.de/c/html"
)

// Node represents a node in the parse tree.
// It is either a Tag or a textNode.
type node interface {
	node()
}

// Tag represents the different mustache tag types.
//
// Not all methods apply to all kinds of tags. Restrictions, if any, are noted
// in the documentation for each method. Use the Type method to find out the
// type of tag before calling type-specific methods. Calling a method
// inappropriate to the type of tag causes a run time panic.
type Tag interface {
	node

	// Type returns the type of the tag.
	Type() TagType
	// Name returns the name of the tag.
	Name() string
	// Tags returns any child tags. It panics for tag types which cannot contain
	// child tags (i.e. variable tags).
	Tags() []Tag
}

// A TagType represents the specific type of mustache tag that a Tag
// represents. The zero TagType is not a valid type.
type TagType uint

// Defines representing the possible Tag types
const (
	Invalid TagType = iota
	Variable
	Section
	InvertedSection
	Partial
)

type varNode struct {
	name string
	raw  bool
}

func (*varNode) node()          {}
func (*varNode) Type() TagType  { return Variable }
func (e *varNode) Name() string { return e.name }
func (*varNode) Tags() []Tag    { panic("mustache: Tags on Variable type") }

type sectionNode struct {
	name      string
	inverted  bool
	startline int
	nodes     []node
}

func (*sectionNode) node() {}
func (e *sectionNode) Type() TagType {
	if e.inverted {
		return InvertedSection
	}
	return Section
}
func (e *sectionNode) Name() string { return e.name }
func (e *sectionNode) Tags() []Tag  { return extractTags(e.nodes) }

type partialNode struct {
	name   string
	indent string
	prov   PartialProvider
}

func (*partialNode) node()          {}
func (*partialNode) Type() TagType  { return Partial }
func (e *partialNode) Name() string { return e.name }
func (*partialNode) Tags() []Tag    { return nil }

type textNode struct {
	text []byte
}

func (*textNode) node() {}

// Template represents a compiled mustache template
type Template struct {
	data    string
	otag    string
	ctag    string
	p       int
	curline int
	nodes   []node
	partial PartialProvider
	errmiss bool // Error when variable is not found?
}

type parseError struct {
	line    int
	message string
}

func (p parseError) Error() string { return fmt.Sprintf("line %d: %s", p.line, p.message) }

// Tags returns the mustache tags for the given template
func (tmpl *Template) Tags() []Tag {
	return extractTags(tmpl.nodes)
}

func extractTags(nodes []node) []Tag {
	tags := make([]Tag, 0, len(nodes))
	for _, elem := range nodes {
		switch elem := elem.(type) {
		case *varNode:
			tags = append(tags, elem)
		case *sectionNode:
			tags = append(tags, elem)
		case *partialNode:
			tags = append(tags, elem)
		}
	}
	return tags
}

func (tmpl *Template) readString(s string) (string, error) {
	newlines := 0
	for i := tmpl.p; ; i++ {
		//are we at the end of the string?
		if i+len(s) > len(tmpl.data) {
			return tmpl.data[tmpl.p:], io.EOF
		}

		if tmpl.data[i] == '\n' {
			newlines++
		}

		if tmpl.data[i] != s[0] {
			continue
		}

		match := true
		for j := 1; j < len(s); j++ {
			if s[j] != tmpl.data[i+j] {
				match = false
				break
			}
		}
		if match {
			e := i + len(s)
			text := tmpl.data[tmpl.p:e]
			tmpl.p = e

			tmpl.curline += newlines
			return text, nil
		}
	}
}

type textReadingResult struct {
	text          string
	padding       string
	mayStandalone bool
}

func (tmpl *Template) readText() (*textReadingResult, error) {
	pPrev := tmpl.p
	text, err := tmpl.readString(tmpl.otag)
	if err == io.EOF {
		return &textReadingResult{
			text:          text,
			padding:       "",
			mayStandalone: false,
		}, err
	}
	i := tmpl.p - len(tmpl.otag)
	for ; i > pPrev; i-- {
		if tmpl.data[i-1] != ' ' && tmpl.data[i-1] != '\t' {
			break
		}
	}

	if i == 0 || tmpl.data[i-1] == '\n' {
		return &textReadingResult{
			text:          tmpl.data[pPrev:i],
			padding:       tmpl.data[i : tmpl.p-len(tmpl.otag)],
			mayStandalone: true,
		}, nil
	}

	return &textReadingResult{
		text:          tmpl.data[pPrev : tmpl.p-len(tmpl.otag)],
		padding:       "",
		mayStandalone: false,
	}, nil
}

type tagReadingResult struct {
	tag        string
	standalone bool
}

var skipWhitespaceTagTypes = map[byte]struct{}{
	'#': {}, '^': {}, '/': {}, '<': {}, '>': {}, '=': {}, '!': {},
}

func (tmpl *Template) readTag(mayStandalone bool) (*tagReadingResult, error) {
	var text string
	var err error
	if tmpl.p < len(tmpl.data) && tmpl.data[tmpl.p] == '{' {
		text, err = tmpl.readString("}" + tmpl.ctag)
	} else {
		text, err = tmpl.readString(tmpl.ctag)
	}

	if err == io.EOF {
		//put the remaining text in a block
		return nil, parseError{tmpl.curline, "unmatched open tag"}
	}

	text = text[:len(text)-len(tmpl.ctag)]

	//trim the close tag off the text
	tag := strings.TrimSpace(text)
	if tag == "" {
		return nil, parseError{tmpl.curline, "empty tag"}
	}

	eow := tmpl.p
	for i := tmpl.p; i < len(tmpl.data); i++ {
		if !(tmpl.data[i] == ' ' || tmpl.data[i] == '\t') {
			eow = i
			break
		}
	}

	standalone := tmpl.skipWhitespaceTag(tag, eow, mayStandalone)

	return &tagReadingResult{
		tag:        tag,
		standalone: standalone,
	}, nil
}

func (tmpl *Template) skipWhitespaceTag(tag string, eow int, mayStandalone bool) bool {
	if !mayStandalone {
		return true
	}
	// Skip all whitespaces apeared after these types of tags until end of line if
	// the line only contains a tag and whitespaces.
	if _, ok := skipWhitespaceTagTypes[tag[0]]; !ok {
		return false
	}
	if eow == len(tmpl.data) {
		tmpl.p = eow
		return true
	}
	if eow < len(tmpl.data) && tmpl.data[eow] == '\n' {
		tmpl.p = eow + 1
		tmpl.curline++
		return true
	}
	if eow+1 < len(tmpl.data) && tmpl.data[eow] == '\r' && tmpl.data[eow+1] == '\n' {
		tmpl.p = eow + 2
		tmpl.curline++
		return true
	}
	return false
}

func (tmpl *Template) parsePartial(name, indent string) *partialNode {
	return &partialNode{
		name:   name,
		indent: indent,
		prov:   tmpl.partial,
	}
}

func (tmpl *Template) parseSection(section *sectionNode) error {
	for {
		textResult, err := tmpl.readText()
		text := textResult.text
		padding := textResult.padding
		mayStandalone := textResult.mayStandalone

		if err == io.EOF {
			//put the remaining text in a block
			return parseError{section.startline, "Section " + section.name + " has no closing tag"}
		}

		// put text into an item
		section.nodes = append(section.nodes, &textNode{[]byte(text)})

		tagResult, err := tmpl.readTag(mayStandalone)
		if err != nil {
			return err
		}

		if !tagResult.standalone {
			section.nodes = append(section.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err = tmpl.parseSection(sn)
			if err != nil {
				return err
			}
			section.nodes = append(section.nodes, sn)
		case '/':
			name := strings.TrimSpace(tag[1:])
			if name != section.name {
				return parseError{tmpl.curline, "interleaved closing tag: " + name}
			}
			return nil
		case '>':
			name := strings.TrimSpace(tag[1:])
			partial := tmpl.parsePartial(name, textResult.padding)
			section.nodes = append(section.nodes, partial)
		case '=':
			if tag[len(tag)-1] != '=' {
				return parseError{tmpl.curline, "Invalid meta tag"}
			}
			tag = strings.TrimSpace(tag[1 : len(tag)-1])
			newtags := strings.SplitN(tag, " ", 2)
			if len(newtags) == 2 {
				tmpl.otag = newtags[0]
				tmpl.ctag = newtags[1]
			}
		case '{':
			if tag[len(tag)-1] == '}' {
				//use a raw tag
				name := strings.TrimSpace(tag[1 : len(tag)-1])
				section.nodes = append(section.nodes, &varNode{name, true})
			}
		case '&':
			name := strings.TrimSpace(tag[1:])
			section.nodes = append(section.nodes, &varNode{name, true})
		default:
			section.nodes = append(section.nodes, &varNode{tag, false})
		}
	}
}

func (tmpl *Template) parse() error {
	for {
		textResult, err := tmpl.readText()
		text := textResult.text
		padding := textResult.padding
		mayStandalone := textResult.mayStandalone

		if err == io.EOF {
			//put the remaining text in a block
			tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)})
			return nil
		}

		// put text into an item
		tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(text)})

		tagResult, err := tmpl.readTag(mayStandalone)
		if err != nil {
			return err
		}

		if !tagResult.standalone {
			tmpl.nodes = append(tmpl.nodes, &textNode{[]byte(padding)})
		}

		tag := tagResult.tag
		switch tag[0] {
		case '!':
			//ignore comment
		case '#', '^':
			name := strings.TrimSpace(tag[1:])
			sn := &sectionNode{name, tag[0] == '^', tmpl.curline, []node{}}
			err = tmpl.parseSection(sn)
			if err != nil {
				return err
			}
			tmpl.nodes = append(tmpl.nodes, sn)
		case '/':
			return parseError{tmpl.curline, "unmatched close tag"}
		case '>':
			name := strings.TrimSpace(tag[1:])
			partial := tmpl.parsePartial(name, textResult.padding)
			tmpl.nodes = append(tmpl.nodes, partial)
		case '=':
			if tag[len(tag)-1] != '=' {
				return parseError{tmpl.curline, "Invalid meta tag"}
			}
			tag = strings.TrimSpace(tag[1 : len(tag)-1])
			newtags := strings.SplitN(tag, " ", 2)
			if len(newtags) == 2 {
				tmpl.otag = newtags[0]
				tmpl.ctag = newtags[1]
			}
		case '{':
			//use a raw tag
			if tag[len(tag)-1] == '}' {
				name := strings.TrimSpace(tag[1 : len(tag)-1])
				tmpl.nodes = append(tmpl.nodes, &varNode{name, true})
			}
		case '&':
			name := strings.TrimSpace(tag[1:])
			tmpl.nodes = append(tmpl.nodes, &varNode{name, true})
		default:
			tmpl.nodes = append(tmpl.nodes, &varNode{tag, false})
		}
	}
}

// Evaluate interfaces and pointers looking for a value that can look up the
// name, via a struct field, method, or map key, and return the result of the
// lookup.
func lookup(stack []reflect.Value, name string, errMissing bool) (reflect.Value, error) {
	// dot notation
	if pos := strings.IndexByte(name, '.'); pos > 0 && pos < len(name)-1 {
		v, err := lookup(stack, name[:pos], errMissing)
		if err != nil {
			return v, err
		}
		return lookup([]reflect.Value{v}, name[pos+1:], errMissing)
	}

	for i := len(stack) - 1; i >= 0; i-- {
		if val, ok := lookupValue(stack[i], name); ok {
			return val, nil
		}
	}
	if errMissing {
		return reflect.Value{}, fmt.Errorf("missing variable %q", name)
	}
	return reflect.Value{}, nil
}

func lookupValue(v reflect.Value, name string) (reflect.Value, bool) {
	for v.IsValid() {
		typ := v.Type()
		if n := v.Type().NumMethod(); n > 0 {
			for i := 0; i < n; i++ {
				m := typ.Method(i)
				mtyp := m.Type
				if m.Name == name && mtyp.NumIn() == 1 {
					return v.Method(i).Call(nil)[0], true
				}
			}
		}
		if name == "." {
			return v, true
		}
		switch av := v; av.Kind() {
		case reflect.Ptr:
			v = av.Elem()
		case reflect.Interface:
			v = av.Elem()
		case reflect.Struct:
			return sanitizeValue(av.FieldByName(name))
		case reflect.Map:
			return sanitizeValue(av.MapIndex(reflect.ValueOf(name)))
		default:
			return reflect.Value{}, false
		}
	}
	return reflect.Value{}, false
}

func sanitizeValue(v reflect.Value) (reflect.Value, bool) {
	if v.IsValid() {
		return v, true
	}
	return reflect.Value{}, false
}

func isEmpty(v reflect.Value) bool {
	if !v.IsValid() || v.Interface() == nil {
		return true
	}

	valueInd := indirect(v)
	if !valueInd.IsValid() {
		return true
	}
	switch val := valueInd; val.Kind() {
	case reflect.Array, reflect.Slice:
		return val.Len() == 0
	case reflect.String:
		return strings.TrimSpace(val.String()) == ""
	default:
		return valueInd.IsZero()
	}
}

func indirect(v reflect.Value) reflect.Value {
loop:
	for v.IsValid() {
		switch av := v; av.Kind() {
		case reflect.Ptr:
			v = av.Elem()
		case reflect.Interface:
			v = av.Elem()
		default:
			break loop
		}
	}
	return v
}

func (tmpl *Template) renderSection(w io.Writer, section *sectionNode, stack []reflect.Value) error {
	value, err := lookup(stack, section.name, false)
	if err != nil {
		return err
	}

	// if the value is empty, check if it's an inverted section
	if isEmpty(value) != section.inverted {
		return nil
	}

	if !section.inverted {
		switch val := indirect(value); val.Kind() {
		case reflect.Slice, reflect.Array:
			valLen := val.Len()
			enumeration := make([]reflect.Value, valLen)
			for i := 0; i < valLen; i++ {
				enumeration[i] = val.Index(i)
			}
			topStack := len(stack)
			stack = append(stack, enumeration[0])
			for _, elem := range enumeration {
				stack[topStack] = elem
				if err = tmpl.renderNodes(w, section.nodes, stack); err != nil {
					return err
				}
			}
			return nil
		case reflect.Map, reflect.Struct:
			return tmpl.renderNodes(w, section.nodes, append(stack, value))
		}
		return tmpl.renderNodes(w, section.nodes, stack)
	}

	return tmpl.renderNodes(w, section.nodes, stack)
}

func (tmpl *Template) renderNodes(w io.Writer, nodes []node, stack []reflect.Value) error {
	for _, n := range nodes {
		if err := tmpl.renderNode(w, n, stack); err != nil {
			return err
		}
	}
	return nil
}

func (tmpl *Template) renderNode(w io.Writer, node node, stack []reflect.Value) error {
	switch n := node.(type) {
	case *textNode:
		_, err := w.Write(n.text)
		return err
	case *varNode:
		val, err := lookup(stack, n.name, tmpl.errmiss)
		if err != nil {
			return err
		}
		if val.IsValid() {
			if n.raw {
				fmt.Fprint(w, val.Interface())
			} else {
				html.Escape(w, fmt.Sprint(val.Interface()))
			}
		}
	case *sectionNode:
		if err := tmpl.renderSection(w, n, stack); err != nil {
			return err
		}
	case *partialNode:
		partial, err := getPartials(n.prov, n.name, n.indent)
		if err != nil {
			return err
		}
		if err = partial.renderTemplate(w, stack); err != nil {
			return err
		}
	}
	return nil
}

func (tmpl *Template) renderTemplate(w io.Writer, stack []reflect.Value) error {
	for _, n := range tmpl.nodes {
		if err := tmpl.renderNode(w, n, stack); err != nil {
			return err
		}
	}
	return nil
}

// Render uses the given data source - generally a map or struct - to render
// the compiled template to an io.Writer.
func (tmpl *Template) Render(w io.Writer, data interface{}) error {
	return tmpl.renderTemplate(w, []reflect.Value{reflect.ValueOf(data)})
}

// ParseString compiles a mustache template string, retrieving any
// required partials from the given provider. The resulting output can be used
// to efficiently render the template multiple times with different data
// sources.
func ParseString(data string, partials PartialProvider) (*Template, error) {
	if partials == nil {
		partials = &EmptyProvider
	}
	tmpl := Template{data, "{{", "}}", 0, 1, []node{}, partials, false}
	err := tmpl.parse()
	if err != nil {
		return nil, err
	}
	return &tmpl, err
}

// SetErrorOnMissing will produce an error is a variable is not found.
func (tmpl *Template) SetErrorOnMissing() { tmpl.errmiss = true }

// PartialProvider comprises the behaviors required of a struct to be able to
// provide partials to the mustache rendering engine.
type PartialProvider interface {
	// Get accepts the name of a partial and returns the parsed partial, if it
	// could be found; a valid but empty template, if it could not be found; or
	// nil and error if an error occurred (other than an inability to find the
	// partial).
	Get(name string) (string, error)
}

// ErrPartialNotFound is returned if a partial was not found.
type ErrPartialNotFound struct {
	Name string
}

func (err *ErrPartialNotFound) Error() string {
	return "Partial '" + err.Name + "' not found"
}

// StaticProvider implements the PartialProvider interface by providing
// partials drawn from a map, which maps partial name to template contents.
type StaticProvider struct {
	Partials map[string]string
}

// Get accepts the name of a partial and returns the parsed partial.
func (sp *StaticProvider) Get(name string) (string, error) {
	if sp.Partials != nil {
		if data, ok := sp.Partials[name]; ok {
			return data, nil
		}
	}

	return "", &ErrPartialNotFound{name}
}

// emptyProvider will always returns an empty string.
type emptyProvider struct{}

// Get accepts the name of a partial and returns the parsed partial.
func (*emptyProvider) Get(string) (string, error) { return "", nil }

// EmptyProvider is a partial provider that will always return an empty string.
var EmptyProvider emptyProvider

var nonEmptyLine = regexp.MustCompile(`(?m:^(.+)$)`)

func getPartials(partials PartialProvider, name, indent string) (*Template, error) {
	data, err := partials.Get(name)
	if err != nil {
		return nil, err
	}

	// indent non empty lines
	data = nonEmptyLine.ReplaceAllString(data, indent+"$1")
	return ParseString(data, partials)
}

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
































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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.
//
// This file was derived from previous work:
// - https://github.com/hoisie/mustache (License: MIT)
//   Copyright (c) 2009 Michael Hoisie
// - https://github.com/cbroglie/mustache (a fork from above code)
//   Starting with commit [f9b4cbf]
//   Does not have an explicit copyright and obviously continues with
//   above MIT license.
// The license text is included in the same directory where this file is
// located. See file LICENSE.
//-----------------------------------------------------------------------------

package template_test

import (
	"bytes"
	"fmt"
	"strconv"
	"strings"
	"testing"

	"zettelstore.de/z/template"
)

type Test struct {
	tmpl     string
	context  interface{}
	expected string
	err      error
}

type Data struct {
	A bool
	B string
}

type User struct {
	Name string
	ID   int64
}

type Settings struct {
	Allow bool
}

func (u User) Func1() string {
	return u.Name
}

func (u *User) Func2() string {
	return u.Name
}

func (u *User) Func3() (map[string]string, error) {
	return map[string]string{"name": u.Name}, nil
}

func (u *User) Func4() (map[string]string, error) {
	return nil, nil
}

func (u *User) Func5() (*Settings, error) {
	return &Settings{true}, nil
}

func (u *User) Func6() ([]interface{}, error) {
	var v []interface{}
	v = append(v, &Settings{true})
	return v, nil
}

func (u User) Truefunc1() bool {
	return true
}

func (u *User) Truefunc2() bool {
	return true
}

func makeVector(n int) []interface{} {
	var v []interface{}
	for i := 0; i < n; i++ {
		v = append(v, &User{"Mike", 1})
	}
	return v
}

type Category struct {
	Tag         string
	Description string
}

func (c Category) DisplayName() string {
	return c.Tag + " - " + c.Description
}

var tests = []Test{
	{`hello world`, nil, "hello world", nil},
	{`hello {{name}}`, map[string]string{"name": "world"}, "hello world", nil},
	{`{{var}}`, map[string]string{"var": "5 > 2"}, "5 &gt; 2", nil},
	{`{{{var}}}`, map[string]string{"var": "5 > 2"}, "5 > 2", nil},
	// {`{{var}}`, map[string]string{"var": "& \" < >"}, "&amp; &#34; &lt; &gt;", nil},
	{`{{{var}}}`, map[string]string{"var": "& \" < >"}, "& \" < >", nil},
	{`{{a}}{{b}}{{c}}{{d}}`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "abcd", nil},
	{`0{{a}}1{{b}}23{{c}}456{{d}}89`, map[string]string{"a": "a", "b": "b", "c": "c", "d": "d"}, "0a1b23c456d89", nil},
	{`hello {{! comment }}world`, map[string]string{}, "hello world", nil},
	{`{{ a }}{{=<% %>=}}<%b %><%={{ }}=%>{{ c }}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil},
	{`{{ a }}{{= <% %> =}}<%b %><%= {{ }}=%>{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil},

	//section tests
	{`{{#A}}{{B}}{{/A}}`, Data{true, "hello"}, "hello", nil},
	{`{{#A}}{{{B}}}{{/A}}`, Data{true, "5 > 2"}, "5 > 2", nil},
	{`{{#A}}{{B}}{{/A}}`, Data{true, "5 > 2"}, "5 &gt; 2", nil},
	{`{{#A}}{{B}}{{/A}}`, Data{false, "hello"}, "", nil},
	{`{{a}}{{#b}}{{b}}{{/b}}{{c}}`, map[string]string{"a": "a", "b": "b", "c": "c"}, "abc", nil},
	{`{{#A}}{{B}}{{/A}}`, struct {
		A []struct {
			B string
		}
	}{[]struct {
		B string
	}{{"a"}, {"b"}, {"c"}}},
		"abc",
		nil,
	},
	{`{{#A}}{{b}}{{/A}}`, struct{ A []map[string]string }{[]map[string]string{{"b": "a"}, {"b": "b"}, {"b": "c"}}}, "abc", nil},

	{`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil},

	{`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": nil}, "", nil},
	{`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": (*User)(nil)}, "", nil},
	{`{{#users}}gone{{Name}}{{/users}}`, map[string]interface{}{"users": []User{}}, "", nil},

	{`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},
	{`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": []interface{}{&User{"Mike", 12}}}, "Mike", nil},
	{`{{#users}}{{Name}}{{/users}}`, map[string]interface{}{"users": makeVector(1)}, "Mike", nil},
	{`{{Name}}`, User{"Mike", 1}, "Mike", nil},
	{`{{Name}}`, &User{"Mike", 1}, "Mike", nil},
	{"{{#users}}\n{{Name}}\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\nMike\n", nil},
	{"{{#users}}\r\n{{Name}}\r\n{{/users}}", map[string]interface{}{"users": makeVector(2)}, "Mike\r\nMike\r\n", nil},

	//falsy: golang zero values
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": nil}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": false}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": 0.0}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": ""}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": Data{}}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{}}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [0]interface{}{}}, "", nil},
	//falsy: special cases we disagree with golang
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": "\t"}, "", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": []interface{}{0}}, "Hi 0", nil},
	{"{{#a}}Hi {{.}}{{/a}}", map[string]interface{}{"a": [1]interface{}{0}}, "Hi 0", nil},

	//section does not exist
	{`{{#has}}{{/has}}`, &User{"Mike", 1}, "", nil},

	// implicit iterator tests
	{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []string{"a", "b", "c", "d", "e"}}, "\"(a)(b)(c)(d)(e)\"", nil},
	{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []int{1, 2, 3, 4, 5}}, "\"(1)(2)(3)(4)(5)\"", nil},
	{`"{{#list}}({{.}}){{/list}}"`, map[string]interface{}{"list": []float64{1.10, 2.20, 3.30, 4.40, 5.50}}, "\"(1.1)(2.2)(3.3)(4.4)(5.5)\"", nil},

	//inverted section tests
	{`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]interface{}{"a": "a", "b": false, "c": "c"}, "abc", nil},
	{`{{^a}}b{{/a}}`, map[string]interface{}{"a": false}, "b", nil},
	{`{{^a}}b{{/a}}`, map[string]interface{}{"a": true}, "", nil},
	{`{{^a}}b{{/a}}`, map[string]interface{}{"a": "nonempty string"}, "", nil},
	{`{{^a}}b{{/a}}`, map[string]interface{}{"a": []string{}}, "b", nil},
	{`{{a}}{{^b}}b{{/b}}{{c}}`, map[string]string{"a": "a", "c": "c"}, "abc", nil},

	//function tests
	{`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []User{{"Mike", 1}}}, "Mike", nil},
	{`{{#users}}{{Func1}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},
	{`{{#users}}{{Func2}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},

	{`{{#users}}{{#Func3}}{{name}}{{/Func3}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "Mike", nil},
	{`{{#users}}{{#Func4}}{{name}}{{/Func4}}{{/users}}`, map[string]interface{}{"users": []*User{{"Mike", 1}}}, "", nil},
	{`{{#Truefunc1}}abcd{{/Truefunc1}}`, User{"Mike", 1}, "abcd", nil},
	{`{{#Truefunc1}}abcd{{/Truefunc1}}`, &User{"Mike", 1}, "abcd", nil},
	{`{{#Truefunc2}}abcd{{/Truefunc2}}`, &User{"Mike", 1}, "abcd", nil},
	{`{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}`, &User{"Mike", 1}, "abcd", nil},
	{`{{#user}}{{#Func5}}{{#Allow}}abcd{{/Allow}}{{/Func5}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil},
	{`{{#user}}{{#Func6}}{{#Allow}}abcd{{/Allow}}{{/Func6}}{{/user}}`, map[string]interface{}{"user": &User{"Mike", 1}}, "abcd", nil},

	//context chaining
	{`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"section": map[string]string{"name": "world"}}, "hello world", nil},
	{`hello {{#section}}{{name}}{{/section}}`, map[string]interface{}{"name": "bob", "section": map[string]string{"name": "world"}}, "hello world", nil},
	{`hello {{#bool}}{{#section}}{{name}}{{/section}}{{/bool}}`, map[string]interface{}{"bool": true, "section": map[string]string{"name": "world"}}, "hello world", nil},
	{`{{#users}}{{canvas}}{{/users}}`, map[string]interface{}{"canvas": "hello", "users": []User{{"Mike", 1}}}, "hello", nil},
	{`{{#categories}}{{DisplayName}}{{/categories}}`, map[string][]*Category{
		"categories": {&Category{"a", "b"}},
	}, "a - b", nil},

	//dotted names(dot notation)
	{`"{{person.name}}" == "{{#person}}{{name}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil},
	{`"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"`, map[string]interface{}{"person": map[string]string{"name": "Joe"}}, `"Joe" == "Joe"`, nil},
	{`"{{a.b.c.d.e.name}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}}, `"Phil" == "Phil"`, nil},
	{`"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Phil"}}}}}, "b": map[string]interface{}{"c": map[string]interface{}{"d": map[string]interface{}{"e": map[string]string{"name": "Wrong"}}}}}, `"Phil" == "Phil"`, nil},
}

func parseString(data string) (*template.Template, error) {
	return template.ParseString(data, nil)
}

func render(tmpl *template.Template, data interface{}) (string, error) {
	var buf bytes.Buffer
	err := tmpl.Render(&buf, data)
	return buf.String(), err
}

func renderString(data string, errMissing bool, value interface{}) (string, error) {
	tmpl, err := parseString(data)
	if err != nil {
		return "", err
	}
	if errMissing {
		tmpl.SetErrorOnMissing()
	}
	return render(tmpl, value)
}

func TestBasic(t *testing.T) {
	t.Parallel()
	for _, test := range tests {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err)
		} else if output != test.expected {
			t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
		}
	}

	// Now set "error on missing variable" and test again
	for _, test := range tests {
		output, err := renderString(test.tmpl, true, test.context)
		if err != nil {
			t.Errorf("%q expected %q but got error: %v", test.tmpl, test.expected, err)
		} else if output != test.expected {
			t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
		}
	}
}

var missing = []Test{
	//does not exist
	{`{{dne}}`, map[string]string{"name": "world"}, "", nil},
	{`{{dne}}`, User{"Mike", 1}, "", nil},
	{`{{dne}}`, &User{"Mike", 1}, "", nil},
	//dotted names(dot notation)
	{`"{{a.b.c}}" == ""`, map[string]interface{}{}, `"" == ""`, nil},
	{`"{{a.b.c.name}}" == ""`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "c": map[string]string{"name": "Jim"}}, `"" == ""`, nil},
	{`{{#a}}{{b.c}}{{/a}}`, map[string]interface{}{"a": map[string]interface{}{"b": map[string]string{}}, "b": map[string]string{"c": "ERROR"}}, "", nil},
}

func TestMissing(t *testing.T) {
	t.Parallel()
	for _, test := range missing {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			t.Error(err)
		} else if output != test.expected {
			t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
		}
	}

	// Now set "error on missing varaible" and confirm we get errors.
	for _, test := range missing {
		output, err := renderString(test.tmpl, true, test.context)
		if err == nil {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, output)
		} else if !strings.Contains(err.Error(), "missing variable") {
			t.Errorf("%q expected missing variable error but got %q", test.tmpl, err.Error())
		}
	}
}

var malformed = []Test{
	{`{{#a}}{{}}{{/a}}`, Data{true, "hello"}, "", fmt.Errorf("line 1: empty tag")},
	{`{{}}`, nil, "", fmt.Errorf("line 1: empty tag")},
	{`{{}`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
	{`{{`, nil, "", fmt.Errorf("line 1: unmatched open tag")},
	//invalid syntax - https://github.com/hoisie/mustache/issues/10
	{`{{#a}}{{#b}}{{/a}}{{/b}}}`, map[string]interface{}{}, "", fmt.Errorf("line 1: interleaved closing tag: a")},
}

func TestMalformed(t *testing.T) {
	t.Parallel()
	for _, test := range malformed {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			if test.err == nil {
				t.Error(err)
			} else if test.err.Error() != err.Error() {
				t.Errorf("%q expected error %q but got error %q", test.tmpl, test.err.Error(), err.Error())
			}
		} else {
			if test.err == nil {
				t.Errorf("%q expected %q got %q", test.tmpl, test.expected, output)
			} else {
				t.Errorf("%q expected error %q but got %q", test.tmpl, test.err.Error(), output)
			}
		}
	}
}

type Person struct {
	FirstName string
	LastName  string
}

func (p *Person) Name1() string {
	return p.FirstName + " " + p.LastName
}

func (p Person) Name2() string {
	return p.FirstName + " " + p.LastName
}

func TestPointerReceiver(t *testing.T) {
	t.Parallel()
	p := Person{"John", "Smith"}
	tests := []struct {
		tmpl     string
		context  interface{}
		expected string
	}{
		{
			tmpl:     "{{Name1}}",
			context:  &p,
			expected: "John Smith",
		},
		{
			tmpl:     "{{Name2}}",
			context:  &p,
			expected: "John Smith",
		},
		{
			tmpl:     "{{Name1}}",
			context:  p,
			expected: "",
		},
		{
			tmpl:     "{{Name2}}",
			context:  p,
			expected: "John Smith",
		},
	}
	for _, test := range tests {
		output, err := renderString(test.tmpl, false, test.context)
		if err != nil {
			t.Error(err)
		} else if output != test.expected {
			t.Errorf("expected %q got %q", test.expected, output)
		}
	}
}

type tag struct {
	Type template.TagType
	Name string
	Tags []tag
}

type tagsTest struct {
	tmpl string
	tags []tag
}

var tagTests = []tagsTest{
	{
		tmpl: `hello world`,
		tags: nil,
	},
	{
		tmpl: `hello {{name}}`,
		tags: []tag{
			{
				Type: template.Variable,
				Name: "name",
			},
		},
	},
	{
		tmpl: `{{#name}}hello {{name}}{{/name}}{{^name}}hello {{name2}}{{/name}}`,
		tags: []tag{
			{
				Type: template.Section,
				Name: "name",
				Tags: []tag{
					{
						Type: template.Variable,
						Name: "name",
					},
				},
			},
			{
				Type: template.InvertedSection,
				Name: "name",
				Tags: []tag{
					{
						Type: template.Variable,
						Name: "name2",
					},
				},
			},
		},
	},
}

func TestTags(t *testing.T) {
	t.Parallel()
	for _, test := range tagTests {
		testTags(t, &test)
	}
}

func testTags(t *testing.T, test *tagsTest) {
	tmpl, err := parseString(test.tmpl)
	if err != nil {
		t.Error(err)
		return
	}
	compareTags(t, tmpl.Tags(), test.tags)
}

func compareTags(t *testing.T, actual []template.Tag, expected []tag) {
	if len(actual) != len(expected) {
		t.Errorf("expected %d tags, got %d", len(expected), len(actual))
		return
	}
	for i, tag := range actual {
		if tag.Type() != expected[i].Type {
			t.Errorf("expected %s, got %s", tagString(expected[i].Type), tagString(tag.Type()))
			return
		}
		if tag.Name() != expected[i].Name {
			t.Errorf("expected %s, got %s", expected[i].Name, tag.Name())
			return
		}

		switch tag.Type() {
		case template.Variable:
			if len(expected[i].Tags) != 0 {
				t.Errorf("expected %d tags, got 0", len(expected[i].Tags))
				return
			}
		case template.Section, template.InvertedSection:
			compareTags(t, tag.Tags(), expected[i].Tags)
		case template.Partial:
			compareTags(t, tag.Tags(), expected[i].Tags)
		default:
			t.Errorf("invalid tag type: %s", tagString(tag.Type()))
			return
		}
	}
}

func tagString(t template.TagType) string {
	if int(t) < len(tagNames) {
		return tagNames[t]
	}
	return "type" + strconv.Itoa(int(t))
}

var tagNames = []string{
	template.Invalid:         "Invalid",
	template.Variable:        "Variable",
	template.Section:         "Section",
	template.InvertedSection: "InvertedSection",
	template.Partial:         "Partial",
}

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

































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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.
//
// This file was derived from previous work:
// - https://github.com/hoisie/mustache (License: MIT)
//   Copyright (c) 2009 Michael Hoisie
// - https://github.com/cbroglie/mustache (a fork from above code)
//   Starting with commit [f9b4cbf]
//   Does not have an explicit copyright and obviously continues with
//   above MIT license.
// The license text is included in the same directory where this file is
// located. See file LICENSE.
//-----------------------------------------------------------------------------

package template_test

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"sort"
	"testing"

	"zettelstore.de/z/template"
)

var enabledTests = map[string]map[string]bool{
	"comments.json": {
		"Inline":                           true,
		"Multiline":                        true,
		"Standalone":                       true,
		"Indented Standalone":              true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Multiline Standalone":             true,
		"Indented Multiline Standalone":    true,
		"Indented Inline":                  true,
		"Surrounding Whitespace":           true,
	},
	"delimiters.json": {
		"Pair Behavior":                    true,
		"Special Characters":               true,
		"Sections":                         true,
		"Inverted Sections":                true,
		"Partial Inheritence":              true,
		"Post-Partial Behavior":            true,
		"Outlying Whitespace (Inline)":     true,
		"Standalone Tag":                   true,
		"Indented Standalone Tag":          true,
		"Pair with Padding":                true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"interpolation.json": {
		"No Interpolation":                             true,
		"Basic Interpolation":                          true,
		"HTML Escaping":                                true,
		"Triple Mustache":                              true,
		"Ampersand":                                    true,
		"Basic Integer Interpolation":                  true,
		"Triple Mustache Integer Interpolation":        true,
		"Ampersand Integer Interpolation":              true,
		"Basic Decimal Interpolation":                  true,
		"Triple Mustache Decimal Interpolation":        true,
		"Ampersand Decimal Interpolation":              true,
		"Basic Context Miss Interpolation":             true,
		"Triple Mustache Context Miss Interpolation":   true,
		"Ampersand Context Miss Interpolation":         true,
		"Dotted Names - Basic Interpolation":           true,
		"Dotted Names - Triple Mustache Interpolation": true,
		"Dotted Names - Ampersand Interpolation":       true,
		"Dotted Names - Arbitrary Depth":               true,
		"Dotted Names - Broken Chains":                 true,
		"Dotted Names - Broken Chain Resolution":       true,
		"Dotted Names - Initial Resolution":            true,
		"Interpolation - Surrounding Whitespace":       true,
		"Triple Mustache - Surrounding Whitespace":     true,
		"Ampersand - Surrounding Whitespace":           true,
		"Interpolation - Standalone":                   true,
		"Triple Mustache - Standalone":                 true,
		"Ampersand - Standalone":                       true,
		"Interpolation With Padding":                   true,
		"Triple Mustache With Padding":                 true,
		"Ampersand With Padding":                       true,
	},
	"inverted.json": {
		"Falsey":                           true,
		"Truthy":                           true,
		"Context":                          true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,
		"Nested (Falsey)":                  true,
		"Nested (Truthy)":                  true,
		"Context Misses":                   true,
		"Dotted Names - Truthy":            true,
		"Dotted Names - Falsey":            true,
		"Internal Whitespace":              true,
		"Indented Inline Sections":         true,
		"Standalone Lines":                 true,
		"Standalone Indented Lines":        true,
		"Padding":                          true,
		"Dotted Names - Broken Chains":     true,
		"Surrounding Whitespace":           true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
	},
	"partials.json": {
		"Basic Behavior":                   true,
		"Failed Lookup":                    true,
		"Context":                          true,
		"Recursion":                        true,
		"Surrounding Whitespace":           true,
		"Inline Indentation":               true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Standalone Indentation":           true,
		"Padding Whitespace":               true,
	},
	"sections.json": {
		"Truthy":                           true,
		"Falsey":                           true,
		"Context":                          true,
		"Deeply Nested Contexts":           true,
		"List":                             true,
		"Empty List":                       true,
		"Doubled":                          true,
		"Nested (Truthy)":                  true,
		"Nested (Falsey)":                  true,
		"Context Misses":                   true,
		"Implicit Iterator - String":       true,
		"Implicit Iterator - Integer":      true,
		"Implicit Iterator - Decimal":      true,
		"Implicit Iterator - Array":        true,
		"Dotted Names - Truthy":            true,
		"Dotted Names - Falsey":            true,
		"Dotted Names - Broken Chains":     true,
		"Surrounding Whitespace":           true,
		"Internal Whitespace":              true,
		"Indented Inline Sections":         true,
		"Standalone Lines":                 true,
		"Indented Standalone Lines":        true,
		"Standalone Line Endings":          true,
		"Standalone Without Previous Line": true,
		"Standalone Without Newline":       true,
		"Padding":                          true,
	},
	"~lambdas.json": nil, // not implemented
}

type specTest struct {
	Name        string            `json:"name"`
	Data        interface{}       `json:"data"`
	Expected    string            `json:"expected"`
	Template    string            `json:"template"`
	Description string            `json:"desc"`
	Partials    map[string]string `json:"partials"`
}

type specTestSuite struct {
	Tests []specTest `json:"tests"`
}

func getRoot() string {
	curDir, err := os.Getwd()
	if err != nil {
		curDir = os.Getenv("PWD")
	}
	return filepath.Join(curDir, "..", "testdata", "mustache")
}

func TestSpec(t *testing.T) {
	t.Parallel()
	root := getRoot()
	if _, err := os.Stat(root); err != nil {
		if errors.Is(err, os.ErrNotExist) {
			t.Fatalf("Could not find the mustache testdata folder at %s'", root)
		}
		t.Fatal(err)
	}

	paths, err := filepath.Glob(filepath.Join(root, "*.json"))
	if err != nil {
		t.Fatal(err)
	}
	sort.Strings(paths)

	for _, path := range paths {
		_, file := filepath.Split(path)
		enabled, ok := enabledTests[file]
		if !ok {
			t.Errorf("Unexpected file %s, consider adding to enabledFiles", file)
			continue
		}
		if enabled == nil {
			continue
		}
		b, err2 := os.ReadFile(path)
		if err2 != nil {
			t.Fatal(err2)
		}
		var suite specTestSuite
		err2 = json.Unmarshal(b, &suite)
		if err2 != nil {
			t.Fatal(err2)
		}
		for _, test := range suite.Tests {
			runTest(t, file, &test)
		}
	}
}

func selectProvider(partials map[string]string) template.PartialProvider {
	if len(partials) == 0 {
		return &template.EmptyProvider
	}
	return &template.StaticProvider{partials}
}

func runTest(t *testing.T, file string, test *specTest) {
	enabled, ok := enabledTests[file][test.Name]
	if !ok {
		t.Errorf("[%s %s]: Unexpected test, add to enabledTests", file, test.Name)
	}
	if !enabled {
		t.Logf("[%s %s]: Skipped", file, test.Name)
		return
	}

	tmpl, err := template.ParseString(test.Template, selectProvider(test.Partials))
	if err != nil {
		t.Errorf("[%s %s]: %s", file, test.Name, err.Error())
		return
	}
	out, err := render(tmpl, test.Data)
	if err != nil {
		t.Errorf("[%s %s]: %s", file, test.Name, err.Error())
		return
	}
	if out != test.Expected {
		t.Errorf("[%s %s]: Expected %q, got %q", file, test.Name, test.Expected, out)
		return
	}
	t.Logf("[%s %s]: Passed", file, test.Name)
}

Deleted testdata/mustache/comments.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n  This is a\n  multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n  {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":"  {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n  {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n  {{!\n    Something's going on here...\n  }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":"  12 \n","template":"  12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345  67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]}

Deleted testdata/mustache/delimiters.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n  I got interpolated.\n  |data|\n\n  {{data}}\n  I got interpolated.\n]\n","template":"[\n{{#section}}\n  {{data}}\n  |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n  {{data}}\n  |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n  I got interpolated.\n  |data|\n\n  {{data}}\n  I got interpolated.\n]\n","template":"[\n{{^section}}\n  {{data}}\n  |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n  {{data}}\n  |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes.  .yes. ]\n[ .yes.  .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}.  .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"|  |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n  {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":"  {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n  {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @   @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]}

Deleted testdata/mustache/interpolation.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag.  A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n  1) Split the name on periods; the first part is the name to resolve, any\n  remaining parts should be retained.\n  2) Walk the context stack from top to bottom, finding the first context\n  that is a) a hash containing the name as a key OR b) an object responding\n  to a method with the given name.\n  3) If the context is a hash, the data is the value associated with the\n  name.\n  4) If the context is an object, the data is the value returned by the\n  method with the given name.\n  5) If any name parts were retained in step 1, each should be resolved\n  against a context stack containing only the result from the former\n  resolution.  If any part fails resolution, the result should be considered\n  falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: &amp; &quot; &lt; &gt;\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":"  ---\n","template":"  {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":"  ---\n","template":"  {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":"  ---\n","template":"  {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}

Deleted testdata/mustache/inverted.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag.  Name resolution is as\nfollows:\n  1) Split the name on periods; the first part is the name to resolve, any\n  remaining parts should be retained.\n  2) Walk the context stack from top to bottom, finding the first context\n  that is a) a hash containing the name as a key OR b) an object responding\n  to a method with the given name.\n  3) If the context is a hash, the data is the value associated with the\n  name.\n  4) If the context is an object and the method with the given name has an\n  arity of 1, the method SHOULD be called with a String containing the\n  unprocessed contents of the sections; the data is the value returned.\n  5) Otherwise, the data is the value returned by calling the method with\n  the given name.\n  6) If any name parts were retained in step 1, each should be resolved\n  against a context stack containing only the result from the former\n  resolution.  If any part fails resolution, the result should be considered\n  falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A  E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" |  \n  | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n  {{^boolean}}\n|\n  {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":"  {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n  {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}

Deleted testdata/mustache/partials.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject.  Set Delimiter tags MUST NOT\naffect the parsing of a partial.  The partial MUST be rendered against the\ncontext stack local to the tag.  If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate.  If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X<Y<>>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":"  |  >\n>\n","template":"  {{data}}  {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":"  >\n  >>","template":"  {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n  >\n  >","template":">\n  {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]}

Deleted testdata/mustache/sections.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag.  Name resolution is as\nfollows:\n  1) Split the name on periods; the first part is the name to resolve, any\n  remaining parts should be retained.\n  2) Walk the context stack from top to bottom, finding the first context\n  that is a) a hash containing the name as a key OR b) an object responding\n  to a method with the given name.\n  3) If the context is a hash, the data is the value associated with the\n  name.\n  4) If the context is an object and the method with the given name has an\n  arity of 1, the method SHOULD be called with a String containing the\n  unprocessed contents of the sections; the data is the value returned.\n  5) Otherwise, the data is the value returned by calling the method with\n  the given name.\n  6) If any name parts were retained in step 1, each should be resolved\n  against a context stack containing only the result from the former\n  resolution.  If any part fails resolution, the result should be considered\n  falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A  E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" |  \n  | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n  {{#boolean}}\n|\n  {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":"  {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n  {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]}

Deleted testdata/mustache/~lambdas.json.

1

-
{"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Lambdas are a special-cased data type for use in interpolations and\nsections.\n\nWhen used as the data value for an Interpolation tag, the lambda MUST be\ntreatable as an arity 0 function, and invoked as such.  The returned value\nMUST be rendered against the default delimiters, then interpolated in place\nof the lambda.\n\nWhen used as the data value for a Section tag, the lambda MUST be treatable\nas an arity 1 function, and invoked as such (passing a String containing the\nunprocessed section contents).  The returned value MUST be rendered against\nthe current delimiters, then interpolated in place of the section.\n","tests":[{"name":"Interpolation","data":{"lambda":{"php":"return \"world\";","clojure":"(fn [] \"world\")","__tag__":"code","perl":"sub { \"world\" }","python":"lambda: \"world\"","ruby":"proc { \"world\" }","js":"function() { return \"world\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be interpolated."},{"name":"Interpolation - Expansion","data":{"planet":"world","lambda":{"php":"return \"{{planet}}\";","clojure":"(fn [] \"{{planet}}\")","__tag__":"code","perl":"sub { \"{{planet}}\" }","python":"lambda: \"{{planet}}\"","ruby":"proc { \"{{planet}}\" }","js":"function() { return \"{{planet}}\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be parsed."},{"name":"Interpolation - Alternate Delimiters","data":{"planet":"world","lambda":{"php":"return \"|planet| => {{planet}}\";","clojure":"(fn [] \"|planet| => {{planet}}\")","__tag__":"code","perl":"sub { \"|planet| => {{planet}}\" }","python":"lambda: \"|planet| => {{planet}}\"","ruby":"proc { \"|planet| => {{planet}}\" }","js":"function() { return \"|planet| => {{planet}}\" }"}},"expected":"Hello, (|planet| => world)!","template":"{{= | | =}}\nHello, (|&lambda|)!","desc":"A lambda's return value should parse with the default delimiters."},{"name":"Interpolation - Multiple Calls","data":{"lambda":{"php":"global $calls; return ++$calls;","clojure":"(def g (atom 0)) (fn [] (swap! g inc))","__tag__":"code","perl":"sub { no strict; $calls += 1 }","python":"lambda: globals().update(calls=globals().get(\"calls\",0)+1) or calls","ruby":"proc { $calls ||= 0; $calls += 1 }","js":"function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }"}},"expected":"1 == 2 == 3","template":"{{lambda}} == {{{lambda}}} == {{lambda}}","desc":"Interpolated lambdas should not be cached."},{"name":"Escaping","data":{"lambda":{"php":"return \">\";","clojure":"(fn [] \">\")","__tag__":"code","perl":"sub { \">\" }","python":"lambda: \">\"","ruby":"proc { \">\" }","js":"function() { return \">\" }"}},"expected":"<&gt;>","template":"<{{lambda}}{{{lambda}}}","desc":"Lambda results should be appropriately escaped."},{"name":"Section","data":{"x":"Error!","lambda":{"php":"return ($text == \"{{x}}\") ? \"yes\" : \"no\";","clojure":"(fn [text] (if (= text \"{{x}}\") \"yes\" \"no\"))","__tag__":"code","perl":"sub { $_[0] eq \"{{x}}\" ? \"yes\" : \"no\" }","python":"lambda text: text == \"{{x}}\" and \"yes\" or \"no\"","ruby":"proc { |text| text == \"{{x}}\" ? \"yes\" : \"no\" }","js":"function(txt) { return (txt == \"{{x}}\" ? \"yes\" : \"no\") }"}},"expected":"<yes>","template":"<{{#lambda}}{{x}}{{/lambda}}>","desc":"Lambdas used for sections should receive the raw section string."},{"name":"Section - Expansion","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}}\" . $text;","clojure":"(fn [text] (str text \"{{planet}}\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}}\" . $_[0] }","python":"lambda text: \"%s{{planet}}%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}}#{text}\" }","js":"function(txt) { return txt + \"{{planet}}\" + txt }"}},"expected":"<-Earth->","template":"<{{#lambda}}-{{/lambda}}>","desc":"Lambdas used for sections should have their results parsed."},{"name":"Section - Alternate Delimiters","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}} => |planet|\" . $text;","clojure":"(fn [text] (str text \"{{planet}} => |planet|\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}} => |planet|\" . $_[0] }","python":"lambda text: \"%s{{planet}} => |planet|%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}} => |planet|#{text}\" }","js":"function(txt) { return txt + \"{{planet}} => |planet|\" + txt }"}},"expected":"<-{{planet}} => Earth->","template":"{{= | | =}}<|#lambda|-|/lambda|>","desc":"Lambdas used for sections should parse with the current delimiters."},{"name":"Section - Multiple Calls","data":{"lambda":{"php":"return \"__\" . $text . \"__\";","clojure":"(fn [text] (str \"__\" text \"__\"))","__tag__":"code","perl":"sub { \"__\" . $_[0] . \"__\" }","python":"lambda text: \"__%s__\" % (text)","ruby":"proc { |text| \"__#{text}__\" }","js":"function(txt) { return \"__\" + txt + \"__\" }"}},"expected":"__FILE__ != __LINE__","template":"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}","desc":"Lambdas used for sections should not be cached."},{"name":"Inverted Section","data":{"static":"static","lambda":{"php":"return false;","clojure":"(fn [text] false)","__tag__":"code","perl":"sub { 0 }","python":"lambda text: 0","ruby":"proc { |text| false }","js":"function(txt) { return false }"}},"expected":"<>","template":"<{{^lambda}}{{static}}{{/lambda}}>","desc":"Lambdas used for inverted sections should be considered truthy."}]}

Added testdata/testbox/00000999999999.zettel.












1
2
3
4
5
6
7
8
9
10
11
+
+
+
+
+
+
+
+
+
+
+
id: 00000999999999
title: Zettelstore Application Directory
role: configuration
syntax: none
app-zid: 00000999999999
created: 20240703235900
lang: en
modified: 20240708125724
nozid-zid: 9999999998
noappzid: 00000999999999
visibility: login

Added testdata/testbox/20230929102100.zettel.








1
2
3
4
5
6
7
+
+
+
+
+
+
+
id: 20230929102100
title: #test
role: tag
syntax: zmk
created: 20230929102125

Zettel with this tag are testing the Zettelstore.

Changes to tests/client/client_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
1

2
3
4
5
6
7
8
9
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
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client is licensed 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 client provides a client for accessing the Zettelstore via its API.
package client_test

import (
	"context"
	"flag"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/c/client"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"zettelstore.de/z/kernel"
)

func nextZid(zid api.ZettelID) api.ZettelID {
	numVal, err := strconv.ParseUint(string(zid), 10, 64)
	if err != nil {
		panic(err)
45
46
47
48
49
50
51
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
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







-
-
-
-
-
-
+
+
+
+
+
+


















-
+
















+
-
+




-
+



-
+








-
+







-
+


-
+



-
+





-
+



-
+











-
+




+
+
+
-
-
+
+



-
+















-

-
+







		}

	}
}

func TestListZettel(t *testing.T) {
	const (
		ownerZettel      = 47
		configRoleZettel = 29
		writerZettel     = ownerZettel - 23
		readerZettel     = ownerZettel - 23
		creatorZettel    = 7
		publicZettel     = 4
		ownerZettel      = 57
		configRoleZettel = 35
		writerZettel     = ownerZettel - 25
		readerZettel     = ownerZettel - 25
		creatorZettel    = 10
		publicZettel     = 5
	)

	testdata := []struct {
		user string
		exp  int
	}{
		{"", publicZettel},
		{"creator", creatorZettel},
		{"reader", readerZettel},
		{"writer", writerZettel},
		{"owner", ownerZettel},
	}

	t.Parallel()
	c := getClient()
	for i, tc := range testdata {
		t.Run(fmt.Sprintf("User %d/%q", i, tc.user), func(tt *testing.T) {
			c.SetAuth(tc.user, tc.user)
			q, h, l, err := c.ListZettelJSON(context.Background(), "")
			q, h, l, err := c.QueryZettelData(context.Background(), "")
			if err != nil {
				tt.Error(err)
				return
			}
			if q != "" {
				tt.Errorf("Query should be empty, but is %q", q)
			}
			if h != "" {
				tt.Errorf("Human should be empty, but is %q", q)
			}
			got := len(l)
			if got != tc.exp {
				tt.Errorf("List of length %d expected, but got %d\n%v", tc.exp, got, l)
			}
		})
	}
	search := api.KeyRole + api.SearchOperatorHas + api.ValueRoleConfiguration + " ORDER id"
	q, h, l, err := c.ListZettelJSON(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration)
	q, h, l, err := c.QueryZettelData(context.Background(), search)
	if err != nil {
		t.Error(err)
		return
	}
	expQ := "role:configuration"
	expQ := "role:configuration ORDER id"
	if q != expQ {
		t.Errorf("Query should be %q, but is %q", expQ, q)
	}
	expH := "role HAS configuration"
	expH := "role HAS configuration ORDER id"
	if h != expH {
		t.Errorf("Human should be %q, but is %q", expH, h)
	}
	got := len(l)
	if got != configRoleZettel {
		t.Errorf("List of length %d expected, but got %d\n%v", configRoleZettel, got, l)
	}

	pl, err := c.ListZettel(context.Background(), api.KeyRole+api.SearchOperatorHas+api.ValueRoleConfiguration)
	pl, err := c.QueryZettel(context.Background(), search)
	if err != nil {
		t.Error(err)
		return
	}
	compareZettelList(t, pl, l)
}

func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaJSON) {
func compareZettelList(t *testing.T, pl [][]byte, l []api.ZidMetaRights) {
	t.Helper()
	if len(pl) != len(l) {
		t.Errorf("Different list lenght: Plain=%d, JSON=%d", len(pl), len(l))
		t.Errorf("Different list lenght: Plain=%d, Data=%d", len(pl), len(l))
	} else {
		for i, line := range pl {
			if got := api.ZettelID(line[:14]); got != l[i].ID {
				t.Errorf("%d: JSON=%q, got=%q", i, l[i].ID, got)
				t.Errorf("%d: Data=%q, got=%q", i, l[i].ID, got)
			}
		}
	}
}

func TestGetZettelJSON(t *testing.T) {
func TestGetZettelData(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
	z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if m := z.Meta; len(m) == 0 {
		t.Errorf("Exptected non-empty meta, but got %v", z.Meta)
	}
	if z.Content == "" || z.Encoding != "" {
		t.Errorf("Expect non-empty content, but empty encoding (got %q)", z.Encoding)
	}

	m, err := c.GetMeta(context.Background(), api.ZidDefaultHome)
	mr, err := c.GetMetaData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if mr.Rights == api.ZettelCanNone {
		t.Error("rights must be greater zero")
	}
	if len(m) != len(z.Meta) {
		t.Errorf("Pure meta differs from zettel meta: %s vs %s", m, z.Meta)
	if len(mr.Meta) != len(z.Meta) {
		t.Errorf("Pure meta differs from zettel meta: %s vs %s", mr.Meta, z.Meta)
		return
	}
	for k, v := range z.Meta {
		got, ok := m[k]
		got, ok := mr.Meta[k]
		if !ok {
			t.Errorf("Pure meta has no key %q", k)
			continue
		}
		if got != v {
			t.Errorf("Pure meta has different value for key %q: %q vs %q", k, got, v)
		}
	}
}

func TestGetParsedEvaluatedZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	encodings := []api.EncodingEnum{
		api.EncoderZJSON,
		api.EncoderHTML,
		api.EncoderSexpr,
		api.EncoderSz,
		api.EncoderText,
	}
	for _, enc := range encodings {
		content, err := c.GetParsedZettel(context.Background(), api.ZidDefaultHome, enc)
		if err != nil {
			t.Error(err)
			continue
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
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







-
-
-
-
-
-
-
-
-
-
+










-
+




-
-
-
-
-
-
+
+


-
-
+
+
+
+














-
+




-
+
-
-
-
+
+
+
-
-
-
+


-
+
-
-
-
-
+
-
-
-
+
+
+
+


-
+
-
-
+
+
+
-
-
-
-

-






-
+




-
-
-
-
-
-
+
+







		}
		if len(content) == 0 {
			t.Errorf("Empty content for evaluated encoding %v", enc)
		}
	}
}

func checkZid(t *testing.T, expected, got api.ZettelID) bool {
	t.Helper()
	if expected != got {
		t.Errorf("Expected a Zid %q, but got %q", expected, got)
		return false
	}
	return true
}

func checkListZid(t *testing.T, l []api.ZidMetaJSON, pos int, expected api.ZettelID) {
func checkListZid(t *testing.T, l []api.ZidMetaRights, pos int, expected api.ZettelID) {
	t.Helper()
	if got := api.ZettelID(l[pos].ID); got != expected {
		t.Errorf("Expected result[%d]=%v, but got %v", pos, expected, got)
	}
}

func TestGetZettelOrder(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelOrder(context.Background(), api.ZidTOCNewTemplate)
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidTOCNewTemplate)+" "+api.ItemsDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, api.ZidTOCNewTemplate, rl.ID) {
		return
	}
	l := rl.List
	if got := len(l); got != 2 {
		t.Errorf("Expected list of length 2, got %d", got)
	if got := len(metaSeq); got != 4 {
		t.Errorf("Expected list of length 4, got %d", got)
		return
	}
	checkListZid(t, l, 0, api.ZidTemplateNewZettel)
	checkListZid(t, l, 1, api.ZidTemplateNewUser)
	checkListZid(t, metaSeq, 0, api.ZidTemplateNewZettel)
	checkListZid(t, metaSeq, 1, api.ZidTemplateNewRole)
	checkListZid(t, metaSeq, 2, api.ZidTemplateNewTag)
	checkListZid(t, metaSeq, 3, api.ZidTemplateNewUser)
}

func TestGetZettelContext(t *testing.T) {
	const (
		allUserZid = api.ZettelID("20211019200500")
		ownerZid   = api.ZettelID("20210629163300")
		writerZid  = api.ZettelID("20210629165000")
		readerZid  = api.ZettelID("20210629165024")
		creatorZid = api.ZettelID("20210629165050")
		limitAll   = 3
	)
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.GetZettelContext(context.Background(), ownerZid, client.DirBoth, 0, limitAll)
	rl, err := c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT LIMIT "+strconv.Itoa(limitAll))
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, ownerZid, rl.ID) {
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid, writerZid}, rl)
		return
	}
	l := rl.List

	rl, err = c.QueryZettel(context.Background(), string(ownerZid)+" CONTEXT BACKWARD")
	if err != nil {
	if got := len(l); got != limitAll {
		t.Errorf("Expected list of length %d, got %d", limitAll, got)
		t.Error(rl)
		t.Error(err)
		return
	}
	checkListZid(t, l, 0, allUserZid)
	checkZidList(t, []api.ZettelID{ownerZid, allUserZid}, rl)
	checkListZid(t, l, 1, writerZid)
	checkListZid(t, l, 2, readerZid)
	// checkListZid(t, l, 3, creatorZid)

}
	rl, err = c.GetZettelContext(context.Background(), ownerZid, client.DirBackward, 0, 0)
	if err != nil {
		t.Error(err)
func checkZidList(t *testing.T, exp []api.ZettelID, got [][]byte) {
	t.Helper()
	if len(exp) != len(got) {
		t.Errorf("expected a list fo length %d, but got %d", len(exp), len(got))
		return
	}
	if !checkZid(t, ownerZid, rl.ID) {
	for i, expZid := range exp {
		return
	}
		if gotZid := api.ZettelID(got[i][:14]); expZid != gotZid {
			t.Errorf("lists differ at pos %d: expected id %v, but got %v", i, expZid, gotZid)
		}
	l = rl.List
	if got := len(l); got != 1 {
		t.Errorf("Expected list of length 1, got %d", got)
		return
	}
	checkListZid(t, l, 0, allUserZid)
}

func TestGetUnlinkedReferences(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	zl, err := c.GetUnlinkedReferences(context.Background(), api.ZidDefaultHome, nil)
	_, _, metaSeq, err := c.QueryZettelData(context.Background(), string(api.ZidDefaultHome)+" "+api.UnlinkedDirective)
	if err != nil {
		t.Error(err)
		return
	}
	if !checkZid(t, api.ZidDefaultHome, zl.ID) {
		return
	}
	l := zl.List
	if got := len(l); got != 1 {
		t.Errorf("Expected list of length 1, got %d", got)
	if got := len(metaSeq); got != 1 {
		t.Errorf("Expected list of length 1, got %d:\n%v", got, metaSeq)
		return
	}
}

func failNoErrorOrNoCode(t *testing.T, err error, goodCode int) bool {
	if err != nil {
		if cErr, ok := err.(*client.Error); ok {
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
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







-
+












-
-
+
+


-
-
+
+




-
-
-
+
+
+




+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+




-
-
-
+
+
+
-
-
-
-
+
-






-
+








+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







	}
}

func TestListTags(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	tm, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyTags)
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyTags)
	if err != nil {
		t.Error(err)
		return
	}
	tags := []struct {
		key  string
		size int
	}{
		{"#invisible", 1},
		{"#user", 4},
		{"#test", 4},
	}
	if len(tm) != len(tags) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(tags), len(tm), tm)
	if len(agg) != len(tags) {
		t.Errorf("Expected %d different tags, but got %d (%v)", len(tags), len(agg), agg)
	}
	for _, tag := range tags {
		if zl, ok := tm[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, tm)
		if zl, ok := agg[tag.key]; !ok {
			t.Errorf("No tag %v: %v", tag.key, agg)
		} else if len(zl) != tag.size {
			t.Errorf("Expected %d zettel with tag %v, but got %v", tag.size, tag.key, zl)
		}
	}
	for i, id := range tm["#user"] {
		if id != tm["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", tm["#user"], tm["#test"])
	for i, id := range agg["#user"] {
		if id != agg["#test"][i] {
			t.Errorf("Tags #user and #test have different content: %v vs %v", agg["#user"], agg["#test"])
		}
	}
}

func TestTagZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.AllowRedirect(true)
	c.SetAuth("owner", "owner")
	ctx := context.Background()
	zid, err := c.TagZettel(ctx, "nosuchtag")
	if err != nil {
		t.Error(err)
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.TagZettel(ctx, "#test")
	exp := api.ZettelID("20230929102100")
	if err != nil {
		t.Error(err)
	} else if zid != exp {
		t.Errorf("tag zettel for #test should be %q, but got %q", exp, zid)
	}
}

func TestListRoles(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("owner", "owner")
	rl, err := c.QueryMapMeta(context.Background(), api.ActionSeparator+api.KeyRole)
	agg, err := c.QueryAggregate(context.Background(), api.ActionSeparator+api.KeyRole)
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "role", "user", "tag", "zettel"}
	if len(agg) != len(exp) {
		t.Errorf("Expected %d different roles, but got %d (%v)", len(exp), len(agg), agg)
	}
	for _, id := range exp {
		if _, found := agg[id]; !found {
			t.Errorf("Role map expected key %q", id)
		}
	}
}

func TestRoleZettel(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.AllowRedirect(true)
	c.SetAuth("owner", "owner")
	ctx := context.Background()
	zid, err := c.RoleZettel(ctx, "nosuchrole")
	if err != nil {
		t.Error("AAA", err)
	} else if zid != "" {
		t.Errorf("no zid expected, but got %q", zid)
	}
	zid, err = c.RoleZettel(ctx, "zettel")
	exp := api.ZettelID("00000000060010")
	if err != nil {
		t.Error(err)
	} else if zid != exp {
		t.Errorf("role zettel for zettel should be %q, but got %q", exp, zid)
	}
}

func TestRedirect(t *testing.T) {
	t.Parallel()
	c := getClient()
	search := api.OrderDirective + " " + api.ReverseDirective + " " + api.KeyID + api.ActionSeparator + api.RedirectAction
	ub := c.NewURLBuilder('z').AppendQuery(search)
	respRedirect, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respRedirect.Body.Close()
	bodyRedirect, err := io.ReadAll(respRedirect.Body)
	if err != nil {
		t.Error(err)
		return
	}
	ub.ClearQuery().SetZid(api.ZidEmoji)
	respEmoji, err := http.Get(ub.String())
	if err != nil {
		t.Error(err)
		return
	}
	defer respEmoji.Body.Close()
	bodyEmoji, err := io.ReadAll(respEmoji.Body)
	if err != nil {
		t.Error(err)
		return
	}
	exp := []string{"configuration", "user", "zettel"}
	if len(rl) != len(exp) {
		t.Errorf("Expected %d different tags, but got only %d (%v)", len(exp), len(rl), rl)
	if !slices.Equal(bodyRedirect, bodyEmoji) {
		t.Error("Wrong redirect")
		t.Error("REDIRECT", respRedirect)
	}
	for _, id := range exp {
		if _, found := rl[id]; !found {
			t.Errorf("Role map expected key %q", id)
		t.Error("EXPECTED", respEmoji)
		}
	}
}

func TestVersion(t *testing.T) {
	t.Parallel()
	c := getClient()
	ver, err := c.GetVersionJSON(context.Background())
	ver, err := c.GetVersionInfo(context.Background())
	if err != nil {
		t.Error(err)
		return
	}
	if ver.Major != -1 || ver.Minor != -1 || ver.Patch != -1 || ver.Info != kernel.CoreDefaultVersion || ver.Hash != "" {
		t.Error(ver)
	}
}

func TestApplicationZid(t *testing.T) {
	c := getClient()
	c.SetAuth("reader", "reader")
	zid, err := c.GetApplicationZid(context.Background(), "app")
	if err != nil {
		t.Error(err)
		return
	}
	if zid != api.ZidAppDirectory {
		t.Errorf("c.GetApplicationZid(\"app\") should result in %q, but got: %q", api.ZidAppDirectory, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "noappzid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}
	if zid, err = c.GetApplicationZid(context.Background(), "nozid"); err == nil {
		t.Errorf(`c.GetApplicationZid("nozid") should result in error, but got: %v`, zid)
	}
}

var baseURL string

func init() {
	flag.StringVar(&baseURL, "base-url", "", "Base URL")
}

Changes to tests/client/crud_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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// Copyright (c) 2021-present Detlef Stern
//
// This file is part of Zettelstore.
//
// Zettelstore client is licensed 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 client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"zettelstore.de/c/client"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
)

// ---------------------------------------------------------------------------
// Tests that change the Zettelstore must nor run parallel to other tests.

func TestCreateGetRenameDeleteZettel(t *testing.T) {
func TestCreateGetDeleteZettel(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	zettel := `title: A Test

Example content.`
	c := getClient()
	c.SetAuth("owner", "owner")
	zid, err := c.CreateZettel(context.Background(), []byte(zettel))
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
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







-
-
-
-
-
-
+
-
-
+


-
+



-
+












-
-
-
-
-
-
-
+
-

-
+


-
+




-
+









-
+







	}
	exp := `title: A Test

Example content.`
	if string(data) != exp {
		t.Errorf("Expected zettel data: %q, but got %q", exp, data)
	}
	newZid := nextZid(zid)
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}


	doDelete(t, c, newZid)
	doDelete(t, c, zid)
}

func TestCreateGetRenameDeleteZettelJSON(t *testing.T) {
func TestCreateGetDeleteZettelDataCreator(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("creator", "creator")
	zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta:     nil,
		Encoding: "",
		Content:  "Example",
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	if !zid.IsValid() {
		t.Error("Invalid zettel ID", zid)
		return
	}
	newZid := nextZid(zid)
	c.SetAuth("owner", "owner")
	err = c.RenameZettel(context.Background(), zid, newZid)
	if err != nil {
		t.Error("Cannot rename", zid, ":", err)
		newZid = zid
	}


	c.SetAuth("owner", "owner")
	doDelete(t, c, newZid)
	doDelete(t, c, zid)
}

func TestCreateGetDeleteZettelJSON(t *testing.T) {
func TestCreateGetDeleteZettelData(t *testing.T) {
	// Is not to be allowed to run in parallel with other tests.
	c := getClient()
	c.SetAuth("owner", "owner")
	wrongModified := "19691231115959"
	zid, err := c.CreateZettelJSON(context.Background(), &api.ZettelDataJSON{
	zid, err := c.CreateZettelData(context.Background(), api.ZettelData{
		Meta: api.ZettelMeta{
			api.KeyTitle:    "A\nTitle", // \n must be converted into a space
			api.KeyModified: wrongModified,
		},
	})
	if err != nil {
		t.Error("Cannot create zettel:", err)
		return
	}
	z, err := c.GetZettelJSON(context.Background(), zid)
	z, err := c.GetZettelData(context.Background(), zid)
	if err != nil {
		t.Error("Cannot get zettel:", zid, err)
	} else {
		exp := "A Title"
		if got := z.Meta[api.KeyTitle]; got != exp {
			t.Errorf("Expected title %q, but got %q", exp, got)
		}
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
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







-
+


-
+












-
+




-
+







	if string(zt) != newZettel {
		t.Errorf("Expected zettel %q, got %q", newZettel, zt)
	}
	// Must delete to clean up for next tests
	doDelete(t, c, api.ZidDefaultHome)
}

func TestUpdateZettelJSON(t *testing.T) {
func TestUpdateZettelData(t *testing.T) {
	c := getClient()
	c.SetAuth("writer", "writer")
	z, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
	z, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := z.Meta[api.KeyTitle]; got != "Home" {
		t.Errorf("Title of zettel is not \"Home\", but %q", got)
		return
	}
	newTitle := "New Home"
	z.Meta[api.KeyTitle] = newTitle
	wrongModified := "19691231235959"
	z.Meta[api.KeyModified] = wrongModified
	err = c.UpdateZettelJSON(context.Background(), api.ZidDefaultHome, z)
	err = c.UpdateZettelData(context.Background(), api.ZidDefaultHome, z)
	if err != nil {
		t.Error(err)
		return
	}
	zt, err := c.GetZettelJSON(context.Background(), api.ZidDefaultHome)
	zt, err := c.GetZettelData(context.Background(), api.ZidDefaultHome)
	if err != nil {
		t.Error(err)
		return
	}
	if got := zt.Meta[api.KeyTitle]; got != newTitle {
		t.Errorf("Title of zettel is not %q, but %q", newTitle, got)
	}

Changes to tests/client/embed_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

21
22
23
24
25
26
27
28

-
+






+
+
+









-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 client_test

import (
	"context"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
)

const (
	abcZid   = api.ZettelID("20211020121000")
	abc10Zid = api.ZettelID("20211020121100")
)

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







-
+














-
+







}

func TestZettelTransclusionNoPrivilegeEscalation(t *testing.T) {
	t.Parallel()
	c := getClient()
	c.SetAuth("reader", "reader")

	zettelData, err := c.GetZettelJSON(context.Background(), api.ZidEmoji)
	zettelData, err := c.GetZettelData(context.Background(), api.ZidEmoji)
	if err != nil {
		t.Error(err)
		return
	}
	expectedEnc := "base64"
	if got := zettelData.Encoding; expectedEnc != got {
		t.Errorf("Zettel %q: encoding %q expected, but got %q", abcZid, expectedEnc, got)
	}

	content, err := c.GetEvaluatedZettel(context.Background(), abc10Zid, api.EncoderHTML)
	if err != nil {
		t.Error(err)
		return
	}
	if exp, got := "<p></p>", string(content); exp != got {
	if exp, got := "", string(content); exp != got {
		t.Errorf("Zettel %q must contain %q, but got %q", abc10Zid, exp, got)
	}
}

func stringHead(s string) string {
	const maxLen = 40
	if len(s) <= maxLen {

Changes to tests/markdown_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27

28
29
30



31
32
33
34

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

54
55
56
57
58
59
60
61

-
+






+
+
+









+


-
+
+


-



-
-
-
+
+
+

-



+















-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 tests

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	_ "zettelstore.de/z/encoder/htmlenc"
	_ "zettelstore.de/z/encoder/mdenc"
	_ "zettelstore.de/z/encoder/sexprenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zjsonenc"
	_ "zettelstore.de/z/encoder/shtmlenc"
	_ "zettelstore.de/z/encoder/szenc"
	_ "zettelstore.de/z/encoder/textenc"
	_ "zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	_ "zettelstore.de/z/parser/markdown"
	_ "zettelstore.de/z/parser/zettelmark"
	"zettelstore.de/z/zettel/meta"
)

type markdownTestCase struct {
	Markdown  string `json:"markdown"`
	HTML      string `json:"html"`
	Example   int    `json:"example"`
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	Section   string `json:"section"`
}

func TestEncoderAvailability(t *testing.T) {
	t.Parallel()
	encoderMissing := false
	for _, enc := range encodings {
		enc := encoder.Create(enc)
		enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		if enc == nil {
			t.Errorf("No encoder for %q found", enc)
			encoderMissing = true
		}
	}
	if encoderMissing {
		panic("At least one encoder is missing. See test log")
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
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







-
+


-
-
-
+
+
+





-
+







}

func createMDBlockSlice(markdown string, hi config.HTMLInsecurity) ast.BlockSlice {
	return parser.ParseBlocks(input.NewInput([]byte(markdown)), nil, meta.SyntaxMarkdown, hi)
}

func testAllEncodings(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	var buf bytes.Buffer
	var sb strings.Builder
	testID := tc.Example*100 + 1
	for _, enc := range encodings {
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(st *testing.T) {
			encoder.Create(enc).WriteBlocks(&buf, ast)
			buf.Reset()
		t.Run(fmt.Sprintf("Encode %v %v", enc, testID), func(*testing.T) {
			encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}).WriteBlocks(&sb, ast)
			sb.Reset()
		})
	}
}

func testZmkEncoding(t *testing.T, tc markdownTestCase, ast *ast.BlockSlice) {
	zmkEncoder := encoder.Create(api.EncoderZmk)
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var buf bytes.Buffer
	testID := tc.Example*100 + 1
	t.Run(fmt.Sprintf("Encode zmk %14d", testID), func(st *testing.T) {
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, ast)
		// gotFirst := buf.String()

125
126
127
128
129
130
131
132
133


134
135
136
137
138



139
140
141
142
143
129
130
131
132
133
134
135


136
137
138
139



140
141
142
143
144
145
146
147







-
-
+
+


-
-
-
+
+
+





func TestAdditionalMarkdown(t *testing.T) {
	testcases := []struct {
		md  string
		exp string
	}{
		{`abc<br>def`, `abc@@<br>@@{="html"}def`},
	}
	zmkEncoder := encoder.Create(api.EncoderZmk)
	var buf bytes.Buffer
	zmkEncoder := encoder.Create(api.EncoderZmk, nil)
	var sb strings.Builder
	for i, tc := range testcases {
		ast := createMDBlockSlice(tc.md, config.MarkdownHTML)
		buf.Reset()
		zmkEncoder.WriteBlocks(&buf, &ast)
		got := buf.String()
		sb.Reset()
		zmkEncoder.WriteBlocks(&sb, &ast)
		got := sb.String()
		if got != tc.exp {
			t.Errorf("%d: %q -> %q, but got %q", i, tc.md, tc.exp, got)
		}
	}
}

Changes to tests/naughtystrings_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
1

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

import (
	"bufio"
	"io"
	"os"
	"path/filepath"
	"testing"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	_ "zettelstore.de/z/cmd"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/input"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/meta"
)

// Test all parser / encoder with a list of "naughty strings", i.e. unusual strings
// that often crash software.

func getNaughtyStrings() (result []string, err error) {
	fpath := filepath.Join("..", "testdata", "naughty", "blns.txt")
51
52
53
54
55
56
57
58

59
60
61
62
63
64
65
55
56
57
58
59
60
61

62
63
64
65
66
67
68
69







-
+







		}
	}
	return result
}

func getAllEncoder() (result []encoder.Encoder) {
	for _, enc := range encoder.GetEncodings() {
		e := encoder.Create(enc)
		e := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN})
		result = append(result, e)
	}
	return result
}

func TestNaughtyStringParser(t *testing.T) {
	blns, err := getNaughtyStrings()

Changes to tests/regression_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
1

2
3
4
5
6
7
8
9
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) 2020-2022 Detlef Stern
// 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 tests provides some higher-level tests.
package tests

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/box/manager"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"

	_ "zettelstore.de/z/box/dirbox"
)

var encodings = []api.EncodingEnum{
	api.EncoderHTML,
	api.EncoderSexpr,
	api.EncoderSz,
	api.EncoderText,
	api.EncoderZJSON,
}

func getFileBoxes(wd, kind string) (root string, boxes []box.ManagedBox) {
	root = filepath.Clean(filepath.Join(wd, "..", "testdata", kind))
	entries, err := os.ReadDir(root)
	if err != nil {
		panic(err)
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
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







-
-
-
-
+
+
+
+











+
-
-
+
+



-
-
-
+
+
+







	}
	return u.Path[len(root):]
}

func checkMetaFile(t *testing.T, resultName string, zn *ast.ZettelNode, enc api.EncodingEnum) {
	t.Helper()

	if enc := encoder.Create(enc); enc != nil {
		var buf bytes.Buffer
		enc.WriteMeta(&buf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, buf.String())
	if enc := encoder.Create(enc, &encoder.CreateParameter{Lang: api.ValueLangEN}); enc != nil {
		var sf strings.Builder
		enc.WriteMeta(&sf, zn.Meta, parser.ParseMetadata)
		checkFileContent(t, resultName, sf.String())
		return
	}
	panic(fmt.Sprintf("Unknown writer encoding %q", enc))
}

func checkMetaBox(t *testing.T, p box.ManagedBox, wd, boxName string) {
	ss := p.(box.StartStopper)
	if err := ss.Start(context.Background()); err != nil {
		panic(err)
	}
	metaList := []*meta.Meta{}
	if err := p.ApplyMeta(context.Background(),
	err := p.ApplyMeta(context.Background(), func(m *meta.Meta) { metaList = append(metaList, m) }, nil)
	if err != nil {
		func(m *meta.Meta) { metaList = append(metaList, m) },
		query.AlwaysIncluded); err != nil {
		panic(err)
	}
	for _, meta := range metaList {
		zettel, err2 := p.GetZettel(context.Background(), meta.Zid)
		if err2 != nil {
			panic(err2)
		zettel, err := p.GetZettel(context.Background(), meta.Zid)
		if err != nil {
			panic(err)
		}
		z := parser.ParseZettel(context.Background(), zettel, "", testConfig)
		for _, enc := range encodings {
			t.Run(fmt.Sprintf("%s::%d(%s)", p.Location(), meta.Zid, enc), func(st *testing.T) {
				resultName := filepath.Join(wd, "result", "meta", boxName, z.Zid.String()+"."+enc.String())
				checkMetaFile(st, resultName, z, enc)
			})

Deleted tools/build.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

















































































































































































































































































































































































































































































































































































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// 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 main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"errors"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"zettelstore.de/z/strfun"
)

var directProxy = []string{"GOPROXY=direct"}

func executeCommand(env []string, name string, arg ...string) (string, error) {
	logCommand("EXEC", env, name, arg)
	var out bytes.Buffer
	cmd := prepareCommand(env, name, arg, &out)
	err := cmd.Run()
	return out.String(), err
}

func prepareCommand(env []string, name string, arg []string, out io.Writer) *exec.Cmd {
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = nil
	cmd.Stdout = out
	cmd.Stderr = os.Stderr
	return cmd
}

func logCommand(exec string, env []string, name string, arg []string) {
	if verbose {
		if len(env) > 0 {
			for i, e := range env {
				fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
			}
		}
		fmt.Fprintln(os.Stderr, exec, name, arg)
	}
}

func readVersionFile() (string, error) {
	content, err := os.ReadFile("VERSION")
	if err != nil {
		return "", err
	}
	return strings.TrimFunc(string(content), func(r rune) bool {
		return r <= ' '
	}), nil
}

func getVersion() string {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	return base
}

var dirtyPrefixes = []string{
	"DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}

const dirtySuffix = "-dirty"

func readFossilDirty() (string, error) {
	s, err := executeCommand(nil, "fossil", "status", "--differ")
	if err != nil {
		return "", err
	}
	for _, line := range strfun.SplitLines(s) {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return dirtySuffix, nil
			}
		}
	}
	return "", nil
}

func getFossilDirty() string {
	fossil, err := readFossilDirty()
	if err != nil {
		return ""
	}
	return fossil
}

func findExec(cmd string) string {
	if path, err := executeCommand(nil, "which", cmd); err == nil && path != "" {
		return strings.TrimSpace(path)
	}
	return ""
}

func cmdCheck(forRelease bool) error {
	if err := checkGoTest("./..."); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkShadow(forRelease); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	return checkFossilExtra()
}

func checkGoTest(pkg string, testParams ...string) error {
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := executeCommand(directProxy, "go", args...)
	if err != nil {
		for _, line := range strfun.SplitLines(out) {
			if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
				continue
			}
			fmt.Fprintln(os.Stderr, line)
		}
	}
	return err
}

func checkGoVet() error {
	out, err := executeCommand(nil, "go", "vet", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkShadow(forRelease bool) error {
	path, err := findExecStrict("shadow", forRelease)
	if path == "" {
		return err
	}
	out, err := executeCommand(nil, path, "-strict", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some shadowed variables found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkStaticcheck() error {
	out, err := executeCommand(nil, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
		return err
	}
	out, err := executeCommand(nil, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some unparam problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	if forRelease {
		if out2, err2 := executeCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
			fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
			}
		}
	}
	return err
}

func findExecStrict(cmd string, forRelease bool) (string, error) {
	path := findExec(cmd)
	if path != "" || !forRelease {
		return path, nil
	}
	return "", errors.New("Command '" + cmd + "' not installed, but required for release")
}

func checkFossilExtra() error {
	out, err := executeCommand(nil, "fossil", "extra")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
		return err
	}
	if len(out) > 0 {
		fmt.Fprint(os.Stderr, "Warning: unversioned file(s):")
		for i, extra := range strfun.SplitLines(out) {
			if i > 0 {
				fmt.Fprint(os.Stderr, ",")
			}
			fmt.Fprintf(os.Stderr, " %q", extra)
		}
		fmt.Fprintln(os.Stderr)
	}
	return nil
}

type zsInfo struct {
	cmd          *exec.Cmd
	out          bytes.Buffer
	adminAddress string
}

func cmdTestAPI() error {
	var err error
	var info zsInfo
	needServer := !addressInUse(":23123")
	if needServer {
		err = startZettelstore(&info)
	}
	if err != nil {
		return err
	}
	err = checkGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123")
	if needServer {
		err1 := stopZettelstore(&info)
		if err == nil {
			err = err1
		}
	}
	return err
}

func startZettelstore(info *zsInfo) error {
	info.adminAddress = ":2323"
	name, arg := "go", []string{
		"run", "cmd/zettelstore/main.go", "run",
		"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
	logCommand("FORK", nil, name, arg)
	cmd := prepareCommand(nil, name, arg, &info.out)
	if !verbose {
		cmd.Stderr = nil
	}
	err := cmd.Start()
	for i := 0; i < 100; i++ {
		time.Sleep(time.Millisecond * 100)
		if addressInUse(info.adminAddress) {
			info.cmd = cmd
			return err
		}
	}
	return errors.New("zettelstore did not start")
}

func stopZettelstore(i *zsInfo) error {
	conn, err := net.Dial("tcp", i.adminAddress)
	if err != nil {
		fmt.Println("Unable to stop Zettelstore")
		return err
	}
	io.WriteString(conn, "shutdown\n")
	conn.Close()
	err = i.cmd.Wait()
	return err
}

func addressInUse(address string) bool {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return false
	}
	conn.Close()
	return true
}

func cmdBuild() error {
	return doBuild(directProxy, getVersion(), "bin/zettelstore")
}

func doBuild(env []string, version, target string) error {
	env = append(env, "CGO_ENABLED=0")
	out, err := executeCommand(
		env,
		"go", "build",
		"-tags", "osusergo,netgo",
		"-trimpath",
		"-ldflags", fmt.Sprintf("-X main.version=%v -w", version),
		"-o", target,
		"zettelstore.de/z/cmd/zettelstore",
	)
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	return nil
}

func cmdManual() error {
	base := getReleaseVersionData()
	return createManualZip(".", base)
}

func createManualZip(path, base string) error {
	manualPath := filepath.Join("docs", "manual")
	entries, err := os.ReadDir(manualPath)
	if err != nil {
		return err
	}
	zipName := filepath.Join(path, "manual-"+base+".zip")
	zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	zipWriter := zip.NewWriter(zipFile)
	defer zipWriter.Close()

	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil
}

func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
	info, err := entry.Info()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(info)
	if err != nil {
		return err
	}
	fh.Name = entry.Name()
	fh.Method = zip.Deflate
	w, err := zipWriter.CreateHeader(fh)
	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, entry.Name()))
	if err != nil {
		return err
	}
	defer manualFile.Close()
	_, err = io.Copy(w, manualFile)
	return err
}

func getReleaseVersionData() string {
	if fossil := getFossilDirty(); fossil != "" {
		fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
	}
	base := getVersion()
	if strings.HasSuffix(base, "dev") {
		return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102")
	}
	return base
}

func cmdRelease() error {
	if err := cmdCheck(true); err != nil {
		return err
	}
	base := getReleaseVersionData()
	releases := []struct {
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
	}
	for _, rel := range releases {
		env := append([]string{}, rel.env...)
		env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		env = append(env, directProxy...)
		zsName := filepath.Join("releases", rel.name)
		if err := doBuild(env, base, zsName); err != nil {
			return err
		}
		zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
		if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
			return err
		}
		if err := os.Remove(zsName); err != nil {
			return err
		}
	}
	return createManualZip("releases", base)
}

func createReleaseZip(zsName, zipName, fileName string) error {
	zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	zw := zip.NewWriter(zipFile)
	defer zw.Close()
	err = addFileToZip(zw, zsName, fileName)
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "docs/readmezip.txt", "README.txt")
	return err
}

func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
	zsFile, err := os.Open(filepath)
	if err != nil {
		return err
	}
	defer zsFile.Close()
	stat, err := zsFile.Stat()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(stat)
	if err != nil {
		return err
	}
	fh.Name = filename
	fh.Method = zip.Deflate
	w, err := zipFile.CreateHeader(fh)
	if err != nil {
		return err
	}
	_, err = io.Copy(w, zsFile)
	return err
}

func cmdClean() error {
	for _, dir := range []string{"bin", "releases"} {
		err := os.RemoveAll(dir)
		if err != nil {
			return err
		}
	}
	out, err := executeCommand(nil, "go", "clean", "./...")
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	out, err = executeCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache")
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	return nil
}

func cmdHelp() {
	fmt.Println(`Usage: go run tools/build.go [-v] COMMAND

Options:
  -v       Verbose output.

Commands:
  build     Build the software for local computer.
  check     Check current working state: execute tests,
            static analysis tools, extra files, ...
            Is automatically done when releasing the software.
  clean     Remove all build and release directories.
  help      Output this text.
  manual    Create a ZIP file with all manual zettel
  relcheck  Check current working state for release.
  release   Create the software for various platforms and put them in
            appropriate named ZIP files.
  testapi   Start a Zettelstore and execute API tests.
  version   Print the current version of the software.

All commands can be abbreviated as long as they remain unique.`)
}

var (
	verbose bool
)

func main() {
	flag.BoolVar(&verbose, "v", false, "Verbose output")
	flag.Parse()
	var err error
	args := flag.Args()
	if len(args) < 1 {
		cmdHelp()
	} else {
		switch args[0] {
		case "b", "bu", "bui", "buil", "build":
			err = cmdBuild()
		case "m", "ma", "man", "manu", "manua", "manual":
			err = cmdManual()
		case "r", "re", "rel", "rele", "relea", "releas", "release":
			err = cmdRelease()
		case "cl", "cle", "clea", "clean":
			err = cmdClean()
		case "v", "ve", "ver", "vers", "versi", "versio", "version":
			fmt.Print(getVersion())
		case "ch", "che", "chec", "check":
			err = cmdCheck(false)
		case "relc", "relch", "relche", "relchec", "relcheck":
			err = cmdCheck(true)
		case "t", "te", "tes", "test", "testa", "testap", "testapi":
			cmdTestAPI()
		case "h", "he", "hel", "help":
			cmdHelp()
		default:
			fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
			cmdHelp()
			os.Exit(1)
		}
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

Added tools/build/build.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 main provides a command to build and run the software.
package main

import (
	"archive/zip"
	"bytes"
	"flag"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/tools"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func readVersionFile() (string, error) {
	content, err := os.ReadFile("VERSION")
	if err != nil {
		return "", err
	}
	return strings.TrimFunc(string(content), func(r rune) bool {
		return r <= ' '
	}), nil
}

func getVersion() string {
	base, err := readVersionFile()
	if err != nil {
		base = "dev"
	}
	return base
}

var dirtyPrefixes = []string{
	"DELETED ", "ADDED ", "UPDATED ", "CONFLICT ", "EDITED ", "RENAMED ", "EXTRA "}

const dirtySuffix = "-dirty"

func readFossilDirty() (string, error) {
	s, err := tools.ExecuteCommand(nil, "fossil", "status", "--differ")
	if err != nil {
		return "", err
	}
	for _, line := range strfun.SplitLines(s) {
		for _, prefix := range dirtyPrefixes {
			if strings.HasPrefix(line, prefix) {
				return dirtySuffix, nil
			}
		}
	}
	return "", nil
}

func getFossilDirty() string {
	fossil, err := readFossilDirty()
	if err != nil {
		return ""
	}
	return fossil
}

func cmdBuild() error {
	return doBuild(tools.EnvDirectProxy, getVersion(), "bin/zettelstore")
}

func doBuild(env []string, version, target string) error {
	env = append(env, "CGO_ENABLED=0")
	env = append(env, tools.EnvGoVCS...)
	out, err := tools.ExecuteCommand(
		env,
		"go", "build",
		"-tags", "osusergo,netgo",
		"-trimpath",
		"-ldflags", fmt.Sprintf("-X main.version=%v -w", version),
		"-o", target,
		"zettelstore.de/z/cmd/zettelstore",
	)
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	return nil
}

func cmdHelp() {
	fmt.Println(`Usage: go run tools/build/build.go [-v] COMMAND

Options:
  -v       Verbose output.

Commands:
  build     Build the software for local computer.
  help      Output this text.
  manual    Create a ZIP file with all manual zettel
  release   Create the software for various platforms and put them in
            appropriate named ZIP files.
  version   Print the current version of the software.

All commands can be abbreviated as long as they remain unique.`)
}

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()
	var err error
	args := flag.Args()
	if len(args) < 1 {
		cmdHelp()
	} else {
		switch args[0] {
		case "b", "bu", "bui", "buil", "build":
			err = cmdBuild()
		case "m", "ma", "man", "manu", "manua", "manual":
			err = cmdManual()
		case "r", "re", "rel", "rele", "relea", "releas", "release":
			err = cmdRelease()
		case "v", "ve", "ver", "vers", "versi", "versio", "version":
			fmt.Print(getVersion())
		case "h", "he", "hel", "help":
			cmdHelp()
		default:
			fmt.Fprintf(os.Stderr, "Unknown command %q\n", args[0])
			cmdHelp()
			os.Exit(1)
		}
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

// --- manual

func cmdManual() error {
	base := getReleaseVersionData()
	return createManualZip(".", base)
}

func createManualZip(path, base string) error {
	manualPath := filepath.Join("docs", "manual")
	entries, err := os.ReadDir(manualPath)
	if err != nil {
		return err
	}
	zipName := filepath.Join(path, "manual-"+base+".zip")
	zipFile, err := os.OpenFile(zipName, os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	zipWriter := zip.NewWriter(zipFile)
	defer zipWriter.Close()

	for _, entry := range entries {
		if err = createManualZipEntry(manualPath, entry, zipWriter); err != nil {
			return err
		}
	}
	return nil
}

const versionZid = "00001000000001"

func createManualZipEntry(path string, entry fs.DirEntry, zipWriter *zip.Writer) error {
	info, err := entry.Info()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(info)
	if err != nil {
		return err
	}
	name := entry.Name()
	fh.Name = name
	fh.Method = zip.Deflate
	w, err := zipWriter.CreateHeader(fh)
	if err != nil {
		return err
	}
	manualFile, err := os.Open(filepath.Join(path, name))
	if err != nil {
		return err
	}
	defer manualFile.Close()

	if name != versionZid+".zettel" {
		_, err = io.Copy(w, manualFile)
		return err
	}

	data, err := io.ReadAll(manualFile)
	if err != nil {
		return err
	}
	inp := input.NewInput(data)
	m := meta.NewFromInput(id.MustParse(versionZid), inp)
	m.SetNow(api.KeyModified)

	var buf bytes.Buffer
	if _, err = fmt.Fprintf(&buf, "id: %s\n", versionZid); err != nil {
		return err
	}
	if _, err = m.WriteComputed(&buf); err != nil {
		return err
	}
	version := getVersion()
	if _, err = fmt.Fprintf(&buf, "\n%s", version); err != nil {
		return err
	}
	_, err = io.Copy(w, &buf)
	return err
}

//--- release

func cmdRelease() error {
	if err := tools.Check(true); err != nil {
		return err
	}
	base := getReleaseVersionData()
	releases := []struct {
		arch string
		os   string
		env  []string
		name string
	}{
		{"amd64", "linux", nil, "zettelstore"},
		{"arm", "linux", []string{"GOARM=6"}, "zettelstore"},
		{"arm64", "darwin", nil, "zettelstore"},
		{"amd64", "darwin", nil, "zettelstore"},
		{"amd64", "windows", nil, "zettelstore.exe"},
		{"arm64", "android", nil, "zettelstore"},
	}
	for _, rel := range releases {
		env := append([]string{}, rel.env...)
		env = append(env, "GOARCH="+rel.arch, "GOOS="+rel.os)
		env = append(env, tools.EnvDirectProxy...)
		env = append(env, tools.EnvGoVCS...)
		zsName := filepath.Join("releases", rel.name)
		if err := doBuild(env, base, zsName); err != nil {
			return err
		}
		zipName := fmt.Sprintf("zettelstore-%v-%v-%v.zip", base, rel.os, rel.arch)
		if err := createReleaseZip(zsName, zipName, rel.name); err != nil {
			return err
		}
		if err := os.Remove(zsName); err != nil {
			return err
		}
	}
	return createManualZip("releases", base)
}

func getReleaseVersionData() string {
	if fossil := getFossilDirty(); fossil != "" {
		fmt.Fprintln(os.Stderr, "Warning: releasing a dirty version")
	}
	base := getVersion()
	if strings.HasSuffix(base, "dev") {
		return base[:len(base)-3] + "preview-" + time.Now().Local().Format("20060102")
	}
	return base
}

func createReleaseZip(zsName, zipName, fileName string) error {
	zipFile, err := os.OpenFile(filepath.Join("releases", zipName), os.O_RDWR|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	defer zipFile.Close()
	zw := zip.NewWriter(zipFile)
	defer zw.Close()
	err = addFileToZip(zw, zsName, fileName)
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "LICENSE.txt", "LICENSE.txt")
	if err != nil {
		return err
	}
	err = addFileToZip(zw, "docs/readmezip.txt", "README.txt")
	return err
}

func addFileToZip(zipFile *zip.Writer, filepath, filename string) error {
	zsFile, err := os.Open(filepath)
	if err != nil {
		return err
	}
	defer zsFile.Close()
	stat, err := zsFile.Stat()
	if err != nil {
		return err
	}
	fh, err := zip.FileInfoHeader(stat)
	if err != nil {
		return err
	}
	fh.Name = filename
	fh.Method = zip.Deflate
	w, err := zipFile.CreateHeader(fh)
	if err != nil {
		return err
	}
	_, err = io.Copy(w, zsFile)
	return err
}

Added tools/check/check.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) 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 main provides a command to execute unit tests.
package main

import (
	"flag"
	"fmt"
	"os"

	"zettelstore.de/z/tools"
)

var release bool

func main() {
	flag.BoolVar(&release, "r", false, "Release check")
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

	if err := tools.Check(release); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

Added tools/clean/clean.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 main provides a command to clean / remove development artifacts.
package main

import (
	"flag"
	"fmt"
	"os"

	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

	if err := cmdClean(); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

func cmdClean() error {
	for _, dir := range []string{"bin", "releases"} {
		err := os.RemoveAll(dir)
		if err != nil {
			return err
		}
	}
	out, err := tools.ExecuteCommand(nil, "go", "clean", "./...")
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	out, err = tools.ExecuteCommand(nil, "go", "clean", "-cache", "-modcache", "-testcache")
	if err != nil {
		return err
	}
	if len(out) > 0 {
		fmt.Println(out)
	}
	return nil
}

Added tools/devtools/devtools.go.






























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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 main provides a command to install development tools.
package main

import (
	"flag"
	"fmt"
	"os"

	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

	if err := cmdTools(); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

func cmdTools() error {
	tools := []struct{ name, pack string }{
		{"shadow", "golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest"},
		{"unparam", "mvdan.cc/unparam@latest"},
		{"staticcheck", "honnef.co/go/tools/cmd/staticcheck@latest"},
		{"govulncheck", "golang.org/x/vuln/cmd/govulncheck@latest"},
		{"deadcode", "golang.org/x/tools/cmd/deadcode@latest"},
		{"errcheck", "github.com/kisielk/errcheck@latest"},
		{"revive", "github.com/mgechev/revive@latest"},
	}
	for _, tool := range tools {
		err := doGoInstall(tool.pack)
		if err != nil {
			return err
		}
	}
	return nil
}
func doGoInstall(pack string) error {
	out, err := tools.ExecuteCommand(nil, "go", "install", pack)
	if err != nil {
		fmt.Fprintln(os.Stderr, "Unable to install package", pack)
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

Added tools/htmllint/htmllint.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 main provides a tool to check the validity of HTML zettel documents.
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"math/rand/v2"
	"net/url"
	"os"
	"regexp"
	"slices"
	"strings"

	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/client"
	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

	if err := cmdValidateHTML(flag.Args()); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}
func cmdValidateHTML(args []string) error {
	rawURL := "http://localhost:23123"
	if len(args) > 0 {
		rawURL = args[0]
	}
	u, err := url.Parse(rawURL)
	if err != nil {
		return err
	}
	client := client.NewClient(u)
	_, _, metaList, err := client.QueryZettelData(context.Background(), "")
	if err != nil {
		return err
	}
	zids, perm := calculateZids(metaList)
	for _, kd := range keyDescr {
		msgCount := 0
		fmt.Fprintf(os.Stderr, "Now checking: %s\n", kd.text)
		for _, zid := range zidsToUse(zids, perm, kd.sampleSize) {
			var nmsgs int
			nmsgs, err = validateHTML(client, kd.uc, api.ZettelID(zid))
			if err != nil {
				fmt.Fprintf(os.Stderr, "* error while validating zettel %v with: %v\n", zid, err)
				msgCount++
			} else {
				msgCount += nmsgs
			}
		}
		if msgCount == 1 {
			fmt.Fprintln(os.Stderr, "==> found 1 possible issue")
		} else if msgCount > 1 {
			fmt.Fprintf(os.Stderr, "==> found %v possible issues\n", msgCount)
		}
	}
	return nil
}

func calculateZids(metaList []api.ZidMetaRights) ([]string, []int) {
	zids := make([]string, len(metaList))
	for i, m := range metaList {
		zids[i] = string(m.ID)
	}
	slices.Sort(zids)
	return zids, rand.Perm(len(metaList))
}

func zidsToUse(zids []string, perm []int, sampleSize int) []string {
	if sampleSize < 0 || len(perm) <= sampleSize {
		return zids
	}
	if sampleSize == 0 {
		return nil
	}
	result := make([]string, sampleSize)
	for i := range sampleSize {
		result[i] = zids[perm[i]]
	}
	slices.Sort(result)
	return result
}

var keyDescr = []struct {
	uc         urlCreator
	text       string
	sampleSize int
}{
	{getHTMLZettel, "zettel HTML encoding", -1},
	{createJustKey('h'), "zettel web view", -1},
	{createJustKey('i'), "zettel info view", -1},
	{createJustKey('e'), "zettel edit form", 100},
	{createJustKey('c'), "zettel create form", 10},
	{createJustKey('d'), "zettel delete dialog", 200},
}

type urlCreator func(*client.Client, api.ZettelID) *api.URLBuilder

func createJustKey(key byte) urlCreator {
	return func(c *client.Client, zid api.ZettelID) *api.URLBuilder {
		return c.NewURLBuilder(key).SetZid(zid)
	}
}

func getHTMLZettel(client *client.Client, zid api.ZettelID) *api.URLBuilder {
	return client.NewURLBuilder('z').SetZid(zid).
		AppendKVQuery(api.QueryKeyEncoding, api.EncodingHTML).
		AppendKVQuery(api.QueryKeyPart, api.PartZettel)
}

func validateHTML(client *client.Client, uc urlCreator, zid api.ZettelID) (int, error) {
	ub := uc(client, zid)
	if tools.Verbose {
		fmt.Fprintf(os.Stderr, "GET %v\n", ub)
	}
	data, err := client.Get(context.Background(), ub)
	if err != nil {
		return 0, err
	}
	if len(data) == 0 {
		return 0, nil
	}
	_, stderr, err := tools.ExecuteFilter(data, nil, "tidy", "-e", "-q", "-lang", "en")
	if err != nil {
		switch err.Error() {
		case "exit status 1":
		case "exit status 2":
		default:
			log.Println("SERR", stderr)
			return 0, err
		}
	}
	if stderr == "" {
		return 0, nil
	}
	if msgs := filterTidyMessages(strings.Split(stderr, "\n")); len(msgs) > 0 {
		fmt.Fprintln(os.Stderr, zid)
		for _, msg := range msgs {
			fmt.Fprintln(os.Stderr, "-", msg)
		}
		return len(msgs), nil
	}
	return 0, nil
}

var reLine = regexp.MustCompile(`line \d+ column \d+ - (.+): (.+)`)

func filterTidyMessages(lines []string) []string {
	result := make([]string, 0, len(lines))
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		matches := reLine.FindStringSubmatch(line)
		if len(matches) <= 1 {
			if line == "This document has errors that must be fixed before" ||
				line == "using HTML Tidy to generate a tidied up version." {
				continue
			}
			result = append(result, "!!!"+line)
			continue
		}
		if matches[1] == "Error" {
			if len(matches) > 2 {
				if matches[2] == "<search> is not recognized!" {
					continue
				}
			}
		}
		if matches[1] != "Warning" {
			result = append(result, "???"+line)
			continue
		}
		if len(matches) > 2 {
			switch matches[2] {
			case "discarding unexpected <search>",
				"discarding unexpected </search>",
				`<input> proprietary attribute "inputmode"`:
				continue
			}
		}
		result = append(result, line)
	}
	return result
}

Added tools/testapi/testapi.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 main provides a command to test the API
package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"strings"
	"time"

	"zettelstore.de/z/tools"
)

func main() {
	flag.BoolVar(&tools.Verbose, "v", false, "Verbose output")
	flag.Parse()

	if err := cmdTestAPI(); err != nil {
		fmt.Fprintln(os.Stderr, err)
	}
}

type zsInfo struct {
	cmd          *exec.Cmd
	out          strings.Builder
	adminAddress string
}

func cmdTestAPI() error {
	var err error
	var info zsInfo
	needServer := !addressInUse(":23123")
	if needServer {
		err = startZettelstore(&info)
	}
	if err != nil {
		return err
	}
	err = tools.CheckGoTest("zettelstore.de/z/tests/client", "-base-url", "http://127.0.0.1:23123")
	if needServer {
		err1 := stopZettelstore(&info)
		if err == nil {
			err = err1
		}
	}
	return err
}

func startZettelstore(info *zsInfo) error {
	info.adminAddress = ":2323"
	name, arg := "go", []string{
		"run", "cmd/zettelstore/main.go", "run",
		"-c", "./testdata/testbox/19700101000000.zettel", "-a", info.adminAddress[1:]}
	tools.LogCommand("FORK", nil, name, arg)
	cmd := tools.PrepareCommand(tools.EnvGoVCS, name, arg, nil, &info.out, os.Stderr)
	if !tools.Verbose {
		cmd.Stderr = nil
	}
	err := cmd.Start()
	time.Sleep(2 * time.Second)
	for range 100 {
		time.Sleep(time.Millisecond * 100)
		if addressInUse(info.adminAddress) {
			info.cmd = cmd
			return err
		}
	}
	time.Sleep(4 * time.Second) // Wait for all zettel to be indexed.
	return errors.New("zettelstore did not start")
}

func stopZettelstore(i *zsInfo) error {
	conn, err := net.Dial("tcp", i.adminAddress)
	if err != nil {
		fmt.Println("Unable to stop Zettelstore")
		return err
	}
	io.WriteString(conn, "shutdown\n")
	conn.Close()
	err = i.cmd.Wait()
	return err
}

func addressInUse(address string) bool {
	conn, err := net.Dial("tcp", address)
	if err != nil {
		return false
	}
	conn.Close()
	return true
}

Added tools/tools.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 tools provides a collection of functions to build needed tools.
package tools

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"zettelstore.de/z/strfun"
)

// Some constants to make Go work with fossil.
var (
	EnvDirectProxy = []string{"GOPROXY=direct"}
	EnvGoVCS       = []string{"GOVCS=zettelstore.de:fossil,t73f.de:fossil"}
)

// Verbose signals a verbose tool execution.
var Verbose bool

// ExecuteCommand executes a specific command.
func ExecuteCommand(env []string, name string, arg ...string) (string, error) {
	LogCommand("EXEC", env, name, arg)
	var out strings.Builder
	cmd := PrepareCommand(env, name, arg, nil, &out, os.Stderr)
	err := cmd.Run()
	return out.String(), err
}

// ExecuteFilter executes an external program to be used as a filter.
func ExecuteFilter(data []byte, env []string, name string, arg ...string) (string, string, error) {
	LogCommand("EXEC", env, name, arg)
	var stdout, stderr strings.Builder
	cmd := PrepareCommand(env, name, arg, bytes.NewReader(data), &stdout, &stderr)
	err := cmd.Run()
	return stdout.String(), stderr.String(), err
}

// PrepareCommand creates a commands to be executed.
func PrepareCommand(env []string, name string, arg []string, in io.Reader, stdout, stderr io.Writer) *exec.Cmd {
	if len(env) > 0 {
		env = append(env, os.Environ()...)
	}
	cmd := exec.Command(name, arg...)
	cmd.Env = env
	cmd.Stdin = in
	cmd.Stdout = stdout
	cmd.Stderr = stderr
	return cmd
}

// LogCommand logs the execution of a command.
func LogCommand(exec string, env []string, name string, arg []string) {
	if Verbose {
		if len(env) > 0 {
			for i, e := range env {
				fmt.Fprintf(os.Stderr, "ENV%d %v\n", i+1, e)
			}
		}
		fmt.Fprintln(os.Stderr, exec, name, arg)
	}
}

// Check the source with some linters.
func Check(forRelease bool) error {
	if err := CheckGoTest("./..."); err != nil {
		return err
	}
	if err := checkGoVet(); err != nil {
		return err
	}
	if err := checkShadow(forRelease); err != nil {
		return err
	}
	if err := checkStaticcheck(); err != nil {
		return err
	}
	if err := checkUnparam(forRelease); err != nil {
		return err
	}
	if err := checkRevive(); err != nil {
		return err
	}
	if forRelease {
		if err := checkGoVulncheck(); err != nil {
			return err
		}
	}
	return checkFossilExtra()
}

// CheckGoTest runs all internal unti tests.
func CheckGoTest(pkg string, testParams ...string) error {
	var env []string
	env = append(env, EnvDirectProxy...)
	env = append(env, EnvGoVCS...)
	args := []string{"test", pkg}
	args = append(args, testParams...)
	out, err := ExecuteCommand(env, "go", args...)
	if err != nil {
		for _, line := range strfun.SplitLines(out) {
			if strings.HasPrefix(line, "ok") || strings.HasPrefix(line, "?") {
				continue
			}
			fmt.Fprintln(os.Stderr, line)
		}
	}
	return err
}
func checkGoVet() error {
	out, err := ExecuteCommand(EnvGoVCS, "go", "vet", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkShadow(forRelease bool) error {
	path, err := findExecStrict("shadow", forRelease)
	if path == "" {
		return err
	}
	out, err := ExecuteCommand(EnvGoVCS, path, "-strict", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some shadowed variables found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkStaticcheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "staticcheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some staticcheck problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkRevive() error {
	out, err := ExecuteCommand(EnvGoVCS, "revive", "./...")
	if err != nil || out != "" {
		fmt.Fprintln(os.Stderr, "Some revive problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}

func checkUnparam(forRelease bool) error {
	path, err := findExecStrict("unparam", forRelease)
	if path == "" {
		return err
	}
	out, err := ExecuteCommand(EnvGoVCS, path, "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some unparam problems found")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	if forRelease {
		if out2, err2 := ExecuteCommand(nil, path, "-exported", "-tests", "./..."); err2 != nil {
			fmt.Fprintln(os.Stderr, "Some optional unparam problems found")
			if len(out2) > 0 {
				fmt.Fprintln(os.Stderr, out2)
			}
		}
	}
	return err
}

func checkGoVulncheck() error {
	out, err := ExecuteCommand(EnvGoVCS, "govulncheck", "./...")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Some checks failed")
		if len(out) > 0 {
			fmt.Fprintln(os.Stderr, out)
		}
	}
	return err
}
func findExec(cmd string) string {
	if path, err := ExecuteCommand(nil, "which", cmd); err == nil && path != "" {
		return strings.TrimSpace(path)
	}
	return ""
}

func findExecStrict(cmd string, forRelease bool) (string, error) {
	path := findExec(cmd)
	if path != "" || !forRelease {
		return path, nil
	}
	return "", errors.New("Command '" + cmd + "' not installed, but required for release")
}

func checkFossilExtra() error {
	out, err := ExecuteCommand(nil, "fossil", "extra")
	if err != nil {
		fmt.Fprintln(os.Stderr, "Unable to execute 'fossil extra'")
		return err
	}
	if len(out) > 0 {
		fmt.Fprint(os.Stderr, "Warning: unversioned file(s):")
		for i, extra := range strfun.SplitLines(out) {
			if i > 0 {
				fmt.Fprint(os.Stderr, ",")
			}
			fmt.Fprintf(os.Stderr, " %q", extra)
		}
		fmt.Fprintln(os.Stderr)
	}
	return nil
}

Changes to usecase/authenticate.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
1

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

-
+






+
+
+






-
+



-
+


-
-
-
+
+
+
-

-
-
-
-
-
-





-
-
+



-
+



-
-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"
	"math/rand"
	"math/rand/v2"
	"net/http"
	"time"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/auth/cred"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
	"zettelstore.de/z/query"
)

// AuthenticatePort is the interface used by this use case.
type AuthenticatePort interface {
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	SelectMeta(context.Context, *query.Query) ([]*meta.Meta, error)
}

// Authenticate is the data for this use case.
type Authenticate struct {
	log       *logger.Logger
	token     auth.TokenManager
	port      AuthenticatePort
	ucGetUser GetUser
	ucGetUser *GetUser
}

// NewAuthenticate creates a new use case.
func NewAuthenticate(log *logger.Logger, token auth.TokenManager, authz auth.AuthzManager, port AuthenticatePort) Authenticate {
func NewAuthenticate(log *logger.Logger, token auth.TokenManager, ucGetUser *GetUser) Authenticate {
	return Authenticate{
		log:       log,
		token:     token,
		port:      port,
		ucGetUser: NewGetUser(authz, port),
		ucGetUser: ucGetUser,
	}
}

// Run executes the use case.
//
// Parameter "r" is just included to produce better logging messages. It may be nil. Do not use it
// for other purposes.
92
93
94
95
96
97
98
99

100
101
102
103
104
105
106
86
87
88
89
90
91
92

93
94
95
96
97
98
99
100







-
+







		"$2a$10$WHcSO3G9afJ3zlOYQR1suuf83bCXED2jmzjti/MH4YH4l2mivDuze", id.Invalid, "", "")
}

// addDelay after credential checking to allow some CPU time for other tasks.
// durDelay is the normal delay, if time spend for checking is smaller than
// the minimum delay minDelay. In addition some jitter (+/- 50 ms) is added.
func addDelay(start time.Time, durDelay, minDelay time.Duration) {
	jitter := time.Duration(rand.Intn(100)-50) * time.Millisecond
	jitter := time.Duration(rand.IntN(100)-50) * time.Millisecond
	if elapsed := time.Since(start); elapsed+minDelay < durDelay {
		time.Sleep(durDelay - elapsed + jitter)
	} else {
		time.Sleep(minDelay + jitter)
	}
}

135
136
137
138
139
140
141
142

143
144
145
146

147
148
149

150
151
129
130
131
132
133
134
135

136
137
138
139

140
141
142

143
144
145







-
+



-
+


-
+


	IsAuthenticatedAndValid
	IsAuthenticatedAndInvalid
)

// Run executes the use case.
func (uc *IsAuthenticated) Run(ctx context.Context) IsAuthenticatedResult {
	if !uc.authz.WithAuth() {
		uc.log.Sense().Str("auth", "disabled").Msg("IsAuthenticated")
		uc.log.Info().Str("auth", "disabled").Msg("IsAuthenticated")
		return IsAuthenticatedDisabled
	}
	if uc.port.GetUser(ctx) == nil {
		uc.log.Sense().Msg("IsAuthenticated is false")
		uc.log.Info().Msg("IsAuthenticated is false")
		return IsAuthenticatedAndInvalid
	}
	uc.log.Sense().Msg("IsAuthenticated is true")
	uc.log.Info().Msg("IsAuthenticated is true")
	return IsAuthenticatedAndValid
}

Deleted usecase/context.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






































































































































































































































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

import (
	"context"

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

// ZettelContextPort is the interface used by this use case.
type ZettelContextPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// ZettelContextConfig is the interface to allow the usecase to read some config data.
type ZettelContextConfig interface {
	// Get returns a config value that might be user-specific.
	Get(ctx context.Context, m *meta.Meta, key string) string
}

// ZettelContext is the data for this use case.
type ZettelContext struct {
	port   ZettelContextPort
	config ZettelContextConfig
}

// NewZettelContext creates a new use case.
func NewZettelContext(port ZettelContextPort, config ZettelContextConfig) ZettelContext {
	return ZettelContext{port: port, config: config}
}

// ZettelContextDirection determines the way, the context is calculated.
type ZettelContextDirection int

// Constant values for ZettelContextDirection
const (
	_                     ZettelContextDirection = iota
	ZettelContextForward                         // Traverse all forwarding links
	ZettelContextBackward                        // Traverse all backwaring links
	ZettelContextBoth                            // Traverse both directions
)

// Run executes the use case.
func (uc ZettelContext) Run(ctx context.Context, zid id.Zid, dir ZettelContextDirection, maxCost, limit int) (result []*meta.Meta, err error) {
	start, err := uc.port.GetMeta(ctx, zid)
	if err != nil {
		return nil, err
	}
	tasks := newQueue(start, maxCost, limit)
	isBackward := dir == ZettelContextBoth || dir == ZettelContextBackward
	isForward := dir == ZettelContextBoth || dir == ZettelContextForward
	for {
		m, cost := tasks.next()
		if m == nil {
			break
		}
		result = append(result, m)

		for _, p := range m.ComputedPairsRest() {
			tasks.addPair(ctx, uc.port, p.Key, p.Value, cost, isBackward, isForward)
		}
	}
	return result, nil
}

type ztlCtxTask struct {
	next *ztlCtxTask
	prev *ztlCtxTask
	meta *meta.Meta
	cost int
}

type contextQueue struct {
	seen    id.Set
	first   *ztlCtxTask
	last    *ztlCtxTask
	maxCost int
	limit   int
}

func newQueue(m *meta.Meta, maxCost, limit int) *contextQueue {
	task := &ztlCtxTask{
		next: nil,
		prev: nil,
		meta: m,
		cost: 0,
	}
	result := &contextQueue{
		seen:    id.NewSet(),
		first:   task,
		last:    task,
		maxCost: maxCost,
		limit:   limit,
	}
	return result
}

func (zc *contextQueue) addPair(
	ctx context.Context, port ZettelContextPort,
	key, value string,
	curCost int, isBackward, isForward bool,
) {
	if key == api.KeyBack {
		return
	}
	newCost := curCost + contextCost(key)
	if key == api.KeyBackward {
		if isBackward {
			zc.addIDSet(ctx, port, newCost, value)
		}
		return
	}
	if key == api.KeyForward {
		if isForward {
			zc.addIDSet(ctx, port, newCost, value)
		}
		return
	}
	hasInverse := meta.Inverse(key) != ""
	if (!hasInverse || !isBackward) && (hasInverse || !isForward) {
		return
	}
	if t := meta.Type(key); t == meta.TypeID {
		zc.addID(ctx, port, newCost, value)
	} else if t == meta.TypeIDSet {
		zc.addIDSet(ctx, port, newCost, value)
	}
}

func contextCost(key string) int {
	switch key {
	case api.KeyFolge, api.KeyPrecursor:
		return 1
	case api.KeySuccessors, api.KeyPredecessor:
		return 2
	}
	return 5
}

func (zc *contextQueue) addID(ctx context.Context, port ZettelContextPort, newCost int, value string) {
	if (zc.maxCost > 0 && newCost > zc.maxCost) || zc.hasLimit() {
		return
	}

	zid, err := id.Parse(value)
	if err != nil {
		return
	}

	m, err := port.GetMeta(ctx, zid)
	if err != nil {
		return
	}

	task := &ztlCtxTask{next: nil, prev: nil, meta: m, cost: newCost}
	if zc.first == nil {
		zc.first = task
		zc.last = task
		return
	}

	// Search backward for a task t with at most the same cost
	for t := zc.last; t != nil; t = t.prev {
		if t.cost <= task.cost {
			// Found!
			if t.next != nil {
				t.next.prev = task
			}
			task.next = t.next
			t.next = task
			task.prev = t
			if task.next == nil {
				zc.last = task
			}
			return
		}
	}

	// We have not found a task, therefore the new task is the first one
	task.next = zc.first
	zc.first.prev = task
	zc.first = task
}

func (zc *contextQueue) addIDSet(ctx context.Context, port ZettelContextPort, newCost int, value string) {
	for _, val := range meta.ListFromValue(value) {
		zc.addID(ctx, port, newCost, val)
	}
}

func (zc *contextQueue) next() (*meta.Meta, int) {
	if zc.hasLimit() {
		return nil, -1
	}
	for zc.first != nil {
		task := zc.first
		zc.first = task.next
		if zc.first == nil {
			zc.last = nil
		} else {
			zc.first.prev = nil
		}
		m := task.meta
		zid := m.Zid
		_, found := zc.seen[zid]
		if found {
			continue
		}
		zc.seen.Zid(zid)
		return m, task.cost
	}
	return nil, -1
}

func (zc *contextQueue) hasLimit() bool {
	limit := zc.limit
	return limit > 0 && len(zc.seen) > limit
}

Changes to usecase/create_zettel.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16
17

18
19
20
21
22




23
24
25
26
27
28

29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50




51
52
53
54
55
56

57
58
59
60

61
62
63
64
65
66
67

68
69
70
71

72
73
74
75
76
77
78
79
80
81















82
83
84
85

86
87
88
89
90
91

92
93
94
95
96
97
98



99
100
101







102
103
104
105
106
107
108
1

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

-
+






+
+
+








-
+

-
-
-
-
+
+
+
+





-
+



















-
-
-
+
+
+
+





-
+



-
+






-
+



-
+





-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+



-
+



-
-
-
+







+
+
+


-
+
+
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"
	"time"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// CreateZettelPort is the interface used by this use case.
type CreateZettelPort interface {
	// CreateZettel creates a new zettel.
	CreateZettel(ctx context.Context, zettel domain.Zettel) (id.Zid, error)
	CreateZettel(ctx context.Context, zettel zettel.Zettel) (id.Zid, error)
}

// CreateZettel is the data for this use case.
type CreateZettel struct {
	log      *logger.Logger
	rtConfig config.Config
	port     CreateZettelPort
}

// NewCreateZettel creates a new use case.
func NewCreateZettel(log *logger.Logger, rtConfig config.Config, port CreateZettelPort) CreateZettel {
	return CreateZettel{
		log:      log,
		rtConfig: rtConfig,
		port:     port,
	}
}

// PrepareCopy the zettel for further modification.
func (*CreateZettel) PrepareCopy(origZettel domain.Zettel) domain.Zettel {
	m := origZettel.Meta.Clone()
	if title, found := m.Get(api.KeyTitle); found {
func (*CreateZettel) PrepareCopy(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := origMeta.Clone()
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Copy", "Copy of "))
	}
	setReadonly(m)
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
	return zettel.Zettel{Meta: m, Content: content}
}

// PrepareVersion the zettel for further modification.
func (*CreateZettel) PrepareVersion(origZettel domain.Zettel) domain.Zettel {
func (*CreateZettel) PrepareVersion(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := origMeta.Clone()
	m.Set(api.KeyPredecessor, origMeta.Zid.String())
	setReadonly(m)
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
	return zettel.Zettel{Meta: m, Content: content}
}

// PrepareFolge the zettel for further modification.
func (*CreateZettel) PrepareFolge(origZettel domain.Zettel) domain.Zettel {
func (*CreateZettel) PrepareFolge(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Folge", "Folge of "))
	}
	m.SetNonEmpty(api.KeyRole, origMeta.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, origMeta.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, origMeta.GetDefault(api.KeySyntax, ""))
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return domain.Zettel{Meta: m, Content: domain.NewContent(nil)}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrecursor, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareSequel the zettel for further modification.
func (*CreateZettel) PrepareSequel(origZettel zettel.Zettel) zettel.Zettel {
	origMeta := origZettel.Meta
	m := meta.New(id.Invalid)
	if title, found := origMeta.Get(api.KeyTitle); found {
		m.Set(api.KeyTitle, prependTitle(title, "Sequel", "Sequel of "))
	}
	updateMetaRoleTagsSyntax(m, origMeta)
	m.Set(api.KeyPrequel, origMeta.Zid.String())
	return zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}
}

// PrepareNew the zettel for further modification.
func (*CreateZettel) PrepareNew(origZettel domain.Zettel) domain.Zettel {
func (*CreateZettel) PrepareNew(origZettel zettel.Zettel, newTitle string) zettel.Zettel {
	m := meta.New(id.Invalid)
	om := origZettel.Meta
	m.SetNonEmpty(api.KeyTitle, om.GetDefault(api.KeyTitle, ""))
	m.SetNonEmpty(api.KeyRole, om.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, om.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, om.GetDefault(api.KeySyntax, ""))
	updateMetaRoleTagsSyntax(m, om)

	const prefixLen = len(meta.NewPrefix)
	for _, pair := range om.PairsRest() {
		if key := pair.Key; len(key) > prefixLen && key[0:prefixLen] == meta.NewPrefix {
			m.Set(key[prefixLen:], pair.Value)
		}
	}
	if newTitle != "" {
		m.Set(api.KeyTitle, newTitle)
	}
	content := origZettel.Content
	content.TrimSpace()
	return domain.Zettel{Meta: m, Content: content}
	return zettel.Zettel{Meta: m, Content: content}
}

func updateMetaRoleTagsSyntax(m, orig *meta.Meta) {
	m.SetNonEmpty(api.KeyRole, orig.GetDefault(api.KeyRole, ""))
	m.SetNonEmpty(api.KeyTags, orig.GetDefault(api.KeyTags, ""))
	m.SetNonEmpty(api.KeySyntax, orig.GetDefault(api.KeySyntax, meta.DefaultSyntax))
}

func prependTitle(title, s0, s1 string) string {
	if len(title) > 0 {
		return s1 + title
	}
	return s0
116
117
118
119
120
121
122
123

124
125
126
127
128
129

130
131
132
133
134
135
136
137
137
138
139
140
141
142
143

144
145
146
147
148
149

150
151
152
153
154
155
156
157
158







-
+





-
+








		// could be done: set it to a value, so that the current user will be able
		// to modify it later.
		m.Set(api.KeyReadOnly, api.ValueFalse)
	}
}

// Run executes the use case.
func (uc *CreateZettel) Run(ctx context.Context, zettel domain.Zettel) (id.Zid, error) {
func (uc *CreateZettel) Run(ctx context.Context, zettel zettel.Zettel) (id.Zid, error) {
	m := zettel.Meta
	if m.Zid.IsValid() {
		return m.Zid, nil // TODO: new error: already exists
	}

	m.Set(api.KeyCreated, time.Now().Local().Format(id.ZidLayout))
	m.Set(api.KeyCreated, time.Now().Local().Format(id.TimestampLayout))
	m.Delete(api.KeyModified)
	m.YamlSep = uc.rtConfig.GetYAMLHeader()

	zettel.Content.TrimSpace()
	zid, err := uc.port.CreateZettel(ctx, zettel)
	uc.log.Info().User(ctx).Zid(zid).Err(err).Msg("Create zettel")
	return zid, err
}

Changes to usecase/delete_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
1

2
3
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
// 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 usecase

import (
	"context"

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

// DeleteZettelPort is the interface used by this use case.
type DeleteZettelPort interface {
	// DeleteZettel removes the zettel from the box.
	DeleteZettel(ctx context.Context, zid id.Zid) error
}

Changes to usecase/evaluate.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
1

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

import (
	"context"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Evaluate is the data for this use case.
type Evaluate struct {
	rtConfig  config.Config
	getZettel GetZettel
	getMeta   GetMeta
	rtConfig    config.Config
	ucGetZettel *GetZettel
	ucQuery     *Query
	listMeta  ListMeta
}

// NewEvaluate creates a new use case.
func NewEvaluate(rtConfig config.Config, getZettel GetZettel, getMeta GetMeta, listMeta ListMeta) Evaluate {
func NewEvaluate(rtConfig config.Config, ucGetZettel *GetZettel, ucQuery *Query) Evaluate {
	return Evaluate{
		rtConfig:  rtConfig,
		getZettel: getZettel,
		getMeta:   getMeta,
		rtConfig:    rtConfig,
		ucGetZettel: ucGetZettel,
		ucQuery:     ucQuery,
		listMeta:  listMeta,
	}
}

// Run executes the use case.
func (uc *Evaluate) Run(ctx context.Context, zid id.Zid, syntax string) (*ast.ZettelNode, error) {
	zettel, err := uc.getZettel.Run(ctx, zid)
	zettel, err := uc.ucGetZettel.Run(ctx, zid)
	if err != nil {
		return nil, err
	}
	zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil
	return uc.RunZettel(ctx, zettel, syntax), nil
	if err != nil {
		return nil, err
	}
}

// RunZettel executes the use case for a given zettel.
func (uc *Evaluate) RunZettel(ctx context.Context, zettel zettel.Zettel, syntax string) *ast.ZettelNode {
	zn := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig)
	evaluator.EvaluateZettel(ctx, uc, uc.rtConfig, zn)
	return zn, nil
	return zn
}

// RunBlockNode executes the use case for a metadata list.
func (uc *Evaluate) RunBlockNode(ctx context.Context, bn ast.BlockNode) ast.BlockSlice {
	if bn == nil {
		return nil
	}
	bns := ast.BlockSlice{bn}
	evaluator.EvaluateBlock(ctx, uc, uc.rtConfig, &bns)
	return bns
}

// RunMetadata executes the use case for a metadata value.
func (uc *Evaluate) RunMetadata(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadata(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// RunMetadataNoLink executes the use case for a metadata value, but ignores link and footnote nodes.
func (uc *Evaluate) RunMetadataNoLink(ctx context.Context, value string) ast.InlineSlice {
	is := parser.ParseMetadataNoLink(value)
	evaluator.EvaluateInline(ctx, uc, uc.rtConfig, &is)
	return is
}

// GetMeta retrieves the metadata of a given zettel identifier.
func (uc *Evaluate) GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	return uc.getMeta.Run(ctx, zid)
}

// GetZettel retrieves the full zettel of a given zettel identifier.
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
	return uc.getZettel.Run(ctx, zid)
func (uc *Evaluate) GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	return uc.ucGetZettel.Run(ctx, zid)
}

// SelectMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.listMeta.Run(ctx, q)
// QueryMeta returns a list of metadata that comply to the given selection criteria.
func (uc *Evaluate) QueryMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.ucQuery.Run(ctx, q)
}

Deleted usecase/get_all_meta.go.

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

import (
	"context"

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

// GetAllMetaPort is the interface used by this use case.
type GetAllMetaPort interface {
	// GetAllMeta retrieves just the meta data of a specific zettel.
	GetAllMeta(ctx context.Context, zid id.Zid) ([]*meta.Meta, error)
}

// GetAllMeta is the data for this use case.
type GetAllMeta struct {
	port GetAllMetaPort
}

// NewGetAllMeta creates a new use case.
func NewGetAllMeta(port GetAllMetaPort) GetAllMeta {
	return GetAllMeta{port: port}
}

// Run executes the use case.
func (uc GetAllMeta) Run(ctx context.Context, zid id.Zid) ([]*meta.Meta, error) {
	return uc.port.GetAllMeta(ctx, zid)
}

Added usecase/get_all_zettel.go.










































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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 usecase

import (
	"context"

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

// GetAllZettelPort is the interface used by this use case.
type GetAllZettelPort interface {
	GetAllZettel(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error)
}

// GetAllZettel is the data for this use case.
type GetAllZettel struct {
	port GetAllZettelPort
}

// NewGetAllZettel creates a new use case.
func NewGetAllZettel(port GetAllZettelPort) GetAllZettel {
	return GetAllZettel{port: port}
}

// Run executes the use case.
func (uc GetAllZettel) Run(ctx context.Context, zid id.Zid) ([]zettel.Zettel, error) {
	return uc.port.GetAllZettel(ctx, zid)
}

Deleted usecase/get_meta.go.

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

import (
	"context"

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

// GetMetaPort is the interface used by this use case.
type GetMetaPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// GetMeta is the data for this use case.
type GetMeta struct {
	port GetMetaPort
}

// NewGetMeta creates a new use case.
func NewGetMeta(port GetMetaPort) GetMeta {
	return GetMeta{port: port}
}

// Run executes the use case.
func (uc GetMeta) Run(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
	return uc.port.GetMeta(ctx, zid)
}

Added usecase/get_special_zettel.go.


















































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 usecase

import (
	"context"

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

// TagZettel is the usecase of retrieving a "tag zettel", i.e. a zettel that
// describes a given tag. A tag zettel must have the tag's name in its title
// and must have a role=tag.

// TagZettelPort is the interface used by this use case.
type TagZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// TagZettel is the data for this use case.
type TagZettel struct {
	port  GetZettelPort
	query *Query
}

// NewTagZettel creates a new use case.
func NewTagZettel(port GetZettelPort, query *Query) TagZettel {
	return TagZettel{port: port, query: query}
}

// Run executes the use case.
func (uc TagZettel) Run(ctx context.Context, tag string) (zettel.Zettel, error) {
	tag = meta.NormalizeTag(tag)
	q := query.Parse(
		api.KeyTitle + api.SearchOperatorEqual + tag + " " +
			api.KeyRole + api.SearchOperatorHas + api.ValueRoleTag)
	ml, err := uc.query.Run(ctx, q)
	if err != nil {
		return zettel.Zettel{}, err
	}
	for _, m := range ml {
		z, errZ := uc.port.GetZettel(ctx, m.Zid)
		if errZ == nil {
			return z, nil
		}
	}
	return zettel.Zettel{}, ErrTagZettelNotFound{Tag: tag}
}

// ErrTagZettelNotFound is returned if a tag zettel was not found.
type ErrTagZettelNotFound struct{ Tag string }

func (etznf ErrTagZettelNotFound) Error() string { return "tag zettel not found: " + etznf.Tag }

// RoleZettel is the usecase of retrieving a "role zettel", i.e. a zettel that
// describes a given role. A role zettel must have the role's name in its title
// and must have a role=role.

// RoleZettelPort is the interface used by this use case.
type RoleZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// RoleZettel is the data for this use case.
type RoleZettel struct {
	port  GetZettelPort
	query *Query
}

// NewRoleZettel creates a new use case.
func NewRoleZettel(port GetZettelPort, query *Query) RoleZettel {
	return RoleZettel{port: port, query: query}
}

// Run executes the use case.
func (uc RoleZettel) Run(ctx context.Context, role string) (zettel.Zettel, error) {
	q := query.Parse(
		api.KeyTitle + api.SearchOperatorEqual + role + " " +
			api.KeyRole + api.SearchOperatorHas + api.ValueRoleRole)
	ml, err := uc.query.Run(ctx, q)
	if err != nil {
		return zettel.Zettel{}, err
	}
	for _, m := range ml {
		z, errZ := uc.port.GetZettel(ctx, m.Zid)
		if errZ == nil {
			return z, nil
		}
	}
	return zettel.Zettel{}, ErrRoleZettelNotFound{Role: role}
}

// ErrRoleZettelNotFound is returned if a role zettel was not found.
type ErrRoleZettelNotFound struct{ Role string }

func (etznf ErrRoleZettelNotFound) Error() string { return "role zettel not found: " + etznf.Role }

Changes to usecase/get_user.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
1

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

-
+






+
+
+







+
-
-
-
-
-
-
+
+
+
+
+
+







-
-
+
+




















-
-
-
+
+
+



-
+














-
+














-
+




+





//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"

	"t73f.de/r/zsc/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/query"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// Use case: return user identified by meta key ident.
// ---------------------------------------------------

// GetUserPort is the interface used by this use case.
type GetUserPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// GetUser is the data for this use case.
type GetUser struct {
	authz auth.AuthzManager
	port  GetUserPort
}

// NewGetUser creates a new use case.
func NewGetUser(authz auth.AuthzManager, port GetUserPort) GetUser {
	return GetUser{authz: authz, port: port}
}

// Run executes the use case.
func (uc GetUser) Run(ctx context.Context, ident string) (*meta.Meta, error) {
	ctx = box.NoEnrichContext(ctx)

	// It is important to try first with the owner. First, because another user
	// could give herself the same ''ident''. Second, in most cases the owner
	// will authenticate.
	identMeta, err := uc.port.GetMeta(ctx, uc.authz.Owner())
	if err == nil && identMeta.GetDefault(api.KeyUserID, "") == ident {
		return identMeta, nil
	identZettel, err := uc.port.GetZettel(ctx, uc.authz.Owner())
	if err == nil && identZettel.Meta.GetDefault(api.KeyUserID, "") == ident {
		return identZettel.Meta, nil
	}
	// Owner was not found or has another ident. Try via list search.
	q := query.Parse(api.KeyUserID + api.SearchOperatorHas + ident + " " + api.SearchOperatorHas + ident)
	metaList, err := uc.port.SelectMeta(ctx, q)
	metaList, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil, err
	}
	if len(metaList) < 1 {
		return nil, nil
	}
	return metaList[len(metaList)-1], nil
}

// Use case: return an user identified by zettel id and assert given ident value.
// ------------------------------------------------------------------------------

// GetUserByZidPort is the interface used by this use case.
type GetUserByZidPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// GetUserByZid is the data for this use case.
type GetUserByZid struct {
	port GetUserByZidPort
}

// NewGetUserByZid creates a new use case.
func NewGetUserByZid(port GetUserByZidPort) GetUserByZid {
	return GetUserByZid{port: port}
}

// GetUser executes the use case.
func (uc GetUserByZid) GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error) {
	userMeta, err := uc.port.GetMeta(box.NoEnrichContext(ctx), zid)
	userZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), zid)
	if err != nil {
		return nil, err
	}

	userMeta := userZettel.Meta
	if val, ok := userMeta.Get(api.KeyUserID); !ok || val != ident {
		return nil, nil
	}
	return userMeta, nil
}

Changes to usecase/get_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
1

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

-
+






+
+
+







-
-
+
+





-
+













-
+


//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"

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

// GetZettelPort is the interface used by this use case.
type GetZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// GetZettel is the data for this use case.
type GetZettel struct {
	port GetZettelPort
}

// NewGetZettel creates a new use case.
func NewGetZettel(port GetZettelPort) GetZettel {
	return GetZettel{port: port}
}

// Run executes the use case.
func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (domain.Zettel, error) {
func (uc GetZettel) Run(ctx context.Context, zid id.Zid) (zettel.Zettel, error) {
	return uc.port.GetZettel(ctx, zid)
}

Changes to usecase/lists.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
1

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

-
+






+
+
+







-
+

-
-
-
+
+
+


-
-
-
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-


-
-
+















-
+
















-
-
+















-
+





//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/zettel/meta"
)

// ListMetaPort is the interface used by this use case.
type ListMetaPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// -------- List syntax ------------------------------------------------------
// ListMeta is the data for this use case.
type ListMeta struct {
	port ListMetaPort
}


// NewListMeta creates a new use case.
func NewListMeta(port ListMetaPort) ListMeta {
	return ListMeta{port: port}
}

// Run executes the use case.
func (uc ListMeta) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	return uc.port.SelectMeta(ctx, q)
}

// -------- List roles -------------------------------------------------------

// ListSyntaxPort is the interface used by this use case.
type ListSyntaxPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// ListSyntax is the data for this use case.
type ListSyntax struct {
	port ListSyntaxPort
}

// NewListSyntax creates a new use case.
func NewListSyntax(port ListSyntaxPort) ListSyntax {
	return ListSyntax{port: port}
}

// Run executes the use case.
func (uc ListSyntax) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(api.KeySyntax + api.ExistOperator) // We look for all metadata with a syntax key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q)
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	result := meta.CreateArrangement(metas, api.KeySyntax)
	for _, syn := range parser.GetSyntaxes() {
		if _, found := result[syn]; !found {
			delete(result, syn)
		}
	}
	return result, nil
}

// -------- List roles -------------------------------------------------------

// ListRolesPort is the interface used by this use case.
type ListRolesPort interface {
	// SelectMeta returns all zettel metadata that match the selection criteria.
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// ListRoles is the data for this use case.
type ListRoles struct {
	port ListRolesPort
}

// NewListRoles creates a new use case.
func NewListRoles(port ListRolesPort) ListRoles {
	return ListRoles{port: port}
}

// Run executes the use case.
func (uc ListRoles) Run(ctx context.Context) (meta.Arrangement, error) {
	q := query.Parse(api.KeyRole + api.ExistOperator) // We look for all metadata with an existing role key
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), q)
	metas, err := uc.port.SelectMeta(box.NoEnrichContext(ctx), nil, q)
	if err != nil {
		return nil, err
	}
	return meta.CreateArrangement(metas, api.KeyRole), nil
}

Deleted usecase/order.go.

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

import (
	"context"

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

// ZettelOrderPort is the interface used by this use case.
type ZettelOrderPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
}

// ZettelOrder is the data for this use case.
type ZettelOrder struct {
	port     ZettelOrderPort
	evaluate Evaluate
}

// NewZettelOrder creates a new use case.
func NewZettelOrder(port ZettelOrderPort, evaluate Evaluate) ZettelOrder {
	return ZettelOrder{port: port, evaluate: evaluate}
}

// Run executes the use case.
func (uc ZettelOrder) Run(ctx context.Context, zid id.Zid, syntax string) (
	start *meta.Meta, result []*meta.Meta, err error,
) {
	zn, err := uc.evaluate.Run(ctx, zid, syntax)
	if err != nil {
		return nil, nil, err
	}
	for _, ref := range collect.Order(zn) {
		if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
			if m, err3 := uc.port.GetMeta(ctx, collectedZid); err3 == nil {
				result = append(result, m)
			}
		}
	}
	return zn.Meta, result, nil
}

Changes to usecase/parse_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
1

2
3
4
5
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
// 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 usecase

import (
	"context"

	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/zettel/id"
)

// ParseZettel is the data for this use case.
type ParseZettel struct {
	rtConfig  config.Config
	getZettel GetZettel
}

Added usecase/query.go.























































































































































































































































































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 usecase

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"t73f.de/r/zsc/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// QueryPort is the interface used by this use case.
type QueryPort interface {
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	SelectMeta(ctx context.Context, metaSeq []*meta.Meta, q *query.Query) ([]*meta.Meta, error)
}

// Query is the data for this use case.
type Query struct {
	port       QueryPort
	ucEvaluate Evaluate
}

// NewQuery creates a new use case.
func NewQuery(port QueryPort) Query {
	return Query{port: port}
}

// SetEvaluate sets the usecase Evaluate, because of circular dependencies.
func (uc *Query) SetEvaluate(ucEvaluate *Evaluate) { uc.ucEvaluate = *ucEvaluate }

// Run executes the use case.
func (uc *Query) Run(ctx context.Context, q *query.Query) ([]*meta.Meta, error) {
	zids := q.GetZids()
	if zids == nil {
		return uc.port.SelectMeta(ctx, nil, q)
	}
	if len(zids) == 0 {
		return nil, nil
	}
	metaSeq, err := uc.getMetaZid(ctx, zids)
	if err != nil {
		return metaSeq, err
	}
	if metaSeq = uc.processDirectives(ctx, metaSeq, q.GetDirectives()); len(metaSeq) > 0 {
		return uc.port.SelectMeta(ctx, metaSeq, q)
	}
	return nil, nil
}

func (uc *Query) getMetaZid(ctx context.Context, zids []id.Zid) ([]*meta.Meta, error) {
	metaSeq := make([]*meta.Meta, 0, len(zids))
	for _, zid := range zids {
		m, err := uc.port.GetMeta(ctx, zid)
		if err == nil {
			metaSeq = append(metaSeq, m)
			continue
		}
		if errors.Is(err, &box.ErrNotAllowed{}) {
			continue
		}
		return metaSeq, err
	}
	return metaSeq, nil
}

func (uc *Query) processDirectives(ctx context.Context, metaSeq []*meta.Meta, directives []query.Directive) []*meta.Meta {
	if len(directives) == 0 {
		return metaSeq
	}
	for _, dir := range directives {
		if len(metaSeq) == 0 {
			return nil
		}
		switch ds := dir.(type) {
		case *query.ContextSpec:
			metaSeq = uc.processContextDirective(ctx, ds, metaSeq)
		case *query.IdentSpec:
			// Nothing to do.
		case *query.ItemsSpec:
			metaSeq = uc.processItemsDirective(ctx, ds, metaSeq)
		case *query.UnlinkedSpec:
			metaSeq = uc.processUnlinkedDirective(ctx, ds, metaSeq)
		default:
			panic(fmt.Sprintf("Unknown directive %T", ds))
		}
	}
	if len(metaSeq) == 0 {
		return nil
	}
	return metaSeq
}
func (uc *Query) processContextDirective(ctx context.Context, spec *query.ContextSpec, metaSeq []*meta.Meta) []*meta.Meta {
	return spec.Execute(ctx, metaSeq, uc.port)
}

func (uc *Query) processItemsDirective(ctx context.Context, _ *query.ItemsSpec, metaSeq []*meta.Meta) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(metaSeq))
	for _, m := range metaSeq {
		zn, err := uc.ucEvaluate.Run(ctx, m.Zid, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		if err != nil {
			continue
		}
		for _, ref := range collect.Order(zn) {
			if collectedZid, err2 := id.Parse(ref.URL.Path); err2 == nil {
				if z, err3 := uc.port.GetZettel(ctx, collectedZid); err3 == nil {
					result = append(result, z.Meta)
				}
			}
		}
	}
	return result
}

func (uc *Query) processUnlinkedDirective(ctx context.Context, spec *query.UnlinkedSpec, metaSeq []*meta.Meta) []*meta.Meta {
	words := spec.GetWords(metaSeq)
	if len(words) == 0 {
		return metaSeq
	}
	var sb strings.Builder
	for _, word := range words {
		sb.WriteString(" :")
		sb.WriteString(word)
	}
	q := (*query.Query)(nil).Parse(sb.String())
	candidates, err := uc.port.SelectMeta(ctx, nil, q)
	if err != nil {
		return nil
	}
	metaZids := id.NewSetCap(len(metaSeq))
	refZids := id.NewSetCap(len(metaSeq) * 4) // Assumption: there are four zids per zettel
	for _, m := range metaSeq {
		metaZids.Add(m.Zid)
		refZids.Add(m.Zid)
		for _, pair := range m.ComputedPairsRest() {
			switch meta.Type(pair.Key) {
			case meta.TypeID:
				if zid, errParse := id.Parse(pair.Value); errParse == nil {
					refZids.Add(zid)
				}
			case meta.TypeIDSet:
				for _, value := range meta.ListFromValue(pair.Value) {
					if zid, errParse := id.Parse(value); errParse == nil {
						refZids.Add(zid)
					}
				}
			}
		}
	}
	candidates = filterByZid(candidates, refZids)
	return uc.filterCandidates(ctx, candidates, words)
}

func filterByZid(candidates []*meta.Meta, ignoreSeq *id.Set) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
	for _, m := range candidates {
		if !ignoreSeq.Contains(m.Zid) {
			result = append(result, m)
		}
	}
	return result
}

func (uc *Query) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
candLoop:
	for _, cand := range candidates {
		zettel, err := uc.port.GetZettel(ctx, cand.Zid)
		if err != nil {
			continue
		}
		v := unlinkedVisitor{
			words: words,
			found: false,
		}
		v.text = v.joinWords(words)

		for _, pair := range zettel.Meta.Pairs() {
			if meta.Type(pair.Key) != meta.TypeZettelmarkup {
				continue
			}
			is := uc.ucEvaluate.RunMetadata(ctx, pair.Value)
			ast.Walk(&v, &is)
			if v.found {
				result = append(result, cand)
				continue candLoop
			}
		}

		syntax := zettel.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax)
		if !parser.IsASTParser(syntax) {
			continue
		}
		zn := uc.ucEvaluate.RunZettel(ctx, zettel, syntax)
		ast.Walk(&v, &zn.Ast)
		if v.found {
			result = append(result, cand)
		}
	}
	return result
}

func (*unlinkedVisitor) joinWords(words []string) string {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

type unlinkedVisitor struct {
	words []string
	text  string
	found bool
}

func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		v.checkWords(n)
		return nil
	case *ast.HeadingNode:
		return nil
	case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode:
		return nil
	}
	return v
}

func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) {
	if len(*is) < 2*len(v.words)-1 {
		return
	}
	for _, text := range v.splitInlineTextList(is) {
		if strings.Contains(text, v.text) {
			v.found = true
		}
	}
}

func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, strfun.MakeWords(n.Text)...)
		default:
			if curList != nil {
				result = append(result, v.joinWords(curList))
				curList = nil
			}
		}
	}
	if curList != nil {
		result = append(result, v.joinWords(curList))
	}
	return result
}

Changes to usecase/refresh.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 usecase

import (
	"context"

Added usecase/reindex.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) 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 usecase

import (
	"context"

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

// ReIndexPort is the interface used by this use case.
type ReIndexPort interface {
	ReIndex(context.Context, id.Zid) error
}

// ReIndex is the data for this use case.
type ReIndex struct {
	log  *logger.Logger
	port ReIndexPort
}

// NewReIndex creates a new use case.
func NewReIndex(log *logger.Logger, port ReIndexPort) ReIndex {
	return ReIndex{log: log, port: port}
}

// Run executes the use case.
func (uc *ReIndex) Run(ctx context.Context, zid id.Zid) error {
	err := uc.port.ReIndex(ctx, zid)
	uc.log.Info().User(ctx).Err(err).Zid(zid).Msg("ReIndex zettel")
	return err
}

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

































































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

import (
	"context"

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

// RenameZettelPort is the interface used by this use case.
type RenameZettelPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)

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

// RenameZettel is the data for this use case.
type RenameZettel struct {
	log  *logger.Logger
	port RenameZettelPort
}

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

func (err *ErrZidInUse) Error() string {
	return "Zettel id already in use: " + err.Zid.String()
}

// NewRenameZettel creates a new use case.
func NewRenameZettel(log *logger.Logger, port RenameZettelPort) RenameZettel {
	return RenameZettel{log: log, port: port}
}

// Run executes the use case.
func (uc *RenameZettel) Run(ctx context.Context, curZid, newZid id.Zid) error {
	noEnrichCtx := box.NoEnrichContext(ctx)
	if _, err := uc.port.GetMeta(noEnrichCtx, curZid); err != nil {
		return err
	}
	if newZid == curZid {
		// Nothing to do
		return nil
	}
	if _, err := uc.port.GetMeta(noEnrichCtx, newZid); err == nil {
		return &ErrZidInUse{Zid: newZid}
	}
	err := uc.port.RenameZettel(ctx, curZid, newZid)
	uc.log.Info().User(ctx).Zid(curZid).Err(err).Zid(newZid).Msg("Rename zettel")
	return err
}

Deleted usecase/unlinked_refs.go.

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





















































































































































































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

import (
	"context"
	"strings"
	"unicode"

	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
)

// UnlinkedReferencesPort is the interface used by this use case.
type UnlinkedReferencesPort interface {
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	SelectMeta(ctx context.Context, q *query.Query) ([]*meta.Meta, error)
}

// UnlinkedReferences is the data for this use case.
type UnlinkedReferences struct {
	port     UnlinkedReferencesPort
	rtConfig config.Config
	encText  *textenc.Encoder
}

// NewUnlinkedReferences creates a new use case.
func NewUnlinkedReferences(port UnlinkedReferencesPort, rtConfig config.Config) UnlinkedReferences {
	return UnlinkedReferences{
		port:     port,
		rtConfig: rtConfig,
		encText:  textenc.Create(),
	}
}

// Run executes the usecase with already evaluated title value.
func (uc *UnlinkedReferences) Run(ctx context.Context, phrase string, q *query.Query) ([]*meta.Meta, error) {
	words := makeWords(phrase)
	if len(words) == 0 {
		return nil, nil
	}
	var sb strings.Builder
	for _, word := range words {
		sb.WriteString(" :")
		sb.WriteString(word)
	}
	q = q.Parse(sb.String())

	// Limit applies to the filtering process, not to SelectMeta
	limit := q.GetLimit()
	q = q.SetLimit(0)

	candidates, err := uc.port.SelectMeta(ctx, q)
	if err != nil {
		return nil, err
	}
	q = q.SetLimit(limit) // Restore limit
	return q.Limit(uc.filterCandidates(ctx, candidates, words)), nil
}

func makeWords(text string) []string {
	return strings.FieldsFunc(text, func(r rune) bool {
		return unicode.In(r, unicode.C, unicode.P, unicode.Z)
	})
}

func (uc *UnlinkedReferences) filterCandidates(ctx context.Context, candidates []*meta.Meta, words []string) []*meta.Meta {
	result := make([]*meta.Meta, 0, len(candidates))
candLoop:
	for _, cand := range candidates {
		zettel, err := uc.port.GetZettel(ctx, cand.Zid)
		if err != nil {
			continue
		}
		v := unlinkedVisitor{
			words: words,
			found: false,
		}
		v.text = v.joinWords(words)

		for _, pair := range zettel.Meta.Pairs() {
			if meta.Type(pair.Key) != meta.TypeZettelmarkup {
				continue
			}
			is := parser.ParseMetadata(pair.Value)
			evaluator.EvaluateInline(ctx, uc.port, uc.rtConfig, &is)
			ast.Walk(&v, &is)
			if v.found {
				result = append(result, cand)
				continue candLoop
			}
		}

		syntax := zettel.Meta.GetDefault(api.KeySyntax, "")
		if !parser.IsASTParser(syntax) {
			continue
		}
		zn, err := parser.ParseZettel(ctx, zettel, syntax, uc.rtConfig), nil
		if err != nil {
			continue
		}
		evaluator.EvaluateZettel(ctx, uc.port, uc.rtConfig, zn)
		ast.Walk(&v, &zn.Ast)
		if v.found {
			result = append(result, cand)
		}
	}
	return result
}

func (*unlinkedVisitor) joinWords(words []string) string {
	return " " + strings.ToLower(strings.Join(words, " ")) + " "
}

type unlinkedVisitor struct {
	words []string
	text  string
	found bool
}

func (v *unlinkedVisitor) Visit(node ast.Node) ast.Visitor {
	switch n := node.(type) {
	case *ast.InlineSlice:
		v.checkWords(n)
		return nil
	case *ast.HeadingNode:
		return nil
	case *ast.LinkNode, *ast.EmbedRefNode, *ast.EmbedBLOBNode, *ast.CiteNode:
		return nil
	}
	return v
}

func (v *unlinkedVisitor) checkWords(is *ast.InlineSlice) {
	if len(*is) < 2*len(v.words)-1 {
		return
	}
	for _, text := range v.splitInlineTextList(is) {
		if strings.Contains(text, v.text) {
			v.found = true
		}
	}
}

func (v *unlinkedVisitor) splitInlineTextList(is *ast.InlineSlice) []string {
	var result []string
	var curList []string
	for _, in := range *is {
		switch n := in.(type) {
		case *ast.TextNode:
			curList = append(curList, makeWords(n.Text)...)
		case *ast.SpaceNode:
		default:
			if curList != nil {
				result = append(result, v.joinWords(curList))
				curList = nil
			}
		}
	}
	if curList != nil {
		result = append(result, v.joinWords(curList))
	}
	return result
}

Changes to usecase/update_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20




21
22
23
24
25
26
27
28
29

30
31
32

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
55

-
+






+
+
+







-
+

-
-
-
-
+
+
+
+





-
+


-
+














-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 usecase

import (
	"context"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// UpdateZettelPort is the interface used by this use case.
type UpdateZettelPort interface {
	// GetZettel retrieves a specific zettel.
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)

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

// UpdateZettel is the data for this use case.
type UpdateZettel struct {
	log  *logger.Logger
	port UpdateZettelPort
}

// NewUpdateZettel creates a new use case.
func NewUpdateZettel(log *logger.Logger, port UpdateZettelPort) UpdateZettel {
	return UpdateZettel{log: log, port: port}
}

// Run executes the use case.
func (uc *UpdateZettel) Run(ctx context.Context, zettel domain.Zettel, hasContent bool) error {
func (uc *UpdateZettel) Run(ctx context.Context, zettel zettel.Zettel, hasContent bool) error {
	m := zettel.Meta
	oldZettel, err := uc.port.GetZettel(box.NoEnrichContext(ctx), m.Zid)
	if err != nil {
		return err
	}
	if zettel.Equal(oldZettel, false) {
		return nil
66
67
68
69
70
71
72
73

74
75
69
70
71
72
73
74
75

76
77
78







-
+


	}

	if !hasContent {
		zettel.Content = oldZettel.Content
	}
	zettel.Content.TrimSpace()
	err = uc.port.UpdateZettel(ctx, zettel)
	uc.log.Sense().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
	uc.log.Info().User(ctx).Zid(m.Zid).Err(err).Msg("Update zettel")
	return err
}

Changes to usecase/usecase.go.

1
2

3
4
5
6
7
8



9
10
11
12
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15

-
+






+
+
+




//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 usecase provides (business) use cases for the zettelstore.
package usecase

Changes to usecase/version.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 usecase

import (
	"regexp"
	"strconv"

Added web/adapter/adapter.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 adapter provides handlers for web requests, and some helper tools.
package adapter

import (
	"context"

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

// TryReIndex executes a re-index if the appropriate query action is given.
func TryReIndex(ctx context.Context, actions []string, metaSeq []*meta.Meta, reIndex *usecase.ReIndex) ([]string, error) {
	if lenActions := len(actions); lenActions > 0 {
		tempActions := make([]string, 0, lenActions)
		hasReIndex := false
		for _, act := range actions {
			if !hasReIndex && act == api.ReIndexAction {
				hasReIndex = true
				var errAction error
				for _, m := range metaSeq {
					if err := reIndex.Run(ctx, m.Zid); err != nil {
						errAction = err
					}
				}
				if errAction != nil {
					return nil, errAction
				}
				continue
			}
			tempActions = append(tempActions, act)
		}
		return tempActions, nil
	}
	return nil, nil
}

Changes to web/adapter/api/api.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
1

2
3
4
5
6
7
8
9
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
// 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 api provides api handlers for web requests.
package api

import (
	"bytes"
	"context"
	"net/http"
	"time"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

// API holds all data and methods for delivering API call results.
type API struct {
	log      *logger.Logger
	b        server.Builder
	authz    auth.AuthzManager
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
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







-
-
-








-
+





-
+








-
+














-
-
-








		policy:   pol,

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeAPI).(time.Duration),
	}
	return a
}

// GetURLPrefix returns the configured URL prefix of the web server.
func (a *API) GetURLPrefix() string { return a.b.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (a *API) NewURLBuilder(key byte) *api.URLBuilder { return a.b.NewURLBuilder(key) }

func (a *API) getAuthData(ctx context.Context) *server.AuthData {
	return server.GetAuthData(ctx)
}
func (a *API) withAuth() bool { return a.authz.WithAuth() }
func (a *API) getToken(ident *meta.Meta) ([]byte, error) {
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindJSON)
	return a.token.GetToken(ident, a.tokenLifetime, auth.KindAPI)
}

func (a *API) reportUsecaseError(w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		a.log.IfErr(err).Msg(text)
		a.log.Error().Err(err).Msg(text)
		http.Error(w, http.StatusText(code), code)
		return
	}
	// TODO: must call PrepareHeader somehow
	http.Error(w, text, code)
}

func writeBuffer(w http.ResponseWriter, buf *bytes.Buffer, contentType string) error {
	return adapter.WriteBytes(w, buf.Bytes(), contentType)
	return adapter.WriteData(w, buf.Bytes(), contentType)
}

func (a *API) getRights(ctx context.Context, m *meta.Meta) (result api.ZettelRights) {
	pol := a.policy
	user := server.GetUser(ctx)
	if pol.CanCreate(user, m) {
		result |= api.ZettelCanCreate
	}
	if pol.CanRead(user, m) {
		result |= api.ZettelCanRead
	}
	if pol.CanWrite(user, m, m) {
		result |= api.ZettelCanWrite
	}
	if pol.CanRename(user, m) {
		result |= api.ZettelCanRename
	}
	if pol.CanDelete(user, m) {
		result |= api.ZettelCanDelete
	}
	if result == 0 {
		return api.ZettelCanNone
	}
	return result
}

Changes to web/adapter/api/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
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
1

2
3
4
5
6
7
8
9
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) 2021-2022 Detlef Stern
// 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 api

import (
	"context"
	"net/http"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
)

// MakePostCommandHandler creates a new HTTP handler to execute certain commands.
func (a *API) MakePostCommandHandler(
	ucIsAuth *usecase.IsAuthenticated,
	ucRefresh *usecase.Refresh,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		switch api.Command(r.URL.Query().Get(api.QueryKeyCommand)) {
		case api.CommandAuthenticated:
			handleIsAuthenticated(ctx, w, ucIsAuth)
			return
		case api.CommandRefresh:
			err := ucRefresh.Run(ctx)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			w.WriteHeader(http.StatusNoContent)
			return
		}
		http.Error(w, "Unknown command", http.StatusBadRequest)
	}
	})
}

func handleIsAuthenticated(ctx context.Context, w http.ResponseWriter, ucIsAuth *usecase.IsAuthenticated) {
	switch ucIsAuth.Run(ctx) {
	case usecase.IsAuthenticatedDisabled:
		w.WriteHeader(http.StatusOK)
	case usecase.IsAuthenticatedAndValid:

Changes to web/adapter/api/create_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
1

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

-
+






+
+
+





-


-
-
+
+
-



+
+




-
-
+
+


-
+




-
-
+
+


















-
+




-
+
-
-
-
-
-
-
-
-
-
+
+
-







-
-
-
+
+
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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 api

import (
	"bytes"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := r.URL.Query()
		enc, encStr := getEncoding(r, q)
		var zettel domain.Zettel
		var zettel zettel.Zettel
		var err error
		switch enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, id.Invalid)
		case api.EncoderJson:
			zettel, err = buildZettelFromJSONData(r, id.Invalid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, id.Invalid)
		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}

		ctx := r.Context()
		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		var result []byte
		var contentType string
		location := a.NewURLBuilder('z').SetZid(api.ZettelID(newZid.String()))
		location := a.NewURLBuilder('z').SetZid(newZid.ZettelID())
		switch enc {
		case api.EncoderPlain:
			result = newZid.Bytes()
			contentType = content.PlainText
		case api.EncoderJson:
		case api.EncoderData:
			var buf bytes.Buffer
			err = encodeJSONData(&buf, api.ZidJSON{ID: api.ZettelID(newZid.String())})
			if err != nil {
				a.log.Fatal().Err(err).Zid(newZid).Msg("Unable to store new Zid in buffer")
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				return
			}
			result = buf.Bytes()
			contentType = content.JSON
			result = []byte(sx.Int64(newZid).String())
			contentType = content.SXPF
			location.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
		default:
			panic(encStr)
		}

		h := adapter.PrepareHeader(w, contentType)
		h.Set(api.HeaderLocation, location.String())
		w.WriteHeader(http.StatusCreated)
		_, err = w.Write(result)
		a.log.IfErr(err).Zid(newZid).Msg("Create Zettel")
	}
		if _, err = w.Write(result); err != nil {
			a.log.Error().Err(err).Zid(newZid).Msg("Create Zettel")
		}
	})
}

Changes to web/adapter/api/delete_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
1

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

import (
	"net/http"

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

// MakeDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
	})
}

Changes to web/adapter/api/get_data.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
1

2
3
4
5
6
7
8
9
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) 2022 Detlef Stern
// 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 api

import (
	"bytes"
	"net/http"

	"zettelstore.de/c/api"
	"t73f.de/r/sx"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

// MakeGetDataHandler creates a new HTTP handler to return zettelstore data.
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeGetDataHandler(ucVersion usecase.Version) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		version := ucVersion.Run()
		result := api.VersionJSON{
			Major: version.Major,
			Minor: version.Minor,
			Patch: version.Patch,
			Info:  version.Info,
			Hash:  version.Hash,
		err := a.writeObject(w, id.Invalid, sx.MakeList(
			sx.Int64(version.Major),
			sx.Int64(version.Minor),
			sx.Int64(version.Patch),
			sx.MakeString(version.Info),
			sx.MakeString(version.Hash),
		}
		var buf bytes.Buffer
		err := encodeJSONData(&buf, result)
		))
		if err != nil {
			a.log.Fatal().Err(err).Msg("Unable to version info in buffer")
			a.log.Error().Err(err).Msg("Write Version Info")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		err = writeBuffer(w, &buf, content.JSON)
		a.log.IfErr(err).Msg("Write Version Info")
	}
	})
}

Deleted web/adapter/api/get_order.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41









































-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 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 api provides api handlers for web requests.
package api

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
)

// MakeGetOrderHandler creates a new API handler to return zettel references
// of a given zettel.
func (a *API) MakeGetOrderHandler(zettelOrder usecase.ZettelOrder) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		ctx := r.Context()
		q := r.URL.Query()
		start, metas, err := zettelOrder.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		err = a.writeMetaList(ctx, w, start, metas)
		a.log.IfErr(err).Zid(zid).Msg("Write Zettel Order")
	}
}

Deleted web/adapter/api/get_unlinked_refs.go.

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


























































































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

import (
	"bytes"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
)

// MakeListUnlinkedMetaHandler creates a new HTTP handler for the use case "list unlinked references".
func (a *API) MakeListUnlinkedMetaHandler(
	getMeta usecase.GetMeta,
	unlinkedRefs usecase.UnlinkedReferences,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		ctx := r.Context()
		zm, err := getMeta.Run(ctx, zid)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		que := r.URL.Query()
		phrase := que.Get(api.QueryKeyPhrase)
		if phrase == "" {
			if zmkTitle, found := zm.Get(api.KeyTitle); found {
				isTitle := evaluate.RunMetadata(ctx, zmkTitle)
				encdr := textenc.Create()
				var b strings.Builder
				_, err = encdr.WriteInlines(&b, &isTitle)
				if err == nil {
					phrase = b.String()
				}
			}
		}

		metaList, err := unlinkedRefs.Run(ctx, phrase, adapter.AddUnlinkedRefsToQuery(adapter.GetQuery(que), zm))
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		result := api.ZidMetaRelatedList{
			ID:     api.ZettelID(zid.String()),
			Meta:   zm.Map(),
			Rights: a.getRights(ctx, zm),
			List:   make([]api.ZidMetaJSON, 0, len(metaList)),
		}
		for _, m := range metaList {
			result.List = append(result.List, api.ZidMetaJSON{
				ID:     api.ZettelID(m.Zid.String()),
				Meta:   m.Map(),
				Rights: a.getRights(ctx, m),
			})
		}

		var buf bytes.Buffer
		err = encodeJSONData(&buf, result)
		if err != nil {
			a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store unlinked references in buffer")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		err = writeBuffer(w, &buf, content.JSON)
		a.log.IfErr(err).Zid(zid).Msg("Write Unlinked References")
	}
}

Changes to web/adapter/api/get_zettel.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16
17
18

19


20
21
22
23
24
25
26
27


28
29
30
31
32






33
34
35
36
37
38
39
40
41
42
43
44

45
46
47


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

66
67

68
69
70

71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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




1

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

-
+






+
+
+










+
-
+
+


-





+
+



-
-
+
+
+
+
+
+











-
+

-
-
+
+

















-
+

-
+


-
+


-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-


-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-

-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
+
-
-
-
-
+
+
+
+
+

-
-
-
-
+
+
+
+
+

+




-
+
+
+
+
+















-
+








-
-
-
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 api

import (
	"bytes"
	"context"
	"fmt"
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetZettelHandler creates a new HTTP handler to return a zettel in various encodings.
func (a *API) MakeGetZettelHandler(getMeta usecase.GetMeta, getZettel usecase.GetZettel, parseZettel usecase.ParseZettel, evaluate usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeGetZettelHandler(
	getZettel usecase.GetZettel,
	parseZettel usecase.ParseZettel,
	evaluate usecase.Evaluate,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		part := getPart(q, partContent)
		ctx := r.Context()
		switch enc, encStr := getEncoding(r, q); enc {
		case api.EncoderPlain:
			a.writePlainData(w, ctx, zid, part, getMeta, getZettel)
			a.writePlainData(ctx, w, zid, part, getZettel)

		case api.EncoderJson:
			a.writeJSONData(w, ctx, zid, part, getMeta, getZettel)
		case api.EncoderData:
			a.writeSzData(ctx, w, zid, part, getZettel)

		default:
			var zn *ast.ZettelNode
			var em func(value string) ast.InlineSlice
			if q.Has(api.QueryKeyParseOnly) {
				zn, err = parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
				em = parser.ParseMetadata
			} else {
				zn, err = evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
				em = func(value string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, value)
				}
			}
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
			a.writeEncodedZettelPart(w, zn, em, enc, encStr, part)
			a.writeEncodedZettelPart(ctx, w, zn, em, enc, encStr, part)
		}
	}
	})
}

func (a *API) writePlainData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) {
func (a *API) writePlainData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var contentType string
	var err error

	switch part {
	case partZettel:
		z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		_, err2 = z.Meta.Write(&buf)
		if err2 == nil {
			err2 = buf.WriteByte('\n')
		}
		if err2 == nil {
			_, err = z.Content.Write(&buf)
		}

	case partMeta:
		m, err2 := getMeta.Run(box.NoEnrichContext(ctx), zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		contentType = content.PlainText
		_, err = m.Write(&buf)

	case partContent:
		z, err2 := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, ""))
		_, err = z.Content.Write(&buf)
	}

	if err != nil {
		a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	err = writeBuffer(w, &buf, contentType)
	a.log.IfErr(err).Zid(zid).Msg("Write Plain data")
}

func (a *API) writeJSONData(w http.ResponseWriter, ctx context.Context, zid id.Zid, part partType, getMeta usecase.GetMeta, getZettel usecase.GetZettel) {
	var buf bytes.Buffer
	var err error

	switch part {
	case partZettel:
		z, err2 := getZettel.Run(ctx, zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		zContent, encoding := z.Content.Encode()
	z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
	}

		err = encodeJSONData(&buf, api.ZettelJSON{
			ID:       api.ZettelID(zid.String()),
			Meta:     z.Meta.Map(),
			Encoding: encoding,
			Content:  zContent,
			Rights:   a.getRights(ctx, z.Meta),
	switch part {
	case partZettel:
		_, err = z.Meta.Write(&buf)
		if err == nil {
			err = buf.WriteByte('\n')
		}
		if err == nil {
			_, err = z.Content.Write(&buf)
		}

	case partMeta:
		contentType = content.PlainText
		_, err = z.Meta.Write(&buf)
		})

	case partMeta:
		m, err2 := getMeta.Run(ctx, zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		err = encodeJSONData(&buf, api.MetaJSON{
	case partContent:
		contentType = content.MIMEFromSyntax(z.Meta.GetDefault(api.KeySyntax, meta.DefaultSyntax))
		_, err = z.Content.Write(&buf)
	}

	if err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("Unable to store plain zettel/part in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if err = writeBuffer(w, &buf, contentType); err != nil {
			Meta:   m.Map(),
			Rights: a.getRights(ctx, m),
		})

	case partContent:
		z, err2 := getZettel.Run(ctx, zid)
		if err2 != nil {
			a.reportUsecaseError(w, err2)
			return
		}
		zContent, encoding := z.Content.Encode()
		err = encodeJSONData(&buf, api.ZettelContentJSON{
			Encoding: encoding,
		a.log.Error().Err(err).Zid(zid).Msg("Write Plain data")
	}
}

func (a *API) writeSzData(ctx context.Context, w http.ResponseWriter, zid id.Zid, part partType, getZettel usecase.GetZettel) {
	z, err := getZettel.Run(ctx, zid)
	if err != nil {
		a.reportUsecaseError(w, err)
		return
	}
	var obj sx.Object
	switch part {
	case partZettel:
		zContent, zEncoding := z.Content.Encode()
		obj = sexp.EncodeZettel(api.ZettelData{
			Meta:     z.Meta.Map(),
			Rights:   a.getRights(ctx, z.Meta),
			Encoding: zEncoding,
			Content:  zContent,
		})
	}

	if err != nil {
		a.log.Fatal().Err(err).Zid(zid).Msg("Unable to store zettel in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	case partMeta:
		obj = sexp.EncodeMetaRights(api.MetaRights{
			Meta:   z.Meta.Map(),
			Rights: a.getRights(ctx, z.Meta),
		})
	}
	err = writeBuffer(w, &buf, content.JSON)
	a.log.IfErr(err).Zid(zid).Msg("Write JSON data")
}

	if err = a.writeObject(w, zid, obj); err != nil {
		a.log.Error().Err(err).Zid(zid).Msg("write sx data")
	}
}

func (a *API) writeEncodedZettelPart(
	ctx context.Context,
	w http.ResponseWriter, zn *ast.ZettelNode,
	evalMeta encoder.EvalMetaFunc,
	enc api.EncodingEnum, encStr string, part partType,
) {
	encdr := encoder.Create(enc)
	encdr := encoder.Create(
		enc,
		&encoder.CreateParameter{
			Lang: a.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang),
		})
	if encdr == nil {
		adapter.BadRequest(w, fmt.Sprintf("Zettel %q not available in encoding %q", zn.Meta.Zid, encStr))
		return
	}
	var err error
	var buf bytes.Buffer
	switch part {
	case partZettel:
		_, err = encdr.WriteZettel(&buf, zn, evalMeta)
	case partMeta:
		_, err = encdr.WriteMeta(&buf, zn.InhMeta, evalMeta)
	case partContent:
		_, err = encdr.WriteContent(&buf, zn)
	}
	if err != nil {
		a.log.Fatal().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer")
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Unable to store data in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if buf.Len() == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}

	err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc))
	a.log.IfErr(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
}
	if err = writeBuffer(w, &buf, content.MIMEFromEncoding(enc)); err != nil {
		a.log.Error().Err(err).Zid(zn.Zid).Msg("Write Encoded Zettel")
	}
}

Deleted web/adapter/api/get_zettel_context.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 api

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
)

// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func (a *API) MakeZettelContextHandler(getContext usecase.ZettelContext) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		q := r.URL.Query()
		dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
		cost, ok := adapter.GetInteger(q, api.QueryKeyCost)
		if !ok || cost < 0 {
			cost = 17
		}
		limit, ok := adapter.GetInteger(q, api.QueryKeyLimit)
		if !ok || limit < 0 {
			limit = 200
		}
		ctx := r.Context()
		metaList, err := getContext.Run(ctx, zid, dir, cost, limit)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		err = a.writeMetaList(ctx, w, metaList[0], metaList[1:])
		a.log.IfErr(err).Zid(zid).Msg("Write Context")
	}
}

Deleted web/adapter/api/json.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









































































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

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"

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

func encodeJSONData(w io.Writer, data interface{}) error {
	enc := json.NewEncoder(w)
	enc.SetEscapeHTML(false)
	return enc.Encode(data)
}

func (a *API) writeMetaList(ctx context.Context, w http.ResponseWriter, m *meta.Meta, metaList []*meta.Meta) error {
	outList := make([]api.ZidMetaJSON, len(metaList))
	for i, m := range metaList {
		outList[i].ID = api.ZettelID(m.Zid.String())
		outList[i].Meta = m.Map()
		outList[i].Rights = a.getRights(ctx, m)
	}

	var buf bytes.Buffer
	err := encodeJSONData(&buf, api.ZidMetaRelatedList{
		ID:     api.ZettelID(m.Zid.String()),
		Meta:   m.Map(),
		Rights: a.getRights(ctx, m),
		List:   outList,
	})
	if err != nil {
		a.log.Fatal().Err(err).Zid(m.Zid).Msg("Unable to store meta list in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return nil
	}

	return writeBuffer(w, &buf, content.JSON)
}

func buildZettelFromJSONData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
	var zettel domain.Zettel
	dec := json.NewDecoder(r.Body)
	var zettelData api.ZettelDataJSON
	if err := dec.Decode(&zettelData); err != nil {
		return zettel, err
	}
	m := meta.New(zid)
	for k, v := range zettelData.Meta {
		m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
	}
	zettel.Meta = m
	if err := zettel.Content.SetDecoded(zettelData.Content, zettelData.Encoding); err != nil {
		return zettel, err
	}
	return zettel, nil
}

Changes to web/adapter/api/login.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
1

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

-
+






+
+
+





-
-



-
+



-
+



-
-
+
+

-
-
+
+
+





-
+











-
-
-
+
+
+
+












-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

-
-
+
+


-
-
+
+
+











-
-
+
+
+









-
-
-
+
+
+
+
+
+
+
+
+
+
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 api

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"

	"zettelstore.de/c/api"
	"t73f.de/r/sx"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
)

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user via API.
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !a.withAuth() {
			err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			a.log.IfErr(err).Msg("Login/free")
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Login/free")
			}
			return
		}
		var token []byte
		if ident, cred := retrieveIdentCred(r); ident != "" {
			var err error
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindJSON)
			token, err = ucAuth.Run(r.Context(), r, ident, cred, a.tokenLifetime, auth.KindAPI)
			if err != nil {
				a.reportUsecaseError(w, err)
				return
			}
		}
		if len(token) == 0 {
			w.Header().Set("WWW-Authenticate", `Bearer realm="Default"`)
			http.Error(w, "Authentication failed", http.StatusUnauthorized)
			return
		}

		err := a.writeJSONToken(w, string(token), a.tokenLifetime)
		a.log.IfErr(err).Msg("Login")
	}
		if err := a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Login")
		}
	})
}

func retrieveIdentCred(r *http.Request) (string, string) {
	if ident, cred, ok := adapter.GetCredentialsViaForm(r); ok {
		return ident, cred
	}
	if ident, cred, ok := r.BasicAuth(); ok {
		return ident, cred
	}
	return "", ""
}

func (a *API) writeJSONToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	var buf bytes.Buffer
	je := json.NewEncoder(&buf)
	err := je.Encode(api.AuthJSON{
		Token:   token,
		Type:    "Bearer",
		Expires: int(lifetime / time.Second),
	})
	if err != nil {
		a.log.Fatal().Err(err).Msg("Unable to store token in buffer")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return nil
	}

	return writeBuffer(w, &buf, content.JSON)
}

// MakeRenewAuthHandler creates a new HTTP handler to renew the authenticate of a user.
func (a *API) MakeRenewAuthHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeRenewAuthHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if !a.withAuth() {
			err := a.writeJSONToken(w, "freeaccess", 24*366*10*time.Hour)
			a.log.IfErr(err).Msg("Refresh/free")
			if err := a.writeToken(w, "freeaccess", 24*366*10*time.Hour); err != nil {
				a.log.Error().Err(err).Msg("Refresh/free")
			}
			return
		}
		authData := a.getAuthData(ctx)
		if authData == nil || len(authData.Token) == 0 || authData.User == nil {
			adapter.BadRequest(w, "Not authenticated")
			return
		}
		totalLifetime := authData.Expires.Sub(authData.Issued)
		currentLifetime := authData.Now.Sub(authData.Issued)
		// If we are in the first quarter of the tokens lifetime, return the token
		if currentLifetime*4 < totalLifetime {
			err := a.writeJSONToken(w, string(authData.Token), totalLifetime-currentLifetime)
			a.log.IfErr(err).Msg("Write old token")
			if err := a.writeToken(w, string(authData.Token), totalLifetime-currentLifetime); err != nil {
				a.log.Error().Err(err).Msg("Write old token")
			}
			return
		}

		// Token is a little bit aged. Create a new one
		token, err := a.getToken(authData.User)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		err = a.writeJSONToken(w, string(token), a.tokenLifetime)
		a.log.IfErr(err).Msg("Write renewed token")
	}
		if err = a.writeToken(w, string(token), a.tokenLifetime); err != nil {
			a.log.Error().Err(err).Msg("Write renewed token")
		}
	})
}

func (a *API) writeToken(w http.ResponseWriter, token string, lifetime time.Duration) error {
	return a.writeObject(w, id.Invalid, sx.MakeList(
		sx.MakeString("Bearer"),
		sx.MakeString(token),
		sx.Int64(int64(lifetime/time.Second)),
	))
}

Changes to web/adapter/api/query.go.

1
2

3
4
5
6
7
8



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

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

-
+






+
+
+









+



+
-
-
+
+




+
+



-
-
+
+
+
+
+
+
+

-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+




+
+
+
+
+
+
+
+
+
+
+
+



-
+




-
-
+
+



-
+







-
+





-
-
-
+
+
+
+

-
-
-
+
+
+


-
+

-
+



-
+

-
+






+
+
+
-
+
-

-
+






+
+
+
+
+
+
+
+
+
+
-
+



-
+




-
+




-
+













-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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 api

import (
	"bytes"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"t73f.de/r/sx"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/meta"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeQueryHandler creates a new HTTP handler to perform a query.
func (a *API) MakeQueryHandler(listMeta usecase.ListMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeQueryHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,
	reIndex *usecase.ReIndex,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		sq := adapter.GetQuery(q)

		metaList, err := listMeta.Run(ctx, sq)
		urlQuery := r.URL.Query()
		if a.handleTagZettel(w, r, tagZettel, urlQuery) || a.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}

		sq := adapter.GetQuery(urlQuery)
		metaSeq, err := queryMeta.Run(ctx, sq)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}

		actions, err := adapter.TryReIndex(ctx, sq.Actions(), metaSeq, reIndex)
		if err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		if len(actions) > 0 {
			if len(metaSeq) > 0 {
				for _, act := range actions {
					if act == api.RedirectAction {
						zid := metaSeq[0].Zid
						ub := a.NewURLBuilder('z').SetZid(zid.ZettelID())
						a.redirectFound(w, r, ub, zid)
						return
					}
				}
			}
		}

		var encoder zettelEncoder
		var contentType string
		switch enc, _ := getEncoding(r, q); enc {
		switch enc, _ := getEncoding(r, urlQuery); enc {
		case api.EncoderPlain:
			encoder = &plainZettelEncoder{}
			contentType = content.PlainText

		case api.EncoderJson:
			encoder = &jsonZettelEncoder{
		case api.EncoderData:
			encoder = &dataZettelEncoder{
				sq:        sq,
				getRights: func(m *meta.Meta) api.ZettelRights { return a.getRights(ctx, m) },
			}
			contentType = content.JSON
			contentType = content.SXPF

		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		var buf bytes.Buffer
		err = queryAction(&buf, encoder, metaList, sq)
		err = queryAction(&buf, encoder, metaSeq, actions)
		if err != nil {
			a.log.Error().Err(err).Str("query", sq.String()).Msg("execute query action")
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}

		err = writeBuffer(w, &buf, contentType)
		a.log.IfErr(err).Msg("write result buffer")
	}
		if err = writeBuffer(w, &buf, contentType); err != nil {
			a.log.Error().Err(err).Msg("write result buffer")
		}
	})
}
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, sq *query.Query) error {
	min, max := -1, -1
	if actions := sq.Actions(); len(actions) > 0 {
func queryAction(w io.Writer, enc zettelEncoder, ml []*meta.Meta, actions []string) error {
	minVal, maxVal := -1, -1
	if len(actions) > 0 {
		acts := make([]string, 0, len(actions))
		for _, act := range actions {
			if strings.HasPrefix(act, "MIN") {
			if strings.HasPrefix(act, api.MinAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					min = num
					minVal = num
					continue
				}
			}
			if strings.HasPrefix(act, "MAX") {
			if strings.HasPrefix(act, api.MaxAction) {
				if num, err := strconv.Atoi(act[3:]); err == nil && num > 0 {
					max = num
					maxVal = num
					continue
				}
			}
			acts = append(acts, act)
		}
		for _, act := range acts {
			if act == api.KeysAction {
				return encodeKeysArrangement(w, enc, ml, act)
			}
			key := strings.ToLower(act)
			switch key := strings.ToLower(act); meta.Type(key) {
			switch meta.Type(key) {
			case meta.TypeWord, meta.TypeTagSet:
				return encodeKeyArrangement(w, enc, ml, key, min, max)
				return encodeMetaKeyArrangement(w, enc, ml, key, minVal, maxVal)
			}
		}
	}
	return enc.writeMetaList(w, ml)
}

func encodeKeysArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, act string) error {
	arr := make(meta.Arrangement, 128)
	for _, m := range ml {
		for k := range m.Map() {
			arr[k] = append(arr[k], m)
		}
	}
	return enc.writeArrangement(w, act, arr)
}

func encodeKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, min, max int) error {
func encodeMetaKeyArrangement(w io.Writer, enc zettelEncoder, ml []*meta.Meta, key string, minVal, maxVal int) error {
	arr0 := meta.CreateArrangement(ml, key)
	arr := make(meta.Arrangement, len(arr0))
	for k0, ml0 := range arr0 {
		if len(ml0) < min || (max > 0 && len(ml0) > max) {
		if len(ml0) < minVal || (maxVal > 0 && len(ml0) > maxVal) {
			continue
		}
		arr[k0] = ml0
	}
	return enc.writeArrangement(w, arr)
	return enc.writeArrangement(w, key, arr)
}

type zettelEncoder interface {
	writeMetaList(w io.Writer, ml []*meta.Meta) error
	writeArrangement(w io.Writer, arr meta.Arrangement) error
	writeArrangement(w io.Writer, act string, arr meta.Arrangement) error
}

type plainZettelEncoder struct{}

func (*plainZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
	for _, m := range ml {
		_, err := fmt.Fprintln(w, m.Zid.String(), m.GetTitle())
		if err != nil {
			return err
		}
	}
	return nil
}
func (*plainZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error {
func (*plainZettelEncoder) writeArrangement(w io.Writer, _ string, arr meta.Arrangement) error {
	for key, ml := range arr {
		_, err := io.WriteString(w, key)
		if err != nil {
			return err
		}
		for i, m := range ml {
			if i == 0 {
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











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







-
+




-
-
-
-
+
+
+
+
+
+
-

-
+

+
+


-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+


-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
		if err != nil {
			return err
		}
	}
	return nil
}

type jsonZettelEncoder struct {
type dataZettelEncoder struct {
	sq        *query.Query
	getRights func(*meta.Meta) api.ZettelRights
}

func (jze *jsonZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
	result := make([]api.ZidMetaJSON, 0, len(ml))
	for _, m := range ml {
		result = append(result, api.ZidMetaJSON{
func (dze *dataZettelEncoder) writeMetaList(w io.Writer, ml []*meta.Meta) error {
	result := make(sx.Vector, len(ml)+1)
	result[0] = sx.SymbolList
	symID, symZettel := sx.MakeSymbol("id"), sx.MakeSymbol("zettel")
	for i, m := range ml {
		msz := sexp.EncodeMetaRights(api.MetaRights{
			ID:     api.ZettelID(m.Zid.String()),
			Meta:   m.Map(),
			Rights: jze.getRights(m),
			Rights: dze.getRights(m),
		})
		msz = sx.Cons(sx.MakeList(symID, sx.Int64(m.Zid)), msz.Cdr()).Cons(symZettel)
		result[i+1] = msz
	}

	err := encodeJSONData(w, api.ZettelListJSON{
		Query: jze.sq.String(),
		Human: jze.sq.Human(),
		List:  result,
	})
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("meta-list"),
		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		sx.MakeList(result...),
	))
	return err
}
func (dze *dataZettelEncoder) writeArrangement(w io.Writer, act string, arr meta.Arrangement) error {
	result := sx.Nil()
	for aggKey, metaList := range arr {
		sxMeta := sx.Nil()
		for i := len(metaList) - 1; i >= 0; i-- {
			sxMeta = sxMeta.Cons(sx.Int64(metaList[i].Zid))
		}
		sxMeta = sxMeta.Cons(sx.MakeString(aggKey))
		result = result.Cons(sxMeta)
	}
	_, err := sx.Print(w, sx.MakeList(
		sx.MakeSymbol("aggregate"),
		sx.MakeString(act),
		sx.MakeList(sx.MakeSymbol("query"), sx.MakeString(dze.sq.String())),
		sx.MakeList(sx.MakeSymbol("human"), sx.MakeString(dze.sq.Human())),
		result.Cons(sx.SymbolList),
	))
	return err
}
func (*jsonZettelEncoder) writeArrangement(w io.Writer, arr meta.Arrangement) error {
	mm := make(api.MapMeta, len(arr))
	for key, metaList := range arr {
		zidList := make([]api.ZettelID, 0, len(metaList))
		for _, m := range metaList {
			zidList = append(zidList, api.ZettelID(m.Zid.String()))

func (a *API) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		a.reportUsecaseError(w, err)
		return true
	}
	zid := z.Meta.Zid
	newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
	for key, slVals := range vals {
		if key == api.QueryKeyTag {
			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}
	}
	a.redirectFound(w, r, newURL, zid)
	return true
}

func (a *API) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool {
	role := vals.Get(api.QueryKeyRole)
	if role == "" {
		return false
	}
	ctx := r.Context()
	z, err := roleZettel.Run(ctx, role)
	if err != nil {
		a.reportUsecaseError(w, err)
		return true
	}
	zid := z.Meta.Zid
	newURL := a.NewURLBuilder('z').SetZid(zid.ZettelID())
	for key, slVals := range vals {
		if key == api.QueryKeyRole {
			continue
		}
		for _, val := range slVals {
			newURL.AppendKVQuery(key, val)
		}
	}
		mm[key] = zidList
	}
	return encodeJSONData(w, api.MapListJSON{Map: mm})
}
	a.redirectFound(w, r, newURL, zid)
	return true
}

func (a *API) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder, zid id.Zid) {
	w.Header().Set(api.HeaderContentType, content.PlainText)
	http.Redirect(w, r, ub.String(), http.StatusFound)
	if _, err := io.WriteString(w, zid.String()); err != nil {
		a.log.Error().Err(err).Msg("redirect body")
	}
}

Deleted web/adapter/api/rename_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



































































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

import (
	"net/http"
	"net/url"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
)

// MakeRenameZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}
		newZid, found := getDestinationZid(r)
		if !found {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		if err = renameZettel.Run(r.Context(), zid, newZid); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
}

func getDestinationZid(r *http.Request) (id.Zid, bool) {
	if values, ok := r.Header[api.HeaderDestination]; ok {
		for _, value := range values {
			if zid, ok2 := getZidFromURL(value); ok2 {
				return zid, true
			}
		}
	}
	return id.Invalid, false
}

func getZidFromURL(val string) (id.Zid, bool) {
	u, err := url.Parse(val)
	if err != nil {
		return id.Invalid, false
	}
	if len(u.Path) < len(api.ZidVersion) {
		return id.Invalid, false
	}
	zid, err := id.Parse(u.Path[len(u.Path)-len(api.ZidVersion):])
	if err != nil {
		return id.Invalid, false
	}
	return zid, true
}

Changes to web/adapter/api/request.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
1

2
3
4
5
6
7
8
9
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-2023 Detlef Stern
// 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 api

import (
	"io"
	"net/http"
	"net/url"

	"t73f.de/r/sx/sxreader"
	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/input"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"t73f.de/r/zsc/sexp"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// getEncoding returns the data encoding selected by the caller.
func getEncoding(r *http.Request, q url.Values) (api.EncodingEnum, string) {
	encoding := q.Get(api.QueryKeyEncoding)
	if encoding != "" {
		return api.Encoder(encoding), encoding
45
46
47
48
49
50
51
52
53

54
55
56
57
58
59
60
50
51
52
53
54
55
56


57
58
59
60
61
62
63
64







-
-
+







			}
		}
	}
	return "", false
}

var mapCT2encoding = map[string]string{
	"application/json": "json",
	"text/html":        api.EncodingHTML,
	"text/html": api.EncodingHTML,
}

func contentType2encoding(contentType string) (string, bool) {
	// TODO: only check before first ';'
	enc, ok := mapCT2encoding[contentType]
	return enc, ok
}
96
97
98
99
100
101
102
103


104
105
106

107
108
109
110

111
112

113
114
115































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







-
+
+


-
+



-
+

-
+

-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
func (p partType) DefString(defPart partType) string {
	if p == defPart {
		return ""
	}
	return p.String()
}

func buildZettelFromPlainData(r *http.Request, zid id.Zid) (domain.Zettel, error) {
func buildZettelFromPlainData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	b, err := io.ReadAll(r.Body)
	if err != nil {
		return domain.Zettel{}, err
		return zettel.Zettel{}, err
	}
	inp := input.NewInput(b)
	m := meta.NewFromInput(zid, inp)
	return domain.Zettel{
	return zettel.Zettel{
		Meta:    m,
		Content: domain.NewContent(inp.Src[inp.Pos:]),
		Content: zettel.NewContent(inp.Src[inp.Pos:]),
	}, nil

}
}

func buildZettelFromData(r *http.Request, zid id.Zid) (zettel.Zettel, error) {
	defer r.Body.Close()
	rdr := sxreader.MakeReader(r.Body)
	obj, err := rdr.Read()
	if err != nil {
		return zettel.Zettel{}, err
	}
	zd, err := sexp.ParseZettel(obj)
	if err != nil {
		return zettel.Zettel{}, err
	}

	m := meta.New(zid)
	for k, v := range zd.Meta {
		if !meta.IsComputed(k) {
			m.Set(meta.RemoveNonGraphic(k), meta.RemoveNonGraphic(v))
		}
	}

	var content zettel.Content
	if err = content.SetDecoded(zd.Content, zd.Encoding); err != nil {
		return zettel.Zettel{}, err
	}

	return zettel.Zettel{
		Meta:    m,
		Content: content,
	}, nil
}

Added web/adapter/api/response.go.








































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 api

import (
	"bytes"
	"net/http"

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

func (a *API) writeObject(w http.ResponseWriter, zid id.Zid, obj sx.Object) error {
	var buf bytes.Buffer
	if _, err := sx.Print(&buf, obj); err != nil {
		msg := a.log.Error().Err(err)
		if msg != nil {
			if zid.IsValid() {
				msg = msg.Zid(zid)
			}
			msg.Msg("Unable to store object in buffer")
		}
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return nil
	}
	return writeBuffer(w, &buf, content.SXPF)
}

Changes to web/adapter/api/update_zettel.go.

1
2

3
4
5
6
7
8



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

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

-
+






+
+
+







-
-
-
-
-
+
+
+
+
+



-
-
+
+







-
+



-
-
+
+














-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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 api

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

// MakeUpdateZettelHandler creates a new HTTP handler to update a zettel.
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (a *API) MakeUpdateZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			http.NotFound(w, r)
			return
		}

		q := r.URL.Query()
		var zettel domain.Zettel
		var zettel zettel.Zettel
		switch enc, _ := getEncoding(r, q); enc {
		case api.EncoderPlain:
			zettel, err = buildZettelFromPlainData(r, zid)
		case api.EncoderJson:
			zettel, err = buildZettelFromJSONData(r, zid)
		case api.EncoderData:
			zettel, err = buildZettelFromData(r, zid)
		default:
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}

		if err != nil {
			a.reportUsecaseError(w, adapter.NewErrBadRequest(err.Error()))
			return
		}
		if err = updateZettel.Run(r.Context(), zettel, true); err != nil {
			a.reportUsecaseError(w, err)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	}
	})
}

Changes to web/adapter/errors.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


1

2
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
// 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 adapter provides handlers for web requests.
package adapter

import "net/http"

// BadRequest signals HTTP status code 400.
func BadRequest(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusBadRequest)
}

// Forbidden signals HTTP status code 403.
func Forbidden(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusForbidden)
}

// NotFound signals HTTP status code 404.
// ErrResourceNotFound is signalled when a web resource was not found.
type ErrResourceNotFound struct{ Path string }
func NotFound(w http.ResponseWriter, text string) {
	http.Error(w, text, http.StatusNotFound)
}

func (ernf ErrResourceNotFound) Error() string { return "resource not found: " + ernf.Path }

Changes to web/adapter/request.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
1

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






































-
+






+
+
+










-
+
-


-


















-
-
-
-
-
-
-
-
-
-
-















-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 adapter

import (
	"net/http"
	"net/url"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/query"
	"zettelstore.de/z/usecase"
)

// GetCredentialsViaForm retrieves the authentication credentions from a form.
func GetCredentialsViaForm(r *http.Request) (ident, cred string, ok bool) {
	err := r.ParseForm()
	if err != nil {
		kernel.Main.GetLogger(kernel.WebService).Info().Err(err).Msg("Unable to parse form")
		return "", "", false
	}

	ident = strings.TrimSpace(r.PostFormValue("username"))
	cred = r.PostFormValue("password")
	if ident == "" {
		return "", "", false
	}
	return ident, cred, true
}

// GetInteger returns the integer value of the named query key.
func GetInteger(q url.Values, key string) (int, bool) {
	s := q.Get(key)
	if s != "" {
		if val, err := strconv.Atoi(s); err == nil {
			return val, true
		}
	}
	return 0, false
}

// GetQuery retrieves the specified options from a query.
func GetQuery(vals url.Values) (result *query.Query) {
	if exprs, found := vals[api.QueryKeyQuery]; found {
		result = query.Parse(strings.Join(exprs, " "))
	}
	if seeds, found := vals[api.QueryKeySeed]; found {
		for _, seed := range seeds {
			if si, err := strconv.ParseInt(seed, 10, 31); err == nil {
				result = result.SetSeed(int(si))
				break
			}
		}
	}
	return result
}

// GetZCDirection returns a direction value for a given string.
func GetZCDirection(s string) usecase.ZettelContextDirection {
	switch s {
	case api.DirBackward:
		return usecase.ZettelContextBackward
	case api.DirForward:
		return usecase.ZettelContextForward
	}
	return usecase.ZettelContextBoth
}

// AddUnlinkedRefsToQuery inspects metadata and enhances the given query to ignore
// some zettel identifier.
func AddUnlinkedRefsToQuery(q *query.Query, m *meta.Meta) *query.Query {
	var sb strings.Builder
	sb.WriteString(api.KeyID)
	sb.WriteString("!:")
	sb.WriteString(m.Zid.String())
	for _, pair := range m.ComputedPairsRest() {
		switch meta.Type(pair.Key) {
		case meta.TypeID:
			sb.WriteByte(' ')
			sb.WriteString(api.KeyID)
			sb.WriteString("!:")
			sb.WriteString(pair.Value)
		case meta.TypeIDSet:
			for _, value := range meta.ListFromValue(pair.Value) {
				sb.WriteByte(' ')
				sb.WriteString(api.KeyID)
				sb.WriteString("!:")
				sb.WriteString(value)
			}
		}
	}
	return q.Parse(sb.String())
}

Changes to web/adapter/response.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16

17
18

19
20
21
22
23
24


25
26
27
28
29
30
31
1

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

import (
	"errors"
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/usecase"
)

// WriteBytes emits the given data to the response writer.
func WriteBytes(w http.ResponseWriter, data []byte, contentType string) error {
// WriteData emits the given data to the response writer.
func WriteData(w http.ResponseWriter, data []byte, contentType string) error {
	if len(data) == 0 {
		w.WriteHeader(http.StatusNoContent)
		return nil
	}
	PrepareHeader(w, contentType)
	w.WriteHeader(http.StatusOK)
	_, err := w.Write(data)
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
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







-
+

-
+



-
-
+
+
+

-
-
+
+
+
+

-
-
+
+
+

-
-
+
+
+

+
+
+
+
-
-
+
+
+










+
+
+
+



// ErrBadRequest is returned if the caller made an invalid HTTP request.
type ErrBadRequest struct {
	Text string
}

// NewErrBadRequest creates an new bad request error.
func NewErrBadRequest(text string) error { return &ErrBadRequest{Text: text} }
func NewErrBadRequest(text string) error { return ErrBadRequest{Text: text} }

func (err *ErrBadRequest) Error() string { return err.Text }
func (err ErrBadRequest) Error() string { return err.Text }

// CodeMessageFromError returns an appropriate HTTP status code and text from a given error.
func CodeMessageFromError(err error) (int, string) {
	if err == box.ErrNotFound {
		return http.StatusNotFound, http.StatusText(http.StatusNotFound)
	var eznf box.ErrZettelNotFound
	if errors.As(err, &eznf) {
		return http.StatusNotFound, "Zettel not found: " + eznf.Zid.String()
	}
	if err1, ok := err.(*box.ErrNotAllowed); ok {
		return http.StatusForbidden, err1.Error()
	var ena *box.ErrNotAllowed
	if errors.As(err, &ena) {
		msg := ena.Error()
		return http.StatusForbidden, strings.ToUpper(msg[:1]) + msg[1:]
	}
	if err1, ok := err.(*box.ErrInvalidID); ok {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", err1.Zid)
	var eiz box.ErrInvalidZid
	if errors.As(err, &eiz) {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q not appropriate in this context", eiz.Zid)
	}
	if err1, ok := err.(*usecase.ErrZidInUse); ok {
		return http.StatusBadRequest, fmt.Sprintf("Zettel-ID %q already in use", err1.Zid)
	var etznf usecase.ErrTagZettelNotFound
	if errors.As(err, &etznf) {
		return http.StatusNotFound, "Tag zettel not found: " + etznf.Tag
	}
	var erznf usecase.ErrRoleZettelNotFound
	if errors.As(err, &erznf) {
		return http.StatusNotFound, "Role zettel not found: " + erznf.Role
	}
	if err1, ok := err.(*ErrBadRequest); ok {
		return http.StatusBadRequest, err1.Text
	var ebr ErrBadRequest
	if errors.As(err, &ebr) {
		return http.StatusBadRequest, ebr.Text
	}
	if errors.Is(err, box.ErrStopped) {
		return http.StatusInternalServerError, fmt.Sprintf("Zettelstore not operational: %v", err)
	}
	if errors.Is(err, box.ErrConflict) {
		return http.StatusConflict, "Zettelstore operations conflicted"
	}
	if errors.Is(err, box.ErrCapacity) {
		return http.StatusInsufficientStorage, "Zettelstore reached one of its storage limits"
	}
	var ernf ErrResourceNotFound
	if errors.As(err, &ernf) {
		return http.StatusNotFound, "Resource not found: " + ernf.Path
	}
	return http.StatusInternalServerError, err.Error()
}

Changes to web/adapter/webui/const.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
1

2
3
4
5
6
7
8
9
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) 2022 Detlef Stern
// 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 webui

// WebUI related constants.

const queryKeyAction = "action"
const queryKeyAction = "_action"

// Values for queryKeyAction
const (
	valueActionCopy    = "copy"
	valueActionFolge   = "folge"
	valueActionNew     = "new"
	valueActionSequel  = "sequel"
	valueActionVersion = "version"
)

// Enumeration for queryKeyAction
type createAction uint8

const (
	actionCopy createAction = iota
	actionFolge
	actionNew
	actionSequel
	actionVersion
)

func getCreateAction(s string) createAction {
var createActionMap = map[string]createAction{
	switch s {
	case valueActionCopy:
		return actionCopy
	case valueActionFolge:
	valueActionSequel:  actionSequel,
	valueActionCopy:    actionCopy,
	valueActionFolge:   actionFolge,
		return actionFolge
	case valueActionNew:
	valueActionNew:     actionNew,
		return actionNew
	case valueActionVersion:
		return actionVersion
	default:
		return actionCopy
	valueActionVersion: actionVersion,
}

func getCreateAction(s string) createAction {
	if action, found := createActionMap[s]; found {
		return action
	}
	return actionCopy
}

Changes to web/adapter/webui/create_zettel.go.

1
2

3
4
5
6
7
8



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

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

-
+






+
+
+








+

+
-
+

-
-
-






+
+
+





-
-
-
+
+
+
+
+
+



+
-
+

-
+




-
+






-
+
-
-

-
+

-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+

-
+




















-
-
+
+





-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
-
-
+
-
-
+
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+


-
-
+
+



















-
+

-
+

-
+





+
-
-
-
-
+
+
+
+
+


-
+




+
-
+














-
+

-
-
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/zmkenc"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetCreateZettelHandler creates a new HTTP handler to display the
// HTML edit view for the various zettel creation methods.
func (wui *WebUI) MakeGetCreateZettelHandler(
	getZettel usecase.GetZettel, createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
	getZettel usecase.GetZettel,
	createZettel *usecase.CreateZettel,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()
		op := getCreateAction(q.Get(queryKeyAction))
		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}
		origZettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrZettelNotFound{Zid: zid})
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		switch op {
		case actionCopy:
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "Copy Zettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareCopy(origZettel), "Copy Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "Versionzettel", "", roleData, syntaxData)
		case actionFolge:
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "Folgezettel", "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareFolge(origZettel), "Folge Zettel", "", roleData, syntaxData)
		case actionNew:
			m := origZettel.Meta
			title := parser.ParseMetadata(m.GetTitle())
			title := parser.NormalizedSpacedText(origZettel.Meta.GetTitle())
			newTitle := parser.NormalizedSpacedText(q.Get(api.KeyTitle))
			textTitle, err2 := encodeInlinesText(&title, wui.gentext)
			if err2 != nil {
				wui.reportError(ctx, w, err2)
				return
			}
			htmlTitle := wui.getSimpleHTMLEncoder().InlinesString(&title)
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel), textTitle, htmlTitle, "", roleData, syntaxData)
			wui.renderZettelForm(ctx, w, createZettel.PrepareNew(origZettel, newTitle), title, "", roleData, syntaxData)
		case actionSequel:
			wui.renderZettelForm(ctx, w, createZettel.PrepareSequel(origZettel), "Sequel Zettel", "", roleData, syntaxData)
		case actionVersion:
			wui.renderZettelForm(ctx, w, createZettel.PrepareVersion(origZettel), "Version Zettel", "", roleData, syntaxData)
		}
	}
	})
}

func retrieveDataLists(ctx context.Context, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) ([]string, []string) {
	roleData := dataListFromArrangement(ucListRoles.Run(ctx))
	syntaxData := dataListFromArrangement(ucListSyntax.Run(ctx))
	return roleData, syntaxData
}

func dataListFromArrangement(ar meta.Arrangement, err error) []string {
	if err == nil {
		l := ar.Counted()
		l.SortByCount()
		return l.Categories()
	}
	return nil
}

func (wui *WebUI) renderZettelForm(
	ctx context.Context,
	w http.ResponseWriter,
	zettel domain.Zettel,
	title, heading string,
	ztl zettel.Zettel,
	title string,
	formActionURL string,
	roleData []string,
	syntaxData []string,
) {
	user := server.GetUser(ctx)
	m := zettel.Meta
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), title, "", user, &base)
	m := ztl.Meta

	var sb strings.Builder
	for _, p := range m.PairsRest() {
		sb.WriteString(p.Key)
		sb.WriteString(": ")
		sb.WriteString(p.Value)
		sb.WriteByte('\n')
	}
	env, rb := wui.createRenderEnv(ctx, "form", wui.getUserLang(ctx), title, user)
	wui.renderTemplate(ctx, w, id.FormTemplateZid, &base, formZettelData{
		Heading:       heading,
		FormActionURL: formActionURL,
	rb.bindString("heading", sx.MakeString(title))
	rb.bindString("form-action-url", sx.MakeString(formActionURL))
		MetaTitle:     m.GetDefault(api.KeyTitle, ""),
		MetaTags:      m.GetDefault(api.KeyTags, ""),
		MetaRole:      m.GetDefault(api.KeyRole, ""),
		HasRoleData:   len(roleData) > 0,
		RoleData:      roleData,
	rb.bindString("role-data", makeStringList(roleData))
		HasSyntaxData: len(syntaxData) > 0,
		SyntaxData:    syntaxData,
	rb.bindString("syntax-data", makeStringList(syntaxData))
		MetaSyntax:    m.GetDefault(api.KeySyntax, ""),
		MetaPairsRest: m.PairsRest(),
		IsTextContent: !zettel.Content.IsBinary(),
		Content:       zettel.Content.AsString(),
	rb.bindString("meta", sx.MakeString(sb.String()))
	if !ztl.Content.IsBinary() {
		rb.bindString("content", sx.MakeString(ztl.Content.AsString()))
	})
}

	}
	wui.bindCommonZettelData(ctx, &rb, user, m, &ztl.Content)
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.FormTemplateZid, env)
	}
	if err := rb.err; err != nil {
		wui.reportError(ctx, w, err)
	}
}

// MakePostCreateZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakePostCreateZettelHandler(createZettel *usecase.CreateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		reEdit, zettel, err := parseZettelForm(r, id.Invalid)
		if err == errMissingContent {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Content is missing"))
			return
		}
		if err != nil {
			const msg = "Unable to read form data"
			wui.log.Info().Err(err).Msg(msg)
			wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
			return
		}

		newZid, err := createZettel.Run(ctx, zettel)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(newZid.String())))
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(newZid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(newZid.ZettelID()))
		}
	}
	})
}

// MakeGetZettelFromListHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeGetZettelFromListHandler(
	queryMeta *usecase.Query,
	listMeta usecase.ListMeta, evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {

	return func(w http.ResponseWriter, r *http.Request) {
	evaluate *usecase.Evaluate,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
		ctx := r.Context()
		metaList, err := listMeta.Run(box.NoEnrichQuery(ctx, q), q)
		metaSeq, err := queryMeta.Run(box.NoEnrichQuery(ctx, q), q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		entries, _ := evaluator.QueryAction(ctx, q, metaSeq)
		bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig))
		bns := evaluate.RunBlockNode(ctx, entries)
		enc := zmkenc.Create()
		var zmkContent bytes.Buffer
		_, err = enc.WriteBlocks(&zmkContent, &bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		m := meta.New(id.Invalid)
		m.Set(api.KeyTitle, q.Human())
		m.Set(api.KeySyntax, api.ValueSyntaxZmk)
		if qval := q.String(); qval != "" {
			m.Set(api.KeyQuery, qval)
		}
		zettel := domain.Zettel{Meta: m, Content: domain.NewContent(zmkContent.Bytes())}
		zettel := zettel.Zettel{Meta: m, Content: zettel.NewContent(zmkContent.Bytes())}
		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", "Zettel From List", wui.createNewURL, roleData, syntaxData)
	}
		wui.renderZettelForm(ctx, w, zettel, "Zettel from list", wui.createNewURL, roleData, syntaxData)
	})
}

Changes to web/adapter/webui/delete_zettel.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15

16
17


18
19
20
21
22
23


24
25
26
27
28
29
30


31
32
33


34

35

36
37

38
39
40
41

42
43
44
45
46

47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70



71
72
73
74



75
76
77
78
79
80





81
82
83
84
85
86
87






88
89
90
91
92
93
94
95
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



















1

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

import (
	"net/http"

	"t73f.de/r/sx"
	"zettelstore.de/c/api"
	"zettelstore.de/c/maps"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/maps"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetDeleteZettelHandler creates a new HTTP handler to display the
// HTML delete view of a zettel.
func (wui *WebUI) MakeGetDeleteZettelHandler(
	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
	getZettel usecase.GetZettel,
	getAllZettel usecase.GetAllZettel,
	evaluate *usecase.Evaluate,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		ms, err := getAllMeta.Run(ctx, zid)
		zs, err := getAllZettel.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		m := ms[0]
		m := zs[0].Meta

		var shadowedBox string
		var incomingLinks simpleLinks
		if len(ms) > 1 {
			shadowedBox = ms[1].GetDefault(api.KeyBoxNumber, "???")
		} else {
			getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
			incomingLinks = wui.encodeIncoming(m, getTextTitle)
		}
		uselessFiles := retrieveUselessFiles(m)

		user := server.GetUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Delete Zettel "+m.Zid.String(), "", user, &base)
		wui.renderTemplate(ctx, w, id.DeleteTemplateZid, &base, struct {
			Zid             string
			MetaPairs       []meta.Pair
			HasShadows      bool
			ShadowedBox     string
			Incoming        simpleLinks
			HasUselessFiles bool
			UselessFiles    []string

		}{
			Zid:             zid.String(),
		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(
			ctx, "delete", wui.getUserLang(ctx), "Delete Zettel "+m.Zid.String(), user)
			MetaPairs:       m.ComputedPairs(),
			HasShadows:      shadowedBox != "",
			ShadowedBox:     shadowedBox,
			Incoming:        incomingLinks,
		if len(zs) > 1 {
			rb.bindString("shadowed-box", sx.MakeString(zs[1].Meta.GetDefault(api.KeyBoxNumber, "???")))
			rb.bindString("incoming", nil)
			HasUselessFiles: len(uselessFiles) > 0,
			UselessFiles:    uselessFiles,
		})
	}
}

		} else {
			rb.bindString("shadowed-box", nil)
			rb.bindString("incoming", wui.encodeIncoming(m, wui.makeGetTextTitle(ctx, getZettel)))
		}
		wui.bindCommonZettelData(ctx, &rb, user, m, nil)
func retrieveUselessFiles(m *meta.Meta) []string {
	if val, found := m.Get(api.KeyUselessFiles); found {
		return []string{val}
	}
	return nil
}


		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.DeleteTemplateZid, env)
		} else {
			err = rb.err
		}
// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
	})
}

func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) simpleLinks {
func (wui *WebUI) encodeIncoming(m *meta.Meta, getTextTitle getTextTitleFunc) *sx.Pair {
	zidMap := make(strfun.Set)
	addListValues(zidMap, m, api.KeyBackward)
	for _, kd := range meta.GetSortedKeyDescriptions() {
		inverseKey := kd.Inverse
		if inverseKey == "" {
			continue
		}
		ikd := meta.GetDescription(inverseKey)
		switch ikd.Type {
		case meta.TypeID:
			if val, ok := m.Get(inverseKey); ok {
				zidMap.Set(val)
			}
		case meta.TypeIDSet:
			addListValues(zidMap, m, inverseKey)
		}
	}
	return createSimpleLinks(wui.encodeZidLinks(maps.Keys(zidMap), getTextTitle))
	return wui.zidLinksSxn(maps.Keys(zidMap), getTextTitle)
}

func addListValues(zidMap strfun.Set, m *meta.Meta, key string) {
	if values, ok := m.GetList(key); ok {
		for _, val := range values {
			zidMap.Set(val)
		}
	}
}

// MakePostDeleteZettelHandler creates a new HTTP handler to delete a zettel.
func (wui *WebUI) MakePostDeleteZettelHandler(deleteZettel *usecase.DeleteZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		if err = deleteZettel.Run(r.Context(), zid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	})
}

Changes to web/adapter/webui/edit_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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19



20
21
22
23
24
25
26


27
28
29
30
31
32
33
34

35
36

37
38
39
40
41
42
43
44
45
46
47


48
49
50
51
52
53


54
55
56
57

58
59

60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

81
82

83
84

85
86

-
+






+
+
+







-

-
-
-
+
+
+




-
-
+
+
+
+
+
+

+
-
+

-
+










-
-
+
+




-
-
+
+

+
-
+

-
+




















-
+

-
+

-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

import (
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeEditGetZettelHandler creates a new HTTP handler to display the
// HTML edit view of a zettel.
func (wui *WebUI) MakeEditGetZettelHandler(getZettel usecase.GetZettel, ucListRoles usecase.ListRoles, ucListSyntax usecase.ListSyntax) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeEditGetZettelHandler(
	getZettel usecase.GetZettel,
	ucListRoles usecase.ListRoles,
	ucListSyntax usecase.ListSyntax,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zettel, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		roleData, syntaxData := retrieveDataLists(ctx, ucListRoles, ucListSyntax)
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "Edit Zettel", "", roleData, syntaxData)
	}
		wui.renderZettelForm(ctx, w, zettel, "Edit Zettel", "", roleData, syntaxData)
	})
}

// MakeEditSetZettelHandler creates a new HTTP handler to store content of
// an existing zettel.
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeEditSetZettelHandler(updateZettel *usecase.UpdateZettel) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		reEdit, zettel, err := parseZettelForm(r, zid)
		hasContent := true
		if err != nil {
			if err != errMissingContent {
				const msg = "Unable to read zettel form"
				wui.log.Info().Err(err).Msg(msg)
				wui.reportError(ctx, w, adapter.NewErrBadRequest(msg))
				return
			}
			hasContent = false
		}
		if err = updateZettel.Run(r.Context(), zettel, hasContent); err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		if reEdit {
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(api.ZettelID(zid.String())))
			wui.redirectFound(w, r, wui.NewURLBuilder('e').SetZid(zid.ZettelID()))
		} else {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())))
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(zid.ZettelID()))
		}
	}
	})
}

Changes to web/adapter/webui/favicon.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
1

2
3
4
5
6
7
8
9
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) 2022 Detlef Stern
// 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 webui

import (
	"io"
	"net/http"
	"os"
	"path/filepath"

	"zettelstore.de/z/web/adapter"
)

// MakeFaviconHandler creates a HTTP handler to retrieve the favicon.
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeFaviconHandler(baseDir string) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		filename := filepath.Join(baseDir, "favicon.ico")
		f, err := os.Open(filename)
		if err != nil {
			wui.log.Sense().Err(err).Msg("Favicon not found")
			wui.log.Debug().Err(err).Msg("Favicon not found")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}
		defer f.Close()

		data, err := io.ReadAll(f)
		if err != nil {
			wui.log.Info().Err(err).Msg("Unable to read favicon data")
			wui.log.Error().Err(err).Msg("Unable to read favicon data")
			http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
			return
		}

		err = adapter.WriteBytes(w, data, "")
		wui.log.IfErr(err).Msg("Write favicon")
	}
		if err = adapter.WriteData(w, data, ""); err != nil {
			wui.log.Error().Err(err).Msg("Write favicon")
		}
	})
}

Changes to web/adapter/webui/forms.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24








25
26
27
28
29
30
31
32
33
















34
35
36
37
38
39
40
41

42
43
44
45

46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

61
62
63
64
65
66
67
68

69
70
71

72
73
74
75

76
77
78

79
80
81
82

83
84

85
86
87
88
89
90
91
92

-
+






+
+
+













-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-








-
+



-
+














-
+
+
+
+




-
+


-
+



-
+


-
+



-
+

-
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

import (
	"bytes"
	"errors"
	"io"
	"net/http"
	"regexp"
	"strings"
	"unicode"

	"zettelstore.de/c/api"
	"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/parser"
	"zettelstore.de/z/web/content"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/input"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/content"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

type formZettelData struct {
	Heading       string
	FormActionURL string
	MetaTitle     string
	MetaRole      string
	HasRoleData   bool
	RoleData      []string
	HasSyntaxData bool
	SyntaxData    []string
	MetaTags      string
	MetaSyntax    string
	MetaPairsRest []meta.Pair
	IsTextContent bool
	Content       string
}

var (
	bsCRLF = []byte{'\r', '\n'}
	bsLF   = []byte{'\n'}
)

var errMissingContent = errors.New("missing zettel content")

func parseZettelForm(r *http.Request, zid id.Zid) (bool, domain.Zettel, error) {
func parseZettelForm(r *http.Request, zid id.Zid) (bool, zettel.Zettel, error) {
	maxRequestSize := kernel.Main.GetConfig(kernel.WebService, kernel.WebMaxRequestSize).(int64)
	err := r.ParseMultipartForm(maxRequestSize)
	if err != nil {
		return false, domain.Zettel{}, err
		return false, zettel.Zettel{}, err
	}
	_, doSave := r.Form["save"]

	var m *meta.Meta
	if postMeta, ok := trimmedFormValue(r, "meta"); ok {
		m = meta.NewFromInput(zid, input.NewInput(removeEmptyLines([]byte(postMeta))))
		m.Sanitize()
	} else {
		m = meta.New(zid)
	}
	if postTitle, ok := trimmedFormValue(r, "title"); ok {
		m.Set(api.KeyTitle, meta.RemoveNonGraphic(postTitle))
	}
	if postTags, ok := trimmedFormValue(r, "tags"); ok {
		if tags := strings.Fields(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
		if tags := meta.ListFromValue(meta.RemoveNonGraphic(postTags)); len(tags) > 0 {
			for i, tag := range tags {
				tags[i] = meta.NormalizeTag(tag)
			}
			m.SetList(api.KeyTags, tags)
		}
	}
	if postRole, ok := trimmedFormValue(r, "role"); ok {
		m.Set(api.KeyRole, meta.RemoveNonGraphic(postRole))
		m.SetWord(api.KeyRole, meta.RemoveNonGraphic(postRole))
	}
	if postSyntax, ok := trimmedFormValue(r, "syntax"); ok {
		m.Set(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
		m.SetWord(api.KeySyntax, meta.RemoveNonGraphic(postSyntax))
	}

	if data := textContent(r); data != nil {
		return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(data)}, nil
		return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(data)}, nil
	}
	if data, m2 := uploadedContent(r, m); data != nil {
		return doSave, domain.Zettel{Meta: m2, Content: domain.NewContent(data)}, nil
		return doSave, zettel.Zettel{Meta: m2, Content: zettel.NewContent(data)}, nil
	}

	if allowEmptyContent(m) {
		return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(nil)}, nil
		return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, nil
	}
	return doSave, domain.Zettel{Meta: m, Content: domain.NewContent(nil)}, errMissingContent
	return doSave, zettel.Zettel{Meta: m, Content: zettel.NewContent(nil)}, errMissingContent
}

func trimmedFormValue(r *http.Request, key string) (string, bool) {
	if values, ok := r.PostForm[key]; ok && len(values) > 0 {
		value := strings.TrimSpace(values[0])
		if len(value) > 0 {
			return value, true

Changes to web/adapter/webui/forms_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 webui

import "testing"

func TestRemoveEmptyLines(t *testing.T) {

Changes to web/adapter/webui/get_info.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
1

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

-
+






+
+
+





-


-
+


+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+

-
-
+
-
-
-
-
-
+
-
-
-
-
+
-


-
-
-
-
-
-
-
+
+
+
+
+
+
+



+
-
+

-
+
-
-
-
-
-
-



+
+
+
+
+
-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
+
+
-
-
-

-
+
-
-
-
-
-
+
+
+
+
+


+
+
-
+
+


-
+

-
-
+
-




-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
+
-
-
+
+
-
-
-
+
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
-
-
-
+
+
-





-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+





-
+


-
+




-
-
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
+


-
-
-
+
+
+
+
-
-
+
+
+
-
-
-
-




//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 webui

import (
	"bytes"
	"context"
	"net/http"
	"sort"
	"slices"
	"strings"

	"t73f.de/r/sx"
	"t73f.de/r/zsc/api"
	"zettelstore.de/c/api"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
)

	"zettelstore.de/z/zettel/id"
type metaDataInfo struct {
	Key   string
	Value string
}

)
type matrixLine struct {
	Header   string
	Elements []simpleLink
}


// MakeGetInfoHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetInfoHandler(
	parseZettel usecase.ParseZettel,
	evaluate *usecase.Evaluate,
	getMeta usecase.GetMeta,
	getAllMeta usecase.GetAllMeta,
	unlinkedRefs usecase.UnlinkedReferences,
) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
	ucParseZettel usecase.ParseZettel,
	ucEvaluate *usecase.Evaluate,
	ucGetZettel usecase.GetZettel,
	ucGetAllZettel usecase.GetAllZettel,
	ucQuery *usecase.Query,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		q := r.URL.Query()

		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		zn, err := parseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		zn, err := ucParseZettel.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		evalMetadata := createEvalMetadataFunc(ctx, evaluate)
		enc := wui.getSimpleHTMLEncoder()

		enc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, api.KeyLang))
		pairs := zn.Meta.ComputedPairs()
		metaData := make([]metaDataInfo, len(pairs))
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		getTextTitle := wui.makeGetTextTitle(ctx, ucGetZettel)
		for i, p := range pairs {
			var buf bytes.Buffer
			wui.writeHTMLMetaValue(
				&buf, p.Key, p.Value,
				getTextTitle,
				func(val string) ast.InlineSlice {
					return evaluate.RunMetadata(ctx, val)
		evalMeta := func(val string) ast.InlineSlice {
			return ucEvaluate.RunMetadata(ctx, val)
				},
				enc)
			metaData[i] = metaDataInfo{p.Key, buf.String()}
		}
		summary := collect.References(zn)
		pairs := zn.Meta.ComputedPairs()
		locLinks, qLinks, extLinks := splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
		queryLinks := make([]simpleLink, len(qLinks))
		for i, sq := range qLinks {
			queryLinks[i].Text = sq
			queryLinks[i].URL = wui.NewURLBuilder('h').AppendQuery(sq).String()
		metadata := sx.Nil()
		for i := len(pairs) - 1; i >= 0; i-- {
			key := pairs[i].Key
			sxval := wui.writeHTMLMetaValue(key, pairs[i].Value, getTextTitle, evalMeta, enc)
			metadata = metadata.Cons(sx.Cons(sx.MakeString(key), sxval))
		}

		summary := collect.References(zn)
		locLinks, queryLinks, extLinks := wui.splitLocSeaExtLinks(append(summary.Links, summary.Embeds...))
		textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext)

		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		phrase := q.Get(api.QueryKeyPhrase)
		if phrase == "" {
			phrase = textTitle
			phrase = title
		}
		phrase = strings.TrimSpace(phrase)
		unlinkedMeta, err := unlinkedRefs.Run(
		unlinkedMeta, err := ucQuery.Run(ctx, createUnlinkedQuery(zid, phrase))
			ctx, phrase, adapter.AddUnlinkedRefsToQuery(nil, zn.InhMeta))
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, unlinkedMeta, wui.rtConfig))
		unlinkedContent, err := enc.BlocksString(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		shadowLinks := getShadowLinks(ctx, zid, getAllMeta)
		endnotes, err := enc.BlocksString(&ast.BlockSlice{})
		if err != nil {
			endnotes = ""
		}


		entries, _ := evaluator.QueryAction(ctx, nil, unlinkedMeta)
		user := server.GetUser(ctx)
		canCreate := wui.canCreate(ctx, user)
		apiZid := api.ZettelID(zid.String())
		var base baseData
		bns := ucEvaluate.RunBlockNode(ctx, entries)
		unlinkedContent, _, err := enc.BlocksSxn(&bns)
		if err != nil {
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, "", user, &base)
		wui.renderTemplate(ctx, w, id.InfoTemplateZid, &base, struct {
			wui.reportError(ctx, w, err)
			Zid            string
			WebURL         string
			ContextURL     string
			CanWrite       bool
			EditURL        string
			CanCopy        bool
			CopyURL        string
			CanVersion     bool
			VersionURL     string
			CanFolge       bool
			FolgeURL       string
			CanRename      bool
			RenameURL      string
			CanDelete      bool
			DeleteURL      string
			return
		}
		encTexts := encodingTexts()
			MetaData       []metaDataInfo
			HasLocLinks    bool
			LocLinks       []localLink
			QueryLinks     simpleLinks
			HasExtLinks    bool
			ExtLinks       []string
			ExtNewWindow   string
			UnLinksContent string
			UnLinksPhrase  string
			QueryKeyPhrase string
			EvalMatrix     []matrixLine
			ParseMatrix    []matrixLine
			HasShadowLinks bool
		shadowLinks := getShadowLinks(ctx, zid, ucGetAllZettel)

		user := server.GetUser(ctx)
		env, rb := wui.createRenderEnv(ctx, "info", wui.getUserLang(ctx), title, user)
		rb.bindString("metadata", metadata)
		rb.bindString("local-links", locLinks)
		rb.bindString("query-links", queryLinks)
		rb.bindString("ext-links", extLinks)
		rb.bindString("unlinked-content", unlinkedContent)
		rb.bindString("phrase", sx.MakeString(phrase))
		rb.bindString("query-key-phrase", sx.MakeString(api.QueryKeyPhrase))
		rb.bindString("enc-eval", wui.infoAPIMatrix(zid, false, encTexts))
		rb.bindString("enc-parsed", wui.infoAPIMatrixParsed(zid, encTexts))
		rb.bindString("shadow-links", shadowLinks)
			ShadowLinks    []string
			Endnotes       string
		}{
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.InfoTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
	})
			Zid:            zid.String(),
			WebURL:         wui.NewURLBuilder('h').SetZid(apiZid).String(),
			ContextURL:     wui.NewURLBuilder('k').SetZid(apiZid).String(),
			CanWrite:       wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:        wui.NewURLBuilder('e').SetZid(apiZid).String(),
			CanCopy:        canCreate && !zn.Content.IsBinary(),
			CopyURL:        wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(),
			CanVersion:     canCreate,
			VersionURL:     wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(),
			CanFolge:       canCreate,
			FolgeURL:       wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(),
			CanRename:      wui.canRename(ctx, user, zn.Meta),
			RenameURL:      wui.NewURLBuilder('b').SetZid(apiZid).String(),
			CanDelete:      wui.canDelete(ctx, user, zn.Meta),
			DeleteURL:      wui.NewURLBuilder('d').SetZid(apiZid).String(),
			MetaData:       metaData,
			HasLocLinks:    len(locLinks) > 0,
			LocLinks:       locLinks,
			QueryLinks:     createSimpleLinks(queryLinks),
			HasExtLinks:    len(extLinks) > 0,
			ExtLinks:       extLinks,
}

func (wui *WebUI) splitLocSeaExtLinks(links []*ast.Reference) (locLinks, queries, extLinks *sx.Pair) {
	for i := len(links) - 1; i >= 0; i-- {
		ref := links[i]
		switch ref.State {
		case ast.RefStateHosted, ast.RefStateBased: // Local
			locLinks = locLinks.Cons(sx.MakeString(ref.String()))

		case ast.RefStateQuery:
			queries = queries.Cons(
			ExtNewWindow:   htmlAttrNewWindow(len(extLinks) > 0),
			UnLinksContent: unlinkedContent,
			UnLinksPhrase:  phrase,
			QueryKeyPhrase: api.QueryKeyPhrase,
			EvalMatrix:     wui.infoAPIMatrix(zid, false),
				sx.Cons(
					sx.MakeString(ref.Value),
					sx.MakeString(wui.NewURLBuilder('h').AppendQuery(ref.Value).String())))

		case ast.RefStateExternal:
			ParseMatrix:    wui.infoAPIMatrixPlain(zid),
			HasShadowLinks: len(shadowLinks) > 0,
			ShadowLinks:    shadowLinks,
			extLinks = extLinks.Cons(sx.MakeString(ref.String()))
			Endnotes:       endnotes,
		})
	}
}

type localLink struct {
		}
	}
	return locLinks, queries, extLinks
}

	Valid bool
	Zid   string
func createUnlinkedQuery(zid id.Zid, phrase string) *query.Query {
	var sb strings.Builder
}

	sb.Write(zid.Bytes())
func splitLocSeaExtLinks(links []*ast.Reference) (locLinks []localLink, queries, extLinks []string) {
	if len(links) == 0 {
	sb.WriteByte(' ')
	sb.WriteString(api.UnlinkedDirective)
		return nil, nil, nil
	}
	for _, ref := range links {
	for _, word := range strfun.MakeWords(phrase) {
		if ref.State == ast.RefStateSelf || ref.IsZettel() {
			continue
		sb.WriteByte(' ')
		sb.WriteString(api.PhraseDirective)
		}
		if ref.State == ast.RefStateQuery {
			queries = append(queries, ref.Value)
			continue
		}
		if ref.IsExternal() {
			extLinks = append(extLinks, ref.String())
			continue
		sb.WriteByte(' ')
		sb.WriteString(word)
	}
	sb.WriteByte(' ')
	sb.WriteString(api.OrderDirective)
	sb.WriteByte(' ')
	sb.WriteString(api.KeyID)
		}
		locLinks = append(locLinks, localLink{ref.IsValid(), ref.String()})
	}
	return query.Parse(sb.String())
}
	return locLinks, queries, extLinks
}


func encodingTexts() []string {
func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool) []matrixLine {
	encodings := encoder.GetEncodings()
	encTexts := make([]string, 0, len(encodings))
	for _, f := range encodings {
		encTexts = append(encTexts, f.String())
	}
	sort.Strings(encTexts)
	parts := getParts()
	matrix := make([]matrixLine, 0, len(parts))
	u := wui.NewURLBuilder('z').SetZid(api.ZettelID(zid.String()))
	for _, part := range parts {
		row := make([]simpleLink, len(encTexts))
		for j, enc := range encTexts {
	slices.Sort(encTexts)
	return encTexts
}

var apiParts = []string{api.PartZettel, api.PartMeta, api.PartContent}

func (wui *WebUI) infoAPIMatrix(zid id.Zid, parseOnly bool, encTexts []string) *sx.Pair {
	matrix := sx.Nil()
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
	for ip := len(apiParts) - 1; ip >= 0; ip-- {
		part := apiParts[ip]
		row := sx.Nil()
		for je := len(encTexts) - 1; je >= 0; je-- {
			enc := encTexts[je]
			if parseOnly {
				u.AppendKVQuery(api.QueryKeyParseOnly, "")
			}
			u.AppendKVQuery(api.QueryKeyPart, part)
			u.AppendKVQuery(api.QueryKeyEncoding, enc)
			row[j] = simpleLink{enc, u.AttrString()}
			row = row.Cons(sx.Cons(sx.MakeString(enc), sx.MakeString(u.String())))
			u.ClearQuery()
		}
		matrix = append(matrix, matrixLine{part, row})
		matrix = matrix.Cons(sx.Cons(sx.MakeString(part), row))
	}
	return matrix
}

func (wui *WebUI) infoAPIMatrixPlain(zid id.Zid) []matrixLine {
	matrix := wui.infoAPIMatrix(zid, true)
func (wui *WebUI) infoAPIMatrixParsed(zid id.Zid, encTexts []string) *sx.Pair {
	matrix := wui.infoAPIMatrix(zid, true, encTexts)
	apiZid := api.ZettelID(zid.String())

	// Append plain and JSON format
	u := wui.NewURLBuilder('z').SetZid(apiZid)
	u := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
	for i, part := range getParts() {
		u.AppendKVQuery(api.QueryKeyPart, part)
		matrix[i].Elements = append(matrix[i].Elements, simpleLink{"plain", u.AttrString()})
		u.ClearQuery()
	}

	u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
	u.AppendKVQuery(api.QueryKeyPart, api.PartZettel)
	matrix[0].Elements = append(matrix[0].Elements, simpleLink{"json", u.AttrString()})

	u.ClearQuery()
	u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingJson)
	u.AppendKVQuery(api.QueryKeyPart, api.PartMeta)
	matrix[1].Elements = append(matrix[1].Elements, simpleLink{"json", u.AttrString()})

	for i, row := 0, matrix; i < len(apiParts) && row != nil; row = row.Tail() {
		line, isLine := sx.GetPair(row.Car())
		if !isLine || line == nil {
			continue
		}
		last := line.LastPair()
		part := apiParts[i]
		u.AppendKVQuery(api.QueryKeyPart, part)
		last = last.AppendBang(sx.Cons(sx.MakeString("plain"), sx.MakeString(u.String())))
		u.ClearQuery()
		if i < 2 {
			u.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData)
			u.AppendKVQuery(api.QueryKeyPart, part)
			last.AppendBang(sx.Cons(sx.MakeString("data"), sx.MakeString(u.String())))
	return matrix
}

			u.ClearQuery()
		}
		i++
	}
func getParts() []string {
	return []string{api.PartZettel, api.PartMeta, api.PartContent}
	return matrix
}

func getShadowLinks(ctx context.Context, zid id.Zid, getAllMeta usecase.GetAllMeta) []string {
	ml, err := getAllMeta.Run(ctx, zid)
	if err != nil || len(ml) < 2 {
func getShadowLinks(ctx context.Context, zid id.Zid, getAllZettel usecase.GetAllZettel) *sx.Pair {
	result := sx.Nil()
	if zl, err := getAllZettel.Run(ctx, zid); err == nil {
		for i := len(zl) - 1; i >= 1; i-- {
		return nil
	}
			if boxNo, ok := zl[i].Meta.Get(api.KeyBoxNumber); ok {
				result = result.Cons(sx.MakeString(boxNo))
			}
	result := make([]string, 0, len(ml)-1)
	for _, m := range ml[1:] {
		if boxNo, ok := m.Get(api.KeyBoxNumber); ok {
			result = append(result, boxNo)
		}
	}
	return result
}

Changes to web/adapter/webui/get_zettel.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14

15

16

17
18
19
20
21
22
23
24
25









26
27
28
29
30





31

32

33
34

35
36
37
38
39



40






41
42
43
44
45
46
47
48
49
50

51
52

53
54

55
56

57
58
59





60
61
62


63
64
65

66
67
68
69
70
71



72
73
74
75

76
77

78
79
80
81
82
83
84
85
86
87







88
89

90
91
92
93
94
95






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
1

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

-
+






+
+
+





-
+

+

+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+



-
-
+
+
+
+
+

+
-
+

-
+





+
+
+
-
+
+
+
+
+
+





-
-
-
-
-
+
-
-
+
-
-
+
-
-
+
-
-
-
+
+
+
+
+

-
-
+
+

-
-
+
-
-
-
-
-
-
+
+
+
-
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
-
-
+
-
-
+

-
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-

+
-
+
-
-
-
-
-
+
-
-
-
+
-
-
+
+
-
-
+
-
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
-
-
+
+

-
+


-
+




-
+


-
-
-
+
+
+





-
+

-
+

-
+



-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

import (
	"bytes"
	"context"
	"net/http"
	"strings"

	"t73f.de/r/sx"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeGetHTMLZettelHandler creates a new HTTP handler for the use case "get zettel".
func (wui *WebUI) MakeGetHTMLZettelHandler(evaluate *usecase.Evaluate, getMeta usecase.GetMeta) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeGetHTMLZettelHandler(
	evaluate *usecase.Evaluate,
	getZettel usecase.GetZettel,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		path := r.URL.Path[1:]
		zid, err := id.Parse(r.URL.Path[1:])
		zid, err := id.Parse(path)
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			wui.reportError(ctx, w, box.ErrInvalidZid{Zid: path})
			return
		}

		q := r.URL.Query()
		zn, err := evaluate.Run(ctx, zid, q.Get(api.KeySyntax))
		if err != nil {
			wui.reportError(ctx, w, err)
			return

		}

		zettelLang := wui.getConfig(ctx, zn.InhMeta, api.KeyLang)
		enc := wui.getSimpleHTMLEncoder(zettelLang)
		metaObj := enc.MetaSxn(zn.InhMeta, createEvalMetadataFunc(ctx, evaluate))
		content, endnotes, err := enc.BlocksSxn(&zn.Ast)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		enc := wui.createZettelEncoder(ctx, zn.InhMeta)
		evalMetadata := createEvalMetadataFunc(ctx, evaluate)
		textTitle := encodeEvaluatedTitleText(zn.InhMeta, evalMetadata, wui.gentext)
		htmlTitle := encodeEvaluatedTitleHTML(zn.InhMeta, evalMetadata, enc)
		htmlContent, err := enc.BlocksString(&zn.Ast)
		user := server.GetUser(ctx)
		if err != nil {
			wui.reportError(ctx, w, err)
		getTextTitle := wui.makeGetTextTitle(ctx, getZettel)
			return
		}

		var roleCSSURL string
		cssZid, err := wui.retrieveCSSZidFromRole(ctx, *zn.InhMeta)
		title := parser.NormalizedSpacedText(zn.InhMeta.GetTitle())
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		env, rb := wui.createRenderEnv(ctx, "zettel", zettelLang, title, user)
		rb.bindSymbol(symMetaHeader, metaObj)
		rb.bindString("heading", sx.MakeString(title))
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			rb.bindString("role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+role).String()))
		}
		if cssZid != id.Invalid {
			roleCSSURL = wui.NewURLBuilder('z').SetZid(api.ZettelID(cssZid.String())).String()
		if folgeRole, found := zn.InhMeta.Get(api.KeyFolgeRole); found && folgeRole != "" {
			rb.bindString("folge-role-url", sx.MakeString(wui.NewURLBuilder('h').AppendQuery(api.KeyRole+api.SearchOperatorHas+folgeRole).String()))
		}
		user := server.GetUser(ctx)
		roleText := zn.Meta.GetDefault(api.KeyRole, "")
		rb.bindString("tag-refs", wui.transformTagSet(api.KeyTags, meta.ListFromValue(zn.InhMeta.GetDefault(api.KeyTags, ""))))
		canCreate := wui.canCreate(ctx, user)
		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), evalMetadata)
		extURL, hasExtURL := formatURLFromMeta(zn.Meta, api.KeyURL)
		folgeLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyFolge, getTextTitle))
		backLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeyBack, getTextTitle))
		successorLinks := createSimpleLinks(wui.encodeZettelLinks(zn.InhMeta, api.KeySuccessors, getTextTitle))
		rb.bindString("predecessor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPredecessor, getTextTitle))
		rb.bindString("precursor-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrecursor, getTextTitle))
		rb.bindString("prequel-refs", wui.identifierSetAsLinks(zn.InhMeta, api.KeyPrequel, getTextTitle))
		apiZid := api.ZettelID(zid.String())
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, zn.InhMeta, api.KeyLang), textTitle, roleCSSURL, user, &base)
		base.MetaHeader = enc.MetaString(zn.InhMeta, evalMetadata)
		rb.bindString("urls", metaURLAssoc(zn.InhMeta))
		wui.renderTemplate(ctx, w, id.ZettelTemplateZid, &base, struct {
			HTMLTitle       string
		rb.bindString("content", content)
			RoleCSS         string
			CanWrite        bool
			EditURL         string
			Zid             string
			InfoURL         string
			RoleText        string
			RoleURL         string
			Tags            simpleLinks
			CanCopy         bool
			CopyURL         string
		rb.bindString("endnotes", endnotes)
		wui.bindLinks(ctx, &rb, "folge", zn.InhMeta, api.KeyFolge, config.KeyShowFolgeLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "sequel", zn.InhMeta, api.KeySequel, config.KeyShowSequelLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "back", zn.InhMeta, api.KeyBack, config.KeyShowBackLinks, getTextTitle)
		wui.bindLinks(ctx, &rb, "successor", zn.InhMeta, api.KeySuccessors, config.KeyShowSuccessorLinks, getTextTitle)
		if role, found := zn.InhMeta.Get(api.KeyRole); found && role != "" {
			for _, part := range []string{"meta", "actions", "heading"} {
			CanVersion      bool
			VersionURL      string
				rb.rebindResolved("ROLE-"+role+"-"+part, "ROLE-DEFAULT-"+part)
			CanFolge        bool
			FolgeURL        string
			PredecessorRefs string
			PrecursorRefs   string
			HasExtURL       bool
			ExtURL          string
			}
		}
		wui.bindCommonZettelData(ctx, &rb, user, zn.InhMeta, &zn.Content)
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ZettelTemplateZid, env)
		} else {
			ExtNewWindow    string
			Author          string
			Content         string
			NeedBottomNav   bool
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
			FolgeLinks      simpleLinks
			BackLinks       simpleLinks
			SuccessorLinks  simpleLinks
		}{
		}
	})
			HTMLTitle:       htmlTitle,
			RoleCSS:         roleCSSURL,
			CanWrite:        wui.canWrite(ctx, user, zn.Meta, zn.Content),
			EditURL:         wui.NewURLBuilder('e').SetZid(apiZid).String(),
			Zid:             zid.String(),
			InfoURL:         wui.NewURLBuilder('i').SetZid(apiZid).String(),
			RoleText:        roleText,
			RoleURL:         wui.NewURLBuilder('h').AppendQuery(api.KeyRole + api.SearchOperatorHas + roleText).String(),
			Tags:            createSimpleLinks(wui.buildTagInfos(zn.Meta)),
			CanCopy:         canCreate && !zn.Content.IsBinary(),
			CopyURL:         wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String(),
			CanVersion:      canCreate,
			VersionURL:      wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String(),
			CanFolge:        canCreate,
}
			FolgeURL:        wui.NewURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String(),
			PredecessorRefs: wui.encodeIdentifierSet(zn.InhMeta, api.KeyPredecessor, getTextTitle),
			PrecursorRefs:   wui.encodeIdentifierSet(zn.InhMeta, api.KeyPrecursor, getTextTitle),
			ExtURL:          extURL,
			HasExtURL:       hasExtURL,
			ExtNewWindow:    htmlAttrNewWindow(hasExtURL),
			Author:          zn.Meta.GetDefault(api.KeyAuthor, ""),
			Content:         htmlContent,
			NeedBottomNav:   folgeLinks.Has || backLinks.Has || successorLinks.Has,
			FolgeLinks:      folgeLinks,
			BackLinks:       backLinks,
			SuccessorLinks:  successorLinks,
		})
	}

}

func (wui *WebUI) identifierSetAsLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
func formatURLFromMeta(m *meta.Meta, key string) (string, bool) {
	val, found := m.Get(key)
	if values, ok := m.GetList(key); ok {
	if !found {
		return "", false
		return wui.transformIdentifierSet(values, getTextTitle)
	}
	if found && val == "" {
		return "", false
	}
	return nil
}
	var buf bytes.Buffer
	_, err := html.AttributeEscape(&buf, val)
	if err != nil {
		return "", false
	}
	return buf.String(), true
}


func metaURLAssoc(m *meta.Meta) *sx.Pair {
	var result sx.ListBuilder
	for _, p := range m.PairsRest() {
		if key := p.Key; strings.HasSuffix(key, meta.SuffixKeyURL) {
			if val := p.Value; val != "" {
				result.Add(sx.Cons(sx.MakeString(capitalizeMetaKey(key)), sx.MakeString(val)))
			}
		}
func encodeInlinesText(is *ast.InlineSlice, enc *textenc.Encoder) (string, error) {
	if is == nil || len(*is) == 0 {
		return "", nil
	}
	return result.List()

}
	var buf bytes.Buffer
	_, err := enc.WriteInlines(&buf, is)
	if err != nil {
		return "", err
	}

	return buf.String(), nil
}

func (wui *WebUI) bindLinks(ctx context.Context, rb *renderBinder, varPrefix string, m *meta.Meta, key, configKey string, getTextTitle getTextTitleFunc) {
func (wui *WebUI) buildTagInfos(m *meta.Meta) []simpleLink {
	var tagInfos []simpleLink
	varLinks := varPrefix + "-links"
	var symOpen *sx.Symbol
	if tags, ok := m.GetList(api.KeyTags); ok {
		ub := wui.NewURLBuilder('h')
	switch wui.getConfig(ctx, m, configKey) {
		tagInfos = make([]simpleLink, len(tags))
		for i, tag := range tags {
			tagInfos[i] = simpleLink{Text: tag, URL: ub.AppendQuery(api.KeyTags + api.SearchOperatorHas + tag).String()}
	case "false":
		rb.bindString(varLinks, sx.Nil())
			ub.ClearQuery()
		}
	}
	return tagInfos
}

		return
	case "close":
	default:
		symOpen = shtml.SymAttrOpen
	}
	lstLinks := wui.zettelLinksSxn(m, key, getTextTitle)
func (wui *WebUI) encodeIdentifierSet(m *meta.Meta, key string, getTextTitle getTextTitleFunc) string {
	if value, ok := m.Get(key); ok {
		var buf bytes.Buffer
	rb.bindString(varLinks, lstLinks)
		wui.writeIdentifierSet(&buf, meta.ListFromValue(value), getTextTitle)
		return buf.String()
	if sx.IsNil(lstLinks) {
		return
	}
	return ""
	rb.bindString(varPrefix+"-open", symOpen)
}

func (wui *WebUI) encodeZettelLinks(m *meta.Meta, key string, getTextTitle getTextTitleFunc) []simpleLink {
func (wui *WebUI) zettelLinksSxn(m *meta.Meta, key string, getTextTitle getTextTitleFunc) *sx.Pair {
	values, ok := m.GetList(key)
	if !ok || len(values) == 0 {
		return nil
	}
	return wui.encodeZidLinks(values, getTextTitle)
	return wui.zidLinksSxn(values, getTextTitle)
}

func (wui *WebUI) encodeZidLinks(values []string, getTextTitle getTextTitleFunc) []simpleLink {
	result := make([]simpleLink, 0, len(values))
	for _, val := range values {
func (wui *WebUI) zidLinksSxn(values []string, getTextTitle getTextTitleFunc) (lst *sx.Pair) {
	for i := len(values) - 1; i >= 0; i-- {
		val := values[i]
		zid, err := id.Parse(val)
		if err != nil {
			continue
		}
		if title, found := getTextTitle(zid); found > 0 {
			url := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String())).String()
			url := sx.MakeString(wui.NewURLBuilder('h').SetZid(zid.ZettelID()).String())
			if title == "" {
				result = append(result, simpleLink{Text: val, URL: url})
				lst = lst.Cons(sx.Cons(sx.MakeString(val), url))
			} else {
				result = append(result, simpleLink{Text: title, URL: url})
				lst = lst.Cons(sx.Cons(sx.MakeString(title), url))
			}
		}
	}
	return result
	return lst
}

Changes to web/adapter/webui/goaction.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
1

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

import (
	"net/http"

	"zettelstore.de/z/usecase"
)

// MakeGetGoActionHandler creates a new HTTP handler to execute certain commands.
func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeGetGoActionHandler(ucRefresh *usecase.Refresh) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		// Currently, command "refresh" is the only command to be executed.
		err := ucRefresh.Run(ctx)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
	})
}

Changes to web/adapter/webui/home.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
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20






21
22
23
24
25
26
27
28

29


30
31
32
33


34
35
36


37
38
39
40


41
42
43

44
45
46
47
48
49

50
51
52
53
54
55
56
57
58
59

60
61

-
+






+
+
+









-
-
-
-
-
-
+
+
+
+
+
+


-
+
-
-
+



-
-
+
+

-
-
+
+


-
-
+
+

-
+





-
+









-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 webui

import (
	"context"
	"errors"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/box"
	"zettelstore.de/z/config"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
)

type getRootStore interface {
type getRootPort interface {
	// GetMeta retrieves just the meta data of a specific zettel.
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	GetZettel(ctx context.Context, zid id.Zid) (zettel.Zettel, error)
}

// MakeGetRootHandler creates a new HTTP handler to show the root URL.
func (wui *WebUI) MakeGetRootHandler(s getRootStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeGetRootHandler(s getRootPort) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		if r.URL.Path != "/" {
			wui.reportError(ctx, w, box.ErrNotFound)
		if p := r.URL.Path; p != "/" {
			wui.reportError(ctx, w, adapter.ErrResourceNotFound{Path: p})
			return
		}
		homeZid, _ := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := api.ZettelID(homeZid.String())
		homeZid, _ := id.Parse(wui.getConfig(ctx, nil, config.KeyHomeZettel))
		apiHomeZid := homeZid.ZettelID()
		if homeZid != id.DefaultHomeZid {
			if _, err := s.GetMeta(ctx, homeZid); err == nil {
			if _, err := s.GetZettel(ctx, homeZid); err == nil {
				wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
				return
			}
			homeZid = id.DefaultHomeZid
		}
		_, err := s.GetMeta(ctx, homeZid)
		_, err := s.GetZettel(ctx, homeZid)
		if err == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(apiHomeZid))
			return
		}
		if errors.Is(err, &box.ErrNotAllowed{}) && wui.authz.WithAuth() && server.GetUser(ctx) == nil {
			wui.redirectFound(w, r, wui.NewURLBuilder('i'))
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h'))
	}
	})
}

Changes to web/adapter/webui/htmlgen.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
1

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

-
+






+
+
+





-
+


-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+









-
-
-
-
+
+
+
+


-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+






-
+
+
+
+
+
+
+
+

-
-
+

-
+



-
-
-
+
+
+


-
+
-
-
-
+
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
+
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
-
-
-
-
-
+
+
-
-
+
-
-
+
+
-
+

-
+

+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-

+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
-
-
-
-
-
-
+
+
+
+
-
-
-
+
-

//-----------------------------------------------------------------------------
// Copyright (c) 2022-2023 Detlef Stern
// 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 webui

import (
	"bytes"
	"net/url"
	"strings"

	"codeberg.org/t73fde/sxpf"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/c/sexpr"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/sexprenc"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/query"
	"zettelstore.de/z/strfun"
	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/attrs"
	"t73f.de/r/zsc/maps"
	"t73f.de/r/zsc/shtml"
	"t73f.de/r/zsc/sz"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/szenc"
	"zettelstore.de/z/strfun"
	"zettelstore.de/z/zettel/meta"
)

// Builder allows to build new URLs for the web service.
type urlBuilder interface {
	GetURLPrefix() string
	NewURLBuilder(key byte) *api.URLBuilder
}

type htmlGenerator struct {
	builder   urlBuilder
	textEnc   *textenc.Encoder
	extMarker string
	env       *html.EncEnvironment
	tx    *szenc.Transformer
	th    *shtml.Evaluator
	lang  string
	symAt *sx.Symbol
}

func createGenerator(builder urlBuilder, extMarker string) *htmlGenerator {
	env := html.NewEncEnvironment(nil, 1)
	gen := &htmlGenerator{
		builder:   builder,
		textEnc:   textenc.Create(),
		extMarker: extMarker,
func (wui *WebUI) createGenerator(builder urlBuilder, lang string) *htmlGenerator {
	th := shtml.NewEvaluator(1)

	findA := func(obj sx.Object) (attr, assoc, rest *sx.Pair) {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymA.IsEqual(pair.Car()) {
			return nil, nil, nil
		}
		rest = pair.Tail()
		if rest == nil {
			return nil, nil, nil
		}
		objA := rest.Car()
		attr, isPair = sx.GetPair(objA)
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
			return nil, nil, nil
		}
		return attr, attr.Tail(), rest.Tail()
	}
	linkZettel := func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}

		env:       env,
	}

	env.Builtins.Set(sexpr.SymLinkZettel, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
	env.Builtins.Set(sexpr.SymLinkFound, sxpf.NewBuiltin("linkZ", true, 2, -1, gen.generateLinkZettel))
	env.Builtins.Set(sexpr.SymLinkBased, sxpf.NewBuiltin("linkB", true, 2, -1, gen.generateLinkBased))
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		zid, fragment, hasFragment := strings.Cut(href.GetValue(), "#")
		u := builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
			u = u.SetFragment(fragment)
		}
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	}

	rebind(th, sz.SymLinkZettel, linkZettel)
	rebind(th, sz.SymLinkFound, linkZettel)
	rebind(th, sz.SymLinkBased, func(obj sx.Object) sx.Object {
	env.Builtins.Set(sexpr.SymLinkQuery, sxpf.NewBuiltin("linkQ", true, 2, -1, gen.generateLinkQuery))
	env.Builtins.Set(sexpr.SymLinkExternal, sxpf.NewBuiltin("linkE", true, 2, -1, gen.generateLinkExternal))

		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
	f, err := env.Builtins.LookupForm(sexpr.SymEmbed)
	if err != nil {
		panic(err)
	}
	b := f.(*sxpf.Builtin)
	env.Builtins.Set(sexpr.SymEmbed, sxpf.NewBuiltin(b.Name(), true, 3, -1, gen.makeGenerateEmbed(b.GetValue())))
	return gen
}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		u := builder.NewURLBuilder('/')
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String()+href.GetValue()[1:])))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkQuery, func(obj sx.Object) sx.Object {
		attr, assoc, rest := findA(obj)
		if attr == nil {
			return obj
		}
		hrefP := assoc.Assoc(shtml.SymAttrHref)
		if hrefP == nil {
			return obj
		}
		href, ok := sx.GetString(hrefP.Cdr())
		if !ok {
			return obj
		}
		ur, err := url.Parse(href.GetValue())
		if err != nil {
			return obj
		}
		q := ur.Query().Get(api.QueryKeyQuery)
		if q == "" {
			return obj
		}
		u := builder.NewURLBuilder('h').AppendQuery(q)
		assoc = assoc.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())))
		return rest.Cons(assoc.Cons(sxhtml.SymAttr)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymLinkExternal, func(obj sx.Object) sx.Object {
		attr, _, rest := findA(obj)
		if attr == nil {
			return obj
		}
		a := sz.GetAttributes(attr)
		a = a.Set("target", "_blank")
		a = a.Add("rel", "external").Add("rel", "noreferrer")
		return rest.Cons(shtml.EvaluateAttrbute(a)).Cons(shtml.SymA)
	})
	rebind(th, sz.SymEmbed, func(obj sx.Object) sx.Object {
		pair, isPair := sx.GetPair(obj)
		if !isPair || !shtml.SymIMG.IsEqual(pair.Car()) {
			return obj
		}
		attr, isPair := sx.GetPair(pair.Tail().Car())
		if !isPair || !sxhtml.SymAttr.IsEqual(attr.Car()) {
			return obj
		}
		srcP := attr.Tail().Assoc(shtml.SymAttrSrc)
		if srcP == nil {
			return obj
		}
		src, isString := sx.GetString(srcP.Cdr())
		if !isString {
			return obj
		}
		zid := api.ZettelID(src.GetValue())
		if !zid.IsValid() {
			return obj
		}
		u := builder.NewURLBuilder('z').SetZid(zid)
		imgAttr := attr.Tail().Cons(sx.Cons(shtml.SymAttrSrc, sx.MakeString(u.String()))).Cons(sxhtml.SymAttr)
		return pair.Tail().Tail().Cons(imgAttr).Cons(shtml.SymIMG)
	})

	return &htmlGenerator{
		tx:   szenc.NewTransformer(),
		th:   th,
		lang: lang,
	}
}

func rebind(ev *shtml.Evaluator, sym *sx.Symbol, fn func(sx.Object) sx.Object) {
	prevFn := ev.ResolveBinding(sym)
	ev.Rebind(sym, func(args sx.Vector, env *shtml.Environment) sx.Object {
		obj := prevFn(args, env)
		if env.GetError() == nil {
			return fn(obj)
		}
		return sx.Nil()
	})
}

// SetUnique sets a prefix to make several HTML ids unique.
func (g *htmlGenerator) SetUnique(s string) *htmlGenerator { g.th.SetUnique(s); return g }

var mapMetaKey = map[string]string{
	api.KeyCopyright: "copyright",
	api.KeyLicense:   "license",
}

func (g *htmlGenerator) MetaString(m *meta.Meta, evalMeta encoder.EvalMetaFunc) string {
func (g *htmlGenerator) MetaSxn(m *meta.Meta, evalMeta encoder.EvalMetaFunc) *sx.Pair {
	tm := g.tx.GetMeta(m, evalMeta)
	env := shtml.MakeEnvironment(g.lang)
	hm, err := g.th.Evaluate(tm, &env)
	if err != nil {
		return nil
	}

	ignore := strfun.NewSet(api.KeyTitle, api.KeyLang)
	var buf bytes.Buffer

	metaMap := make(map[string]*sx.Pair, m.Length())
	if tags, ok := m.Get(api.KeyTags); ok {
		writeMetaTags(&buf, tags)
		metaMap[api.KeyTags] = g.transformMetaTags(tags)
		ignore.Set(api.KeyTags)
	}

	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignore.Has(key) {
	for elem := hm; elem != nil; elem = elem.Tail() {
		mlst, isPair := sx.GetPair(elem.Car())
		if !isPair {
			continue
		}
		if altKey, found := mapMetaKey[key]; found {
		att, isPair := sx.GetPair(mlst.Tail().Car())
			buf.WriteString(`<meta name="`)
			buf.WriteString(altKey)
		} else {
		if !isPair {
			buf.WriteString(`<meta name="zs-`)
			buf.WriteString(key)
		}
		buf.WriteString(`" content="`)
			continue
		is := evalMeta(p.Value)
		var sb strings.Builder
		g.textEnc.WriteInlines(&sb, &is)
		html.AttributeEscape(&buf, sb.String())
		buf.WriteString("\">\n")
	}
	return buf.String()
}
		}
		if !att.Car().IsEqual(g.symAt) {
			continue
		}
func writeMetaTags(buf *bytes.Buffer, tags string) {
	buf.WriteString(`<meta name="keywords" content="`)
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
		a := make(attrs.Attributes, 32)
		for aelem := att.Tail(); aelem != nil; aelem = aelem.Tail() {
			if p, ok := sx.GetPair(aelem.Car()); ok {
				key := p.Car()
				val := p.Cdr()
				if tail, isTail := sx.GetPair(val); isTail {
			buf.WriteString(", ")
		}
					val = tail.Car()
				}
		html.AttributeEscape(buf, strings.TrimPrefix(val, "#"))
	}
				a = a.Set(sz.GoValue(key), sz.GoValue(val))
			}
	buf.WriteString("\">\n")
}

		}
		name, found := a.Get("name")
		if !found || ignore.Has(name) {
			continue
		}
// BlocksString encodes a block slice.
func (g *htmlGenerator) BlocksString(bs *ast.BlockSlice) (string, error) {
	if bs == nil || len(*bs) == 0 {
		return "", nil
	}
	lst := sexprenc.GetSexpr(bs)

		newName, found := mapMetaKey[name]
	var buf bytes.Buffer
	g.env.ReplaceWriter(&buf)
	sxpf.Eval(g.env, lst)
	if g.env.GetError() == nil {
		if !found {
		g.env.WriteEndnotes()
	}
			continue
		}
	g.env.ReplaceWriter(nil)
	return buf.String(), g.env.GetError()
}

		a = a.Set("name", newName)
		metaMap[newName] = g.th.EvaluateMeta(a)
	}
	result := sx.Nil()
	keys := maps.Keys(metaMap)
	for i := len(keys) - 1; i >= 0; i-- {
		result = result.Cons(metaMap[keys[i]])
	}
// InlinesString writes an inline slice to the writer
func (g *htmlGenerator) InlinesString(is *ast.InlineSlice) string {
	if is == nil || len(*is) == 0 {
		return ""
	}
	return result
}
	return html.EvaluateInline(g.env, sexprenc.GetSexpr(is), true, false)
}

func (g *htmlGenerator) generateLinkZettel(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)

func (g *htmlGenerator) transformMetaTags(tags string) *sx.Pair {
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		zid, fragment, hasFragment := strings.Cut(refValue, "#")
	var sb strings.Builder
		u := g.builder.NewURLBuilder('h').SetZid(api.ZettelID(zid))
		if hasFragment {
	for i, val := range meta.ListFromValue(tags) {
		if i > 0 {
			u = u.SetFragment(fragment)
			sb.WriteString(", ")
		}
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
		sb.WriteString(strings.TrimPrefix(val, "#"))
	}
	metaTags := sb.String()
	if len(metaTags) == 0 {
	return nil, nil
}

		return nil
	}
	return g.th.EvaluateMeta(attrs.Attributes{"name": "keywords", "content": metaTags})
}
func (g *htmlGenerator) generateLinkBased(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		u := g.builder.NewURLBuilder('/').SetRawLocal(refValue)
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	return nil, nil

func (g *htmlGenerator) BlocksSxn(bs *ast.BlockSlice) (content, endnotes *sx.Pair, _ error) {
	if bs == nil || len(*bs) == 0 {
		return nil, nil, nil
}

func (g *htmlGenerator) generateLinkQuery(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		queryExpr := query.Parse(refValue).String()
		u := g.builder.NewURLBuilder('h').AppendQuery(queryExpr)
		html.WriteLink(env, args, a.Set("href", u.String()), refValue, "")
	}
	sx := g.tx.GetSz(bs)
	env := shtml.MakeEnvironment(g.lang)
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
	return nil, nil
}

		return nil, nil, err
	}
	return sh, shtml.Endnotes(&env), nil
}
func (g *htmlGenerator) generateLinkExternal(senv sxpf.Environment, args *sxpf.Pair, _ int) (sxpf.Value, error) {
	env := senv.(*html.EncEnvironment)
	if a, refValue, ok := html.PrepareLink(env, args); ok {
		a = a.Set("href", refValue).
			AddClass("external").
			Set("target", "_blank").
			Set("rel", "noopener noreferrer")
		html.WriteLink(env, args, a, refValue, g.extMarker)
	}
	return nil, nil
}


// InlinesSxHTML returns an inline slice, encoded as a SxHTML object.
func (g *htmlGenerator) InlinesSxHTML(is *ast.InlineSlice) *sx.Pair {
	if is == nil || len(*is) == 0 {
		return nil
	}
	sx := g.tx.GetSz(is)
func (g *htmlGenerator) makeGenerateEmbed(oldFn sxpf.BuiltinFn) sxpf.BuiltinFn {
	return func(senv sxpf.Environment, args *sxpf.Pair, arity int) (sxpf.Value, error) {
		env := senv.(*html.EncEnvironment)
	env := shtml.MakeEnvironment(g.lang)
		ref := env.GetPair(args.GetTail())
		refValue := env.GetString(ref.GetTail())
		zid := api.ZettelID(refValue)
		if !zid.IsValid() {
			return oldFn(senv, args, arity)
		}
	sh, err := g.th.Evaluate(sx, &env)
	if err != nil {
		return nil
	}
		u := g.builder.NewURLBuilder('z').SetZid(zid)
		env.WriteImageWithSource(args, u.String())
		return nil, nil
	return sh
	}
}

Changes to web/adapter/webui/htmlmeta.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
1

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

-
+






+
+
+







-
-
-
-

+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+


-
-

-




-
+


-
+

-
+

-
+

-
+

-
+

-
+

-
+


+
-
+
+
+
+
+
+
+

+

-
+

-
+
-
-

-
+

-
-
+



-
-
-
-
+
+


-
-
+




-
-
+
+
+
-
-
+
-

+
+

-
-
-
+
+
+



-
+
-
-
+
-
-
-
-
-
-
+
-
-
-
-
+
+
-
-
+
-
-
+
-
-
-
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+

-
-
+
+
-
-
-
-

+


-
+
-
+
+


-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-










-
+

-
+






-
+



-
+

-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

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

	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"zettelstore.de/c/api"
	"zettelstore.de/c/html"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/usecase"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/box"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

var space = []byte{' '}

func (wui *WebUI) writeHTMLMetaValue(
	w io.Writer,
	key, value string,
	getTextTitle getTextTitleFunc,
	evalMetadata evalMetadataFunc,
	gen *htmlGenerator,
) {
) sx.Object {
	switch kt := meta.Type(key); kt {
	case meta.TypeCredential:
		writeCredential(w, value)
		return sx.MakeString(value)
	case meta.TypeEmpty:
		writeEmpty(w, value)
		return sx.MakeString(value)
	case meta.TypeID:
		wui.writeIdentifier(w, value, getTextTitle)
		return wui.transformIdentifier(value, getTextTitle)
	case meta.TypeIDSet:
		wui.writeIdentifierSet(w, meta.ListFromValue(value), getTextTitle)
		return wui.transformIdentifierSet(meta.ListFromValue(value), getTextTitle)
	case meta.TypeNumber:
		wui.writeNumber(w, key, value)
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeString:
		writeString(w, value)
		return sx.MakeString(value)
	case meta.TypeTagSet:
		wui.writeTagSet(w, key, meta.ListFromValue(value))
		return wui.transformTagSet(key, meta.ListFromValue(value))
	case meta.TypeTimestamp:
		if ts, ok := meta.TimeValue(value); ok {
			return sx.MakeList(
			writeTimestamp(w, ts)
				sx.MakeSymbol("time"),
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(sx.MakeSymbol("datetime"), sx.MakeString(ts.Format("2006-01-02T15:04:05"))),
				),
				sx.MakeList(sxhtml.SymNoEscape, sx.MakeString(ts.Format("2006-01-02&nbsp;15:04:05"))),
			)
		}
		return sx.Nil()
	case meta.TypeURL:
		writeURL(w, value)
		return wui.url2html(sx.MakeString(value))
	case meta.TypeWord:
		wui.writeWord(w, key, value)
		return wui.transformKeyValueText(key, value, value)
	case meta.TypeWordSet:
		wui.writeWordSet(w, key, meta.ListFromValue(value))
	case meta.TypeZettelmarkup:
		io.WriteString(w, encodeZmkMetadata(value, evalMetadata, gen))
		return wui.transformZmkMetadata(value, evalMetadata, gen)
	default:
		html.Escape(w, value)
		fmt.Fprintf(w, " <b>(Unhandled type: %v, key: %v)</b>", kt, key)
		return sx.MakeList(shtml.SymSTRONG, sx.MakeString("Unhandled type: "), sx.MakeString(kt.Name))
	}
}

func writeCredential(w io.Writer, val string) { html.Escape(w, val) }
func writeEmpty(w io.Writer, val string)      { html.Escape(w, val) }

func (wui *WebUI) writeIdentifier(w io.Writer, val string, getTextTitle getTextTitleFunc) {
func (wui *WebUI) transformIdentifier(val string, getTextTitle getTextTitleFunc) sx.Object {
	text := sx.MakeString(val)
	zid, err := id.Parse(val)
	if err != nil {
		html.Escape(w, val)
		return
		return text
	}
	title, found := getTextTitle(zid)
	switch {
	case found > 0:
		ub := wui.NewURLBuilder('h').SetZid(api.ZettelID(zid.String()))
		if title == "" {
		ub := wui.NewURLBuilder('h').SetZid(zid.ZettelID())
		attrs := sx.Nil()
		if title != "" {
			fmt.Fprintf(w, "<a href=\"%v\">%v</a>", ub, zid)
		} else {
			attrs = attrs.Cons(sx.Cons(shtml.SymAttrTitle, sx.MakeString(title)))
			fmt.Fprintf(w, "<a href=\"%v\" title=\"%v\">%v</a>", ub, title, zid)
		}
		attrs = attrs.Cons(sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String()))).Cons(sxhtml.SymAttr)
		return sx.Nil().Cons(sx.MakeString(zid.String())).Cons(attrs).Cons(shtml.SymA)
	case found == 0:
		fmt.Fprintf(w, "<s>%v</s>", val)
	case found < 0:
		io.WriteString(w, val)
		return sx.MakeList(sx.MakeSymbol("s"), text)
	default: // case found < 0:
		return text
	}
}

func (wui *WebUI) writeIdentifierSet(w io.Writer, vals []string, getTextTitle getTextTitleFunc) {
func (wui *WebUI) transformIdentifierSet(vals []string, getTextTitle getTextTitleFunc) *sx.Pair {
	for i, val := range vals {
		if i > 0 {
	if len(vals) == 0 {
			w.Write(space)
		}
		wui.writeIdentifier(w, val, getTextTitle)
	}
}

		return nil
func (wui *WebUI) writeNumber(w io.Writer, key, val string) {
	wui.writeLink(w, key, val, val)
}

	}
	var space = sx.MakeString(" ")
func writeString(w io.Writer, val string) { html.Escape(w, val) }

	text := make(sx.Vector, 0, 2*len(vals))
func (wui *WebUI) writeTagSet(w io.Writer, key string, tags []string) {
	for i, tag := range tags {
	for _, val := range vals {
		if i > 0 {
			w.Write(space)
		}
		text = append(text, space, wui.transformIdentifier(val, getTextTitle))
	}
		wui.writeLink(w, key, tag, tag)
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
}
}

func writeTimestamp(w io.Writer, ts time.Time) {
	io.WriteString(w, `<time datetime="`)
	io.WriteString(w, ts.Format("2006-01-02T15:04:05"))
	io.WriteString(w, `">`)
	io.WriteString(w, ts.Format("2006-01-02&nbsp;15:04:05"))
	io.WriteString(w, `</time>`)
}

func writeURL(w io.Writer, val string) {
	u, err := url.Parse(val)
	if err != nil {

func (wui *WebUI) transformTagSet(key string, tags []string) *sx.Pair {
	if len(tags) == 0 {
		return nil
	}
	var space = sx.MakeString(" ")
	text := make(sx.Vector, 0, 2*len(tags)+2)
	for _, tag := range tags {
		html.Escape(w, val)
		return
		text = append(text, space, wui.transformKeyValueText(key, tag, tag))
	}
	if us := u.String(); us != "" {
		io.WriteString(w, `<a href="`)
	if len(tags) > 1 {
		text = append(text, space, wui.transformKeyValuesText(key, tags, "(all)"))
		html.AttributeEscape(w, us)
		io.WriteString(w, `" target="_blank" rel="noopener noreferrer">`)
		html.Escape(w, val)
		io.WriteString(w, "</a>")
	}
	return sx.MakeList(text[1:]...).Cons(shtml.SymSPAN)
}

func (wui *WebUI) writeWord(w io.Writer, key, word string) {
func (wui *WebUI) transformKeyValueText(key, value, text string) *sx.Pair {
	wui.writeLink(w, key, word, word)
	ub := wui.NewURLBuilder('h').AppendQuery(key + api.SearchOperatorHas + value)
	return buildHref(ub, text)
}

func (wui *WebUI) writeWordSet(w io.Writer, key string, words []string) {
	for i, word := range words {
func (wui *WebUI) transformKeyValuesText(key string, values []string, text string) *sx.Pair {
	ub := wui.NewURLBuilder('h')
	for _, val := range values {
		if i > 0 {
			w.Write(space)
		}
		wui.writeWord(w, key, word)
	}
}

func (wui *WebUI) writeLink(w io.Writer, key, value, text string) {
		ub = ub.AppendQuery(key + api.SearchOperatorHas + val)
	}
	return buildHref(ub, text)
}

func buildHref(ub *api.URLBuilder, text string) *sx.Pair {
	return sx.MakeList(
	fmt.Fprintf(w, `<a href="%v">`, wui.NewURLBuilder('h').AppendQuery(key+api.SearchOperatorHas+value))
	html.Escape(w, text)
	io.WriteString(w, "</a>")
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
}

type getMetadataFunc func(id.Zid) (*meta.Meta, error)

			sx.Cons(shtml.SymAttrHref, sx.MakeString(ub.String())),
		),
		sx.MakeString(text),
	)
func createGetMetadataFunc(ctx context.Context, getMeta usecase.GetMeta) getMetadataFunc {
	return func(zid id.Zid) (*meta.Meta, error) { return getMeta.Run(box.NoEnrichContext(ctx), zid) }
}

type evalMetadataFunc = func(string) ast.InlineSlice

func createEvalMetadataFunc(ctx context.Context, evaluate *usecase.Evaluate) evalMetadataFunc {
	return func(value string) ast.InlineSlice { return evaluate.RunMetadata(ctx, value) }
}

type getTextTitleFunc func(id.Zid) (string, int)

func (wui *WebUI) makeGetTextTitle(getMetadata getMetadataFunc, evalMetadata evalMetadataFunc) getTextTitleFunc {
func (wui *WebUI) makeGetTextTitle(ctx context.Context, getZettel usecase.GetZettel) getTextTitleFunc {
	return func(zid id.Zid) (string, int) {
		m, err := getMetadata(zid)
		z, err := getZettel.Run(box.NoEnrichContext(ctx), zid)
		if err != nil {
			if errors.Is(err, &box.ErrNotAllowed{}) {
				return "", -1
			}
			return "", 0
		}
		return encodeEvaluatedTitleText(m, evalMetadata, wui.gentext), 1
		return parser.NormalizedSpacedText(z.Meta.GetTitle()), 1
	}
}

func encodeZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
func (wui *WebUI) transformZmkMetadata(value string, evalMetadata evalMetadataFunc, gen *htmlGenerator) sx.Object {
	is := evalMetadata(value)
	return gen.InlinesString(&is)
	return gen.InlinesSxHTML(&is).Cons(shtml.SymSPAN)
}

Changes to web/adapter/webui/lists.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
1

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

-
+






+
+
+





-

-


+
+


-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+

+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
+
+
+
+

-
+




-
+
-
-
-
-
+
-
-
-
-
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
+
+
+
-
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
+
-
-
-
+
+
+
-
-
+
-
-
-
-
+
-
-
+
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+

-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+

+


-
-
-
-
+
+
+
+

+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
-
-
+
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2023 Detlef Stern
// 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 webui

import (
	"bytes"
	"context"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoding/atom"
	"zettelstore.de/z/encoding/rss"
	"zettelstore.de/z/encoding/xml"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/query"
	"t73f.de/r/sx"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/ast"
	"zettelstore.de/z/evaluator"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(
	queryMeta *usecase.Query,
	tagZettel *usecase.TagZettel,
	roleZettel *usecase.RoleZettel,

	reIndex *usecase.ReIndex) http.Handler {
// MakeListHTMLMetaHandler creates a HTTP handler for rendering the list of zettel as HTML.
func (wui *WebUI) MakeListHTMLMetaHandler(listMeta usecase.ListMeta, evaluate *usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		q := adapter.GetQuery(r.URL.Query())
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		urlQuery := r.URL.Query()
		if wui.handleTagZettel(w, r, tagZettel, urlQuery) ||
			wui.handleRoleZettel(w, r, roleZettel, urlQuery) {
			return
		}
		q := adapter.GetQuery(urlQuery)
		q = q.SetDeterministic()
		ctx := r.Context()
		metaList, err := listMeta.Run(box.NoEnrichQuery(ctx, q), q)
		metaSeq, err := queryMeta.Run(ctx, q)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if actions := q.Actions(); len(actions) > 0 {
		actions, err := adapter.TryReIndex(ctx, q.Actions(), metaSeq, reIndex)
			switch actions[0] {
			case "ATOM":
				wui.renderAtom(w, q, metaList)
				return
		if err != nil {
			case "RSS":
				wui.renderRSS(ctx, w, q, metaList)
				return
			}
			wui.reportError(ctx, w, err)
			return
		}
		}
		bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, q, metaList, wui.rtConfig))
		enc := wui.getSimpleHTMLEncoder()
		if len(actions) > 0 && len(metaSeq) > 0 {
			for _, act := range actions {
				if act == api.RedirectAction {
					ub := wui.NewURLBuilder('h').SetZid(metaSeq[0].Zid.ZettelID())
		htmlContent, err := enc.BlocksString(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		seed, found := q.GetSeed()
		if !found {
			seed = 0
		}
					wui.redirectFound(w, r, ub)
					return
				}
			}
		}

		userLang := wui.getUserLang(ctx)

		var content, endnotes *sx.Pair
		numEntries := 0
		if bn, cnt := evaluator.QueryAction(ctx, q, metaSeq); bn != nil {
			enc := wui.getSimpleHTMLEncoder(userLang)
			content, endnotes, err = enc.BlocksSxn(&ast.BlockSlice{bn})
			if err != nil {
				wui.reportError(ctx, w, err)
				return
			}
			numEntries = cnt
		}

		siteName := wui.rtConfig.GetSiteName()
		user := server.GetUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base)
		wui.renderTemplate(ctx, w, id.ListTemplateZid, &base, struct {
			Title         string
		env, rb := wui.createRenderEnv(ctx, "list", userLang, siteName, user)
		if q == nil {
			rb.bindString("heading", sx.MakeString(siteName))
			SearchURL     string
			QueryValue    string
			QueryKeyQuery string
			Content       string
		} else {
			var sb strings.Builder
			q.PrintHuman(&sb)
			CanCreate     bool
			CreateURL     string
			QueryKeySeed  string
			rb.bindString("heading", sx.MakeString(sb.String()))
		}
		rb.bindString("query-value", sx.MakeString(q.String()))
			Seed          int
		}{
			Title:         wui.listTitleQuery(q),
		if tzl := q.GetMetaValues(api.KeyTags, false); len(tzl) > 0 {
			sxTzl, sxNoTzl := wui.transformTagZettelList(ctx, tagZettel, tzl)
			SearchURL:     base.SearchURL,
			QueryValue:    q.String(),
			if !sx.IsNil(sxTzl) {
				rb.bindString("tag-zettel", sxTzl)
			QueryKeyQuery: base.QueryKeyQuery,
			Content:       htmlContent,
			CanCreate:     wui.canCreate(ctx, user),
			CreateURL:     base.CreateNewURL,
			}
			if !sx.IsNil(sxNoTzl) && wui.canCreate(ctx, user) {
				rb.bindString("create-tag-zettel", sxNoTzl)
			QueryKeySeed:  base.QueryKeySeed,
			Seed:          seed,
		})
	}
}

			}
		}
		if rzl := q.GetMetaValues(api.KeyRole, false); len(rzl) > 0 {
func (wui *WebUI) renderRSS(ctx context.Context, w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var rssConfig rss.Configuration
	rssConfig.Setup(ctx, wui.rtConfig)
			sxRzl, sxNoRzl := wui.transformRoleZettelList(ctx, roleZettel, rzl)
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		rssConfig.Title = strings.Join(actions[2:], " ")
	}
			if !sx.IsNil(sxRzl) {
				rb.bindString("role-zettel", sxRzl)
			}
	data := rssConfig.Marshal(q, ml)

			if !sx.IsNil(sxNoRzl) && wui.canCreate(ctx, user) {
	adapter.PrepareHeader(w, rss.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
				rb.bindString("create-role-zettel", sxNoRzl)
		_, err = w.Write(data)
	}
			}
	if err != nil {
		wui.log.IfErr(err).Msg("unable to write RSS data")
	}
		}
}

		rb.bindString("content", content)
		rb.bindString("endnotes", endnotes)
		rb.bindString("num-entries", sx.Int64(numEntries))
		rb.bindString("num-meta", sx.Int64(len(metaSeq)))
		apiURL := wui.NewURLBuilder('z').AppendQuery(q.String())
		seed, found := q.GetSeed()
		if found {
			apiURL = apiURL.AppendKVQuery(api.QueryKeySeed, strconv.Itoa(seed))
		} else {
			seed = 0
		}
func (wui *WebUI) renderAtom(w http.ResponseWriter, q *query.Query, ml []*meta.Meta) {
	var atomConfig atom.Configuration
	atomConfig.Setup(wui.rtConfig)
		if len(metaSeq) > 0 {
			rb.bindString("plain-url", sx.MakeString(apiURL.String()))
			rb.bindString("data-url", sx.MakeString(apiURL.AppendKVQuery(api.QueryKeyEncoding, api.EncodingData).String()))
			if wui.canCreate(ctx, user) {
	if actions := q.Actions(); len(actions) > 2 && actions[1] == "TITLE" {
		atomConfig.Title = strings.Join(actions[2:], " ")
	}
				rb.bindString("create-url", sx.MakeString(wui.createNewURL))
				rb.bindString("seed", sx.Int64(seed))
			}
	data := atomConfig.Marshal(q, ml)

		}
	adapter.PrepareHeader(w, atom.ContentType)
	w.WriteHeader(http.StatusOK)
	var err error
	if _, err = io.WriteString(w, xml.Header); err == nil {
		_, err = w.Write(data)
	}
	if err != nil {
		wui.log.IfErr(err).Msg("unable to write Atom data")
	}
		if rb.err == nil {
			err = wui.renderSxnTemplate(ctx, w, id.ListTemplateZid, env)
		} else {
			err = rb.err
		}
		if err != nil {
			wui.reportError(ctx, w, err)
		}
}

// MakeZettelContextHandler creates a new HTTP handler for the use case "zettel context".
func (wui *WebUI) MakeZettelContextHandler(getContext usecase.ZettelContext, evaluate *usecase.Evaluate) http.HandlerFunc {
	})
}

func (wui *WebUI) transformTagZettelList(ctx context.Context, tagZettel *usecase.TagZettel, tags []string) (withZettel, withoutZettel *sx.Pair) {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
	slices.Reverse(tags)
	for _, tag := range tags {
		tag = meta.NormalizeTag(tag)
		if _, err := tagZettel.Run(ctx, tag); err == nil {
			u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyTag, tag)
			withZettel = wui.prependZettelLink(withZettel, tag, u)
		} else {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewTag).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, tag)
			withoutZettel = wui.prependZettelLink(withoutZettel, tag, u)
		}
		q := r.URL.Query()
		dir := adapter.GetZCDirection(q.Get(api.QueryKeyDir))
		cost := getIntParameter(q, api.QueryKeyCost, 17)
		limit := getIntParameter(q, api.QueryKeyLimit, 200)
		metaList, err := getContext.Run(ctx, zid, dir, cost, limit)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
	}
		bns := evaluate.RunBlockNode(ctx, evaluator.QueryAction(ctx, nil, metaList, wui.rtConfig))
		enc := wui.getSimpleHTMLEncoder()
		htmlContent, err := enc.BlocksString(&bns)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		apiZid := api.ZettelID(zid.String())
	return withZettel, withoutZettel
}

		costs := []string{"2", "3", "5", "8", "13", "21", "34", "55", "89"}
		costLinks := make([]simpleLink, len(costs))
		costURL := wui.NewURLBuilder('k').SetZid(apiZid)
func (wui *WebUI) transformRoleZettelList(ctx context.Context, roleZettel *usecase.RoleZettel, roles []string) (withZettel, withoutZettel *sx.Pair) {
	slices.Reverse(roles)
	for _, role := range roles {
		if _, err := roleZettel.Run(ctx, role); err == nil {
			u := wui.NewURLBuilder('h').AppendKVQuery(api.QueryKeyRole, role)
			withZettel = wui.prependZettelLink(withZettel, role, u)
		} else {
			u := wui.NewURLBuilder('c').SetZid(api.ZidTemplateNewRole).AppendKVQuery(queryKeyAction, valueActionNew).AppendKVQuery(api.KeyTitle, role)
		for i, cost := range costs {
			costURL.ClearQuery()
			switch dir {
			case usecase.ZettelContextBackward:
				costURL.AppendKVQuery(api.QueryKeyDir, api.DirBackward)
			case usecase.ZettelContextForward:
			withoutZettel = wui.prependZettelLink(withoutZettel, role, u)
				costURL.AppendKVQuery(api.QueryKeyDir, api.DirForward)
			}
		}
			costURL.AppendKVQuery(api.QueryKeyCost, cost)
			costLinks[i].Text = cost
			costLinks[i].URL = costURL.String()
		}
		var base baseData
		user := server.GetUser(ctx)
	}
	return withZettel, withoutZettel
}

func (wui *WebUI) prependZettelLink(sxZtl *sx.Pair, name string, u *api.URLBuilder) *sx.Pair {
	link := sx.MakeList(
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), wui.rtConfig.GetSiteName(), "", user, &base)
		wui.renderTemplate(ctx, w, id.ContextTemplateZid, &base, struct {
			Title   string
			InfoURL string
			Costs   []simpleLink
			Content string
		shtml.SymA,
		sx.MakeList(
			sxhtml.SymAttr,
		}{
			Title:   "Zettel Context",
			InfoURL: wui.NewURLBuilder('i').SetZid(apiZid).String(),
			Costs:   costLinks,
			Content: htmlContent,
		})
			sx.Cons(shtml.SymAttrHref, sx.MakeString(u.String())),
		),
		sx.MakeString(name),
	)
	if sxZtl != nil {
		sxZtl = sxZtl.Cons(sx.MakeString(", "))
	}
	return sxZtl.Cons(link)
}

func getIntParameter(q url.Values, key string, minValue int) int {
	val, ok := adapter.GetInteger(q, key)
	if !ok || val < 0 {
		return minValue
func (wui *WebUI) handleTagZettel(w http.ResponseWriter, r *http.Request, tagZettel *usecase.TagZettel, vals url.Values) bool {
	tag := vals.Get(api.QueryKeyTag)
	if tag == "" {
		return false
	}
	ctx := r.Context()
	z, err := tagZettel.Run(ctx, tag)
	if err != nil {
		wui.reportError(ctx, w, err)
	return val
}

func (wui *WebUI) listTitleQuery(q *query.Query) string {
	if q == nil {
		return wui.rtConfig.GetSiteName()
		return true
	}
	wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID()))
	return true
}

func (wui *WebUI) handleRoleZettel(w http.ResponseWriter, r *http.Request, roleZettel *usecase.RoleZettel, vals url.Values) bool {
	role := vals.Get(api.QueryKeyRole)
	if role == "" {
		return false
	}
	ctx := r.Context()
	z, err := roleZettel.Run(ctx, role)
	if err != nil {
		wui.reportError(ctx, w, err)
		return true
	}
	var buf bytes.Buffer
	q.PrintHuman(&buf)
	return buf.String()
	wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(z.Meta.Zid.ZettelID()))
	return true
}

Changes to web/adapter/webui/login.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
1

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

-
+






+
+
+








-
+

-
-
-
+
+
+




-
-
+
+







-
+



-
-
-
+
+
+
+
-
-
-
-
-
+
+
-
+
+



-
-
+
+










-
+











-
+

//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 webui

import (
	"context"
	"net/http"

	"zettelstore.de/c/api"
	"t73f.de/r/sx"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/zettel/id"
)

// MakeGetLoginOutHandler creates a new HTTP handler to display the HTML login view,
// or to execute a logout.
func (wui *WebUI) MakeGetLoginOutHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakeGetLoginOutHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		query := r.URL.Query()
		if query.Has("logout") {
			wui.clearToken(r.Context(), w)
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		wui.renderLoginForm(wui.clearToken(r.Context(), w), w, false)
	}
	})
}

func (wui *WebUI) renderLoginForm(ctx context.Context, w http.ResponseWriter, retry bool) {
	var base baseData
	wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, nil, api.KeyLang), "Login", "", nil, &base)
	wui.renderTemplate(ctx, w, id.LoginTemplateZid, &base, struct {
	env, rb := wui.createRenderEnv(ctx, "login", wui.getUserLang(ctx), "Login", nil)
	rb.bindString("retry", sx.MakeBoolean(retry))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplate(ctx, w, id.LoginTemplateZid, env)
		Title string
		Retry bool
	}{
		Title: base.Title,
		Retry: retry,
	}
	if err := rb.err; err != nil {
	})
		wui.reportError(ctx, w, err)
	}
}

// MakePostLoginHandler creates a new HTTP handler to authenticate the given user.
func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
func (wui *WebUI) MakePostLoginHandler(ucAuth *usecase.Authenticate) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !wui.authz.WithAuth() {
			wui.redirectFound(w, r, wui.NewURLBuilder('/'))
			return
		}
		ctx := r.Context()
		ident, cred, ok := adapter.GetCredentialsViaForm(r)
		if !ok {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read login form"))
			return
		}
		token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindHTML)
		token, err := ucAuth.Run(ctx, r, ident, cred, wui.tokenLifetime, auth.KindwebUI)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		if token == nil {
			wui.renderLoginForm(wui.clearToken(ctx, w), w, true)
			return
		}

		wui.setToken(w, token)
		wui.redirectFound(w, r, wui.NewURLBuilder('/'))
	}
	})
}

Added web/adapter/webui/meta.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 webui

import (
	"strings"
	"unicode"
	"unicode/utf8"
)

func capitalizeMetaKey(key string) string {
	var sb strings.Builder
	for i, word := range strings.Split(key, "-") {
		if i > 0 {
			sb.WriteByte(' ')
		}
		if newWord, isSpecial := specialWords[word]; isSpecial {
			if newWord == "" {
				sb.WriteString(strings.ToTitle(word))
			} else {
				sb.WriteString(newWord)
			}
			continue
		}
		r, size := utf8.DecodeRuneInString(word)
		if r == utf8.RuneError {
			sb.WriteString(word)
			continue
		}
		sb.WriteRune(unicode.ToTitle(r))
		sb.WriteString(word[size:])
	}
	return sb.String()
}

var specialWords = map[string]string{
	"css":    "",
	"html":   "",
	"github": "GitHub",
	"http":   "",
	"https":  "",
	"pdf":    "",
	"svg":    "",
	"url":    "",
}

Added web/adapter/webui/meta_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 webui

import "testing"

func TestCapitalizeMetaKey(t *testing.T) {
	var testcases = []struct {
		key string
		exp string
	}{
		{"", ""},
		{"alt-url", "Alt URL"},
		{"author", "Author"},
		{"back", "Back"},
		{"box-number", "Box Number"},
		{"cite-key", "Cite Key"},
		{"fedi-url", "Fedi URL"},
		{"github-url", "GitHub URL"},
		{"hshn-bib", "Hshn Bib"},
		{"job-url", "Job URL"},
		{"new-user-id", "New User Id"},
		{"origin-zid", "Origin Zid"},
		{"site-url", "Site URL"},
	}
	for _, tc := range testcases {
		t.Run(tc.key, func(t *testing.T) {
			got := capitalizeMetaKey(tc.key)
			if got != tc.exp {
				t.Errorf("capitalize(%q) == %q, but got %q", tc.key, tc.exp, got)
			}
		})
	}
}

Deleted web/adapter/webui/rename_zettel.go.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99



































































































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

import (
	"fmt"
	"net/http"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
)

// MakeGetRenameZettelHandler creates a new HTTP handler to display the
// HTML rename view of a zettel.
func (wui *WebUI) MakeGetRenameZettelHandler(getMeta usecase.GetMeta, evaluate *usecase.Evaluate) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		zid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		m, err := getMeta.Run(ctx, zid)
		if err != nil {
			wui.reportError(ctx, w, err)
			return
		}

		getTextTitle := wui.makeGetTextTitle(createGetMetadataFunc(ctx, getMeta), createEvalMetadataFunc(ctx, evaluate))
		uselessFiles := retrieveUselessFiles(m)

		user := server.GetUser(ctx)
		var base baseData
		wui.makeBaseData(ctx, wui.rtConfig.Get(ctx, m, api.KeyLang), "Rename Zettel "+zid.String(), "", user, &base)
		wui.renderTemplate(ctx, w, id.RenameTemplateZid, &base, struct {
			Zid             string
			MetaPairs       []meta.Pair
			Incoming        simpleLinks
			HasUselessFiles bool
			UselessFiles    []string
		}{
			Zid:             zid.String(),
			MetaPairs:       m.ComputedPairs(),
			Incoming:        wui.encodeIncoming(m, getTextTitle),
			HasUselessFiles: len(uselessFiles) > 0,
			UselessFiles:    uselessFiles,
		})
	}
}

// MakePostRenameZettelHandler creates a new HTTP handler to rename an existing zettel.
func (wui *WebUI) MakePostRenameZettelHandler(renameZettel *usecase.RenameZettel) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		curZid, err := id.Parse(r.URL.Path[1:])
		if err != nil {
			wui.reportError(ctx, w, box.ErrNotFound)
			return
		}

		if err = r.ParseForm(); err != nil {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Unable to read rename zettel form"))
			return
		}
		if formCurZid, err1 := id.Parse(
			r.PostFormValue("curzid")); err1 != nil || formCurZid != curZid {
			wui.reportError(ctx, w, adapter.NewErrBadRequest("Invalid value for current zettel id in form"))
			return
		}
		formNewZid := strings.TrimSpace(r.PostFormValue("newzid"))
		newZid, err := id.Parse(formNewZid)
		if err != nil {
			wui.reportError(
				ctx, w, adapter.NewErrBadRequest(fmt.Sprintf("Invalid new zettel id %q", formNewZid)))
			return
		}

		if err = renameZettel.Run(r.Context(), curZid, newZid); err != nil {
			wui.reportError(ctx, w, err)
			return
		}
		wui.redirectFound(w, r, wui.NewURLBuilder('h').SetZid(api.ZettelID(newZid.String())))
	}
}

Changes to web/adapter/webui/response.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
16

17
18
19
20
21
22
23
1

2
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) 2021-2022 Detlef Stern
// 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 webui

import (
	"net/http"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
)

func (wui *WebUI) redirectFound(w http.ResponseWriter, r *http.Request, ub *api.URLBuilder) {
	us := ub.String()
	wui.log.Debug().Str("uri", us).Msg("redirect")
	http.Redirect(w, r, us, http.StatusFound)
}

Added web/adapter/webui/sxn_code.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 webui

import (
	"context"
	"fmt"
	"io"

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

func (wui *WebUI) loadAllSxnCodeZettel(ctx context.Context) (id.Digraph, *sxeval.Binding, error) {
	// getMeta MUST currently use GetZettel, because GetMeta just uses the
	// Index, which might not be current.
	getMeta := func(ctx context.Context, zid id.Zid) (*meta.Meta, error) {
		z, err := wui.box.GetZettel(ctx, zid)
		if err != nil {
			return nil, err
		}
		return z.Meta, nil
	}
	dg := buildSxnCodeDigraph(ctx, id.StartSxnZid, getMeta)
	if dg == nil {
		return nil, wui.rootBinding, nil
	}
	dg = dg.AddVertex(id.BaseSxnZid).AddEdge(id.StartSxnZid, id.BaseSxnZid)
	dg = dg.AddVertex(id.PreludeSxnZid).AddEdge(id.BaseSxnZid, id.PreludeSxnZid)
	dg = dg.TransitiveClosure(id.StartSxnZid)

	if zid, isDAG := dg.IsDAG(); !isDAG {
		return nil, nil, fmt.Errorf("zettel %v is part of a dependency cycle", zid)
	}
	bind := wui.rootBinding.MakeChildBinding("zettel", 128)
	for _, zid := range dg.SortReverse() {
		if err := wui.loadSxnCodeZettel(ctx, zid, bind); err != nil {
			return nil, nil, err
		}
	}
	return dg, bind, nil
}

type getMetaFunc func(context.Context, id.Zid) (*meta.Meta, error)

func buildSxnCodeDigraph(ctx context.Context, startZid id.Zid, getMeta getMetaFunc) id.Digraph {
	m, err := getMeta(ctx, startZid)
	if err != nil {
		return nil
	}
	var marked *id.Set
	stack := []*meta.Meta{m}
	dg := id.Digraph(nil).AddVertex(startZid)
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr.Zid) {
			continue
		}
		marked = marked.Add(curr.Zid)
		if precursors, hasPrecursor := curr.GetList(api.KeyPrecursor); hasPrecursor && len(precursors) > 0 {
			for _, pre := range precursors {
				if preZid, errParse := id.Parse(pre); errParse == nil {
					m, err = getMeta(ctx, preZid)
					if err != nil {
						continue
					}
					stack = append(stack, m)
					dg.AddVertex(preZid)
					dg.AddEdge(curr.Zid, preZid)
				}
			}
		}
	}
	return dg
}

func (wui *WebUI) loadSxnCodeZettel(ctx context.Context, zid id.Zid, bind *sxeval.Binding) error {
	rdr, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return err
	}
	env := sxeval.MakeExecutionEnvironment(bind)
	for {
		form, err2 := rdr.Read()
		if err2 != nil {
			if err2 == io.EOF {
				return nil
			}
			return err2
		}
		wui.log.Debug().Zid(zid).Str("form", form.String()).Msg("Loaded sxn code")

		if _, err2 = env.Eval(form); err2 != nil {
			return err2
		}
	}
}

Added web/adapter/webui/template.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 webui

import (
	"bytes"
	"context"
	"fmt"
	"net/http"
	"net/url"

	"t73f.de/r/sx"
	"t73f.de/r/sx/sxbuiltins"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sx/sxreader"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"t73f.de/r/zsc/shtml"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

func (wui *WebUI) createRenderBinding() *sxeval.Binding {
	root := sxeval.MakeRootBinding(len(specials) + len(builtins) + 3)
	for _, syntax := range specials {
		root.BindSpecial(syntax)
	}
	for _, b := range builtins {
		root.BindBuiltin(b)
	}
	_ = root.Bind(sx.MakeSymbol("NIL"), sx.Nil())
	_ = root.Bind(sx.MakeSymbol("T"), sx.MakeSymbol("T"))
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "url-to-html",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			text, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			return wui.url2html(text), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "zid-content-path",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			s, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			zid, err := id.Parse(s.GetValue())
			if err != nil {
				return nil, fmt.Errorf("parsing zettel identifier %q: %w", s.GetValue(), err)
			}
			ub := wui.NewURLBuilder('z').SetZid(zid.ZettelID())
			return sx.MakeString(ub.String()), nil
		},
	})
	root.BindBuiltin(&sxeval.Builtin{
		Name:     "query->url",
		MinArity: 1,
		MaxArity: 1,
		TestPure: sxeval.AssertPure,
		Fn1: func(_ *sxeval.Environment, arg sx.Object) (sx.Object, error) {
			qs, err := sxbuiltins.GetString(arg, 0)
			if err != nil {
				return nil, err
			}
			u := wui.NewURLBuilder('h').AppendQuery(qs.GetValue())
			return sx.MakeString(u.String()), nil
		},
	})
	root.Freeze()
	return root
}

var (
	specials = []*sxeval.Special{
		&sxbuiltins.QuoteS, &sxbuiltins.QuasiquoteS, // quote, quasiquote
		&sxbuiltins.UnquoteS, &sxbuiltins.UnquoteSplicingS, // unquote, unquote-splicing
		&sxbuiltins.DefVarS,                     // defvar
		&sxbuiltins.DefunS, &sxbuiltins.LambdaS, // defun, lambda
		&sxbuiltins.SetXS,     // set!
		&sxbuiltins.IfS,       // if
		&sxbuiltins.BeginS,    // begin
		&sxbuiltins.DefMacroS, // defmacro
		&sxbuiltins.LetS,      // let
	}
	builtins = []*sxeval.Builtin{
		&sxbuiltins.Equal,                // =
		&sxbuiltins.NumGreater,           // >
		&sxbuiltins.NullP,                // null?
		&sxbuiltins.PairP,                // pair?
		&sxbuiltins.Car, &sxbuiltins.Cdr, // car, cdr
		&sxbuiltins.Caar, &sxbuiltins.Cadr, &sxbuiltins.Cdar, &sxbuiltins.Cddr,
		&sxbuiltins.Caaar, &sxbuiltins.Caadr, &sxbuiltins.Cadar, &sxbuiltins.Caddr,
		&sxbuiltins.Cdaar, &sxbuiltins.Cdadr, &sxbuiltins.Cddar, &sxbuiltins.Cdddr,
		&sxbuiltins.List,           // list
		&sxbuiltins.Append,         // append
		&sxbuiltins.Assoc,          // assoc
		&sxbuiltins.Map,            // map
		&sxbuiltins.Apply,          // apply
		&sxbuiltins.Concat,         // concat
		&sxbuiltins.BoundP,         // bound?
		&sxbuiltins.Defined,        // defined?
		&sxbuiltins.CurrentBinding, // current-binding
		&sxbuiltins.BindingLookup,  // binding-lookup
	}
)

func (wui *WebUI) url2html(text sx.String) sx.Object {
	if u, errURL := url.Parse(text.GetValue()); errURL == nil {
		if us := u.String(); us != "" {
			return sx.MakeList(
				shtml.SymA,
				sx.MakeList(
					sxhtml.SymAttr,
					sx.Cons(shtml.SymAttrHref, sx.MakeString(us)),
					sx.Cons(shtml.SymAttrTarget, sx.MakeString("_blank")),
					sx.Cons(shtml.SymAttrRel, sx.MakeString("external noreferrer")),
				),
				text)
		}
	}
	return text
}

func (wui *WebUI) getParentEnv(ctx context.Context) (*sxeval.Binding, error) {
	wui.mxZettelBinding.Lock()
	defer wui.mxZettelBinding.Unlock()
	if parentEnv := wui.zettelBinding; parentEnv != nil {
		return parentEnv, nil
	}
	dag, zettelEnv, err := wui.loadAllSxnCodeZettel(ctx)
	if err != nil {
		wui.log.Error().Err(err).Msg("loading zettel sxn")
		return nil, err
	}
	wui.dag = dag
	wui.zettelBinding = zettelEnv
	return zettelEnv, nil
}

// createRenderEnv creates a new environment and populates it with all relevant data for the base template.
func (wui *WebUI) createRenderEnv(ctx context.Context, name, lang, title string, user *meta.Meta) (*sxeval.Binding, renderBinder) {
	userIsValid, userZettelURL, userIdent := wui.getUserRenderData(user)
	parentEnv, err := wui.getParentEnv(ctx)
	bind := parentEnv.MakeChildBinding(name, 128)
	rb := makeRenderBinder(bind, err)
	rb.bindString("lang", sx.MakeString(lang))
	rb.bindString("css-base-url", sx.MakeString(wui.cssBaseURL))
	rb.bindString("css-user-url", sx.MakeString(wui.cssUserURL))
	rb.bindString("title", sx.MakeString(title))
	rb.bindString("home-url", sx.MakeString(wui.homeURL))
	rb.bindString("with-auth", sx.MakeBoolean(wui.withAuth))
	rb.bindString("user-is-valid", sx.MakeBoolean(userIsValid))
	rb.bindString("user-zettel-url", sx.MakeString(userZettelURL))
	rb.bindString("user-ident", sx.MakeString(userIdent))
	rb.bindString("login-url", sx.MakeString(wui.loginURL))
	rb.bindString("logout-url", sx.MakeString(wui.logoutURL))
	rb.bindString("list-zettel-url", sx.MakeString(wui.listZettelURL))
	rb.bindString("list-roles-url", sx.MakeString(wui.listRolesURL))
	rb.bindString("list-tags-url", sx.MakeString(wui.listTagsURL))
	if wui.canRefresh(user) {
		rb.bindString("refresh-url", sx.MakeString(wui.refreshURL))
	}
	rb.bindString("new-zettel-links", wui.fetchNewTemplatesSxn(ctx, user))
	rb.bindString("search-url", sx.MakeString(wui.searchURL))
	rb.bindString("query-key-query", sx.MakeString(api.QueryKeyQuery))
	rb.bindString("query-key-seed", sx.MakeString(api.QueryKeySeed))
	rb.bindString("FOOTER", wui.calculateFooterSxn(ctx)) // TODO: use real footer
	rb.bindString("debug-mode", sx.MakeBoolean(wui.debug))
	rb.bindSymbol(symMetaHeader, sx.Nil())
	rb.bindSymbol(symDetail, sx.Nil())
	return bind, rb
}

func (wui *WebUI) getUserRenderData(user *meta.Meta) (bool, string, string) {
	if user == nil {
		return false, "", ""
	}
	return true, wui.NewURLBuilder('h').SetZid(user.Zid.ZettelID()).String(), user.GetDefault(api.KeyUserID, "")
}

type renderBinder struct {
	err     error
	binding *sxeval.Binding
}

func makeRenderBinder(bind *sxeval.Binding, err error) renderBinder {
	return renderBinder{binding: bind, err: err}
}
func (rb *renderBinder) bindString(key string, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.binding.Bind(sx.MakeSymbol(key), obj)
	}
}
func (rb *renderBinder) bindSymbol(sym *sx.Symbol, obj sx.Object) {
	if rb.err == nil {
		rb.err = rb.binding.Bind(sym, obj)
	}
}
func (rb *renderBinder) bindKeyValue(key string, value string) {
	rb.bindString("meta-"+key, sx.MakeString(value))
	if kt := meta.Type(key); kt.IsSet {
		rb.bindString("set-meta-"+key, makeStringList(meta.ListFromValue(value)))
	}
}
func (rb *renderBinder) rebindResolved(key, defKey string) {
	if rb.err == nil {
		if obj, found := rb.binding.Resolve(sx.MakeSymbol(key)); found {
			rb.bindString(defKey, obj)
		}
	}
}

func (wui *WebUI) bindCommonZettelData(ctx context.Context, rb *renderBinder, user, m *meta.Meta, content *zettel.Content) {
	strZid := m.Zid.String()
	apiZid := api.ZettelID(strZid)
	newURLBuilder := wui.NewURLBuilder

	rb.bindString("zid", sx.MakeString(strZid))
	rb.bindString("web-url", sx.MakeString(newURLBuilder('h').SetZid(apiZid).String()))
	if content != nil && wui.canWrite(ctx, user, m, *content) {
		rb.bindString("edit-url", sx.MakeString(newURLBuilder('e').SetZid(apiZid).String()))
	}
	rb.bindString("info-url", sx.MakeString(newURLBuilder('i').SetZid(apiZid).String()))
	if wui.canCreate(ctx, user) {
		if content != nil && !content.IsBinary() {
			rb.bindString("copy-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionCopy).String()))
		}
		rb.bindString("version-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionVersion).String()))
		rb.bindString("sequel-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionSequel).String()))
		rb.bindString("folge-url", sx.MakeString(newURLBuilder('c').SetZid(apiZid).AppendKVQuery(queryKeyAction, valueActionFolge).String()))
	}
	if wui.canDelete(ctx, user, m) {
		rb.bindString("delete-url", sx.MakeString(newURLBuilder('d').SetZid(apiZid).String()))
	}
	if val, found := m.Get(api.KeyUselessFiles); found {
		rb.bindString("useless", sx.Cons(sx.MakeString(val), nil))
	}
	queryContext := strZid + " " + api.ContextDirective
	rb.bindString("context-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String()))
	queryContext += " " + api.FullDirective
	rb.bindString("context-full-url", sx.MakeString(newURLBuilder('h').AppendQuery(queryContext).String()))
	if wui.canRefresh(user) {
		rb.bindString("reindex-url", sx.MakeString(newURLBuilder('h').AppendQuery(
			strZid+" "+api.IdentDirective+api.ActionSeparator+api.ReIndexAction).String()))
	}

	// Ensure to have title, role, tags, and syntax included as "meta-*"
	rb.bindKeyValue(api.KeyTitle, m.GetDefault(api.KeyTitle, ""))
	rb.bindKeyValue(api.KeyRole, m.GetDefault(api.KeyRole, ""))
	rb.bindKeyValue(api.KeyTags, m.GetDefault(api.KeyTags, ""))
	rb.bindKeyValue(api.KeySyntax, m.GetDefault(api.KeySyntax, meta.DefaultSyntax))
	var metaPairs sx.ListBuilder
	for _, p := range m.ComputedPairs() {
		key, value := p.Key, p.Value
		metaPairs.Add(sx.Cons(sx.MakeString(key), sx.MakeString(value)))
		rb.bindKeyValue(key, value)
	}
	rb.bindString("metapairs", metaPairs.List())
}

func (wui *WebUI) fetchNewTemplatesSxn(ctx context.Context, user *meta.Meta) (lst *sx.Pair) {
	if !wui.canCreate(ctx, user) {
		return nil
	}
	ctx = box.NoEnrichContext(ctx)
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))
	for i := len(refs) - 1; i >= 0; i-- {
		zid, err2 := id.Parse(refs[i].URL.Path)
		if err2 != nil {
			continue
		}
		z, err2 := wui.box.GetZettel(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, z.Meta) {
			continue
		}
		text := sx.MakeString(parser.NormalizedSpacedText(z.Meta.GetTitle()))
		link := sx.MakeString(wui.NewURLBuilder('c').SetZid(zid.ZettelID()).
			AppendKVQuery(queryKeyAction, valueActionNew).String())

		lst = lst.Cons(sx.Cons(text, link))
	}
	return lst
}
func (wui *WebUI) calculateFooterSxn(ctx context.Context) *sx.Pair {
	if footerZid, err := id.Parse(wui.getConfig(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := wui.getSimpleHTMLEncoder(wui.getConfig(ctx, zn.InhMeta, api.KeyLang)).SetUnique("footer-")
			if content, endnotes, err3 := htmlEnc.BlocksSxn(&zn.Ast); err3 == nil {
				if content != nil && endnotes != nil {
					content.LastPair().SetCdr(sx.Cons(endnotes, nil))
				}
				return content
			}
		}
	}
	return nil
}

func (wui *WebUI) getSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sxeval.Expr, error) {
	if t := wui.getSxnCache(zid); t != nil {
		return t, nil
	}

	reader, err := wui.makeZettelReader(ctx, zid)
	if err != nil {
		return nil, err
	}

	objs, err := reader.ReadAll()
	if err != nil {
		wui.log.Error().Err(err).Zid(zid).Msg("reading sxn template")
		return nil, err
	}
	if len(objs) != 1 {
		return nil, fmt.Errorf("expected 1 expression in template, but got %d", len(objs))
	}
	env := sxeval.MakeExecutionEnvironment(bind)
	t, err := env.Compile(objs[0])
	if err != nil {
		return nil, err
	}

	wui.setSxnCache(zid, t)
	return t, nil
}
func (wui *WebUI) makeZettelReader(ctx context.Context, zid id.Zid) (*sxreader.Reader, error) {
	ztl, err := wui.box.GetZettel(ctx, zid)
	if err != nil {
		return nil, err
	}

	reader := sxreader.MakeReader(bytes.NewReader(ztl.Content.AsBytes()))
	return reader, nil
}

func (wui *WebUI) evalSxnTemplate(ctx context.Context, zid id.Zid, bind *sxeval.Binding) (sx.Object, error) {
	templateExpr, err := wui.getSxnTemplate(ctx, zid, bind)
	if err != nil {
		return nil, err
	}
	env := sxeval.MakeExecutionEnvironment(bind)
	return env.Run(templateExpr)
}

func (wui *WebUI) renderSxnTemplate(ctx context.Context, w http.ResponseWriter, templateID id.Zid, bind *sxeval.Binding) error {
	return wui.renderSxnTemplateStatus(ctx, w, http.StatusOK, templateID, bind)
}
func (wui *WebUI) renderSxnTemplateStatus(ctx context.Context, w http.ResponseWriter, code int, templateID id.Zid, bind *sxeval.Binding) error {
	detailObj, err := wui.evalSxnTemplate(ctx, templateID, bind)
	if err != nil {
		return err
	}
	bind.Bind(symDetail, detailObj)

	pageObj, err := wui.evalSxnTemplate(ctx, id.BaseTemplateZid, bind)
	if err != nil {
		return err
	}
	if msg := wui.log.Debug(); msg != nil {
		// pageObj.String() can be expensive to calculate.
		msg.Str("page", pageObj.String()).Msg("render")
	}

	gen := sxhtml.NewGenerator().SetNewline()
	var sb bytes.Buffer
	_, err = gen.WriteHTML(&sb, pageObj)
	if err != nil {
		return err
	}
	wui.prepareAndWriteHeader(w, code)
	if _, err = w.Write(sb.Bytes()); err != nil {
		wui.log.Error().Err(err).Msg("Unable to write HTML via template")
	}
	return nil // No error reporting, since we do not know what happended during write to client.
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	ctx = context.WithoutCancel(ctx) // Ignore any cancel / timeouts to write an error message.
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	} else {
		wui.log.Debug().Err(err).Msg("reportError")
	}
	user := server.GetUser(ctx)
	env, rb := wui.createRenderEnv(ctx, "error", api.ValueLangEN, "Error", user)
	rb.bindString("heading", sx.MakeString(http.StatusText(code)))
	rb.bindString("message", sx.MakeString(text))
	if rb.err == nil {
		rb.err = wui.renderSxnTemplateStatus(ctx, w, code, id.ErrorTemplateZid, env)
	}
	errSx := rb.err
	if errSx == nil {
		return
	}
	wui.log.Error().Err(errSx).Msg("while rendering error message")

	// if errBind != nil, the HTTP header was not written
	wui.prepareAndWriteHeader(w, http.StatusInternalServerError)
	fmt.Fprintf(
		w,
		`<!DOCTYPE html>
<html>
<head><title>Internal server error</title></head>
<body>
<h1>Internal server error</h1>
<p>When generating error code %d with message:</p><pre>%v</pre><p>an error occured:</p><pre>%v</pre>
</body>
</html>`, code, text, errSx)
}

func makeStringList(sl []string) *sx.Pair {
	if len(sl) == 0 {
		return nil
	}
	result := sx.Nil()
	for i := len(sl) - 1; i >= 0; i-- {
		result = result.Cons(sx.MakeString(sl[i]))
	}
	return result
}

Deleted web/adapter/webui/titleenc.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) 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 webui

import (
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder/textenc"
)

func encodeEvaluatedTitleHTML(m *meta.Meta, evalMetadata evalMetadataFunc, gen *htmlGenerator) string {
	return encodeZmkMetadata(m.GetTitle(), evalMetadata, gen)
}

func encodeEvaluatedTitleText(m *meta.Meta, evalMetadata evalMetadataFunc, enc *textenc.Encoder) string {
	is := evalMetadata(m.GetTitle())
	result, err := encodeInlinesText(&is, enc)
	if err != nil {
		return err.Error()
	}
	return result

}

Changes to web/adapter/webui/webui.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
1

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

-
+






+
+
+






-


-



-
-
-
-
-
+
+
+
+
+
-
-
+
-
-
-
+


-
-



+
+
+















-
-

-
+
-
-
-














-
-
+
+
+
+
+
+
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+
-






+












-
+














-
+
+
+
+
+




+
+
+
+
+
+
+
+
+
+
+
+



-
-
+
+




-
-
-
+
+
+
+
+

-
+


-
+

-
+


-
+
-

-
+

-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+








-
+

-
+
-
-
-
-










-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2023 Detlef Stern
// 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 webui provides web-UI handlers for web requests.
package webui

import (
	"bytes"
	"context"
	"net/http"
	"strings"
	"sync"
	"time"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/box"
	"zettelstore.de/z/collect"
	"zettelstore.de/z/config"
	"t73f.de/r/sx"
	"t73f.de/r/sx/sxeval"
	"t73f.de/r/sxwebs/sxhtml"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/id"
	"zettelstore.de/z/box"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/encoder"
	"zettelstore.de/z/encoder/textenc"
	"zettelstore.de/z/config"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/parser"
	"zettelstore.de/z/template"
	"zettelstore.de/z/usecase"
	"zettelstore.de/z/web/adapter"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/id"
	"zettelstore.de/z/zettel/meta"
)

// WebUI holds all data for delivering the web ui.
type WebUI struct {
	log      *logger.Logger
	debug    bool
	ab       server.AuthBuilder
	authz    auth.AuthzManager
	rtConfig config.Config
	token    auth.TokenManager
	box      webuiBox
	policy   auth.Policy

	evalZettel *usecase.Evaluate

	gentext *textenc.Encoder

	mxCache       sync.RWMutex
	templateCache map[id.Zid]*template.Template
	templateCache map[id.Zid]sxeval.Expr

	mxRoleCSSMap sync.RWMutex
	roleCSSMap   map[string]id.Zid

	tokenLifetime time.Duration
	cssBaseURL    string
	cssUserURL    string
	homeURL       string
	listZettelURL string
	listRolesURL  string
	listTagsURL   string
	refreshURL    string
	withAuth      bool
	loginURL      string
	logoutURL     string
	searchURL     string
	createNewURL  string
}


	rootBinding     *sxeval.Binding
	mxZettelBinding sync.Mutex
	zettelBinding   *sxeval.Binding
	dag             id.Digraph
	genHTML         *sxhtml.Generator
}

// webuiBox contains all box methods that are needed for WebUI operation.
//
// Note: these function must not do auth checking.
type webuiBox interface {
	CanCreateZettel(ctx context.Context) bool
	GetZettel(ctx context.Context, zid id.Zid) (domain.Zettel, error)
	GetMeta(ctx context.Context, zid id.Zid) (*meta.Meta, error)
	CanUpdateZettel(ctx context.Context, zettel domain.Zettel) bool
	AllowRenameZettel(ctx context.Context, zid id.Zid) bool
	CanCreateZettel(context.Context) bool
	GetZettel(context.Context, id.Zid) (zettel.Zettel, error)
	GetMeta(context.Context, id.Zid) (*meta.Meta, error)
	CanUpdateZettel(context.Context, zettel.Zettel) bool
	CanDeleteZettel(context.Context, id.Zid) bool
	CanDeleteZettel(ctx context.Context, zid id.Zid) bool
}

// New creates a new WebUI struct.
func New(log *logger.Logger, ab server.AuthBuilder, authz auth.AuthzManager, rtConfig config.Config, token auth.TokenManager,
	mgr box.Manager, pol auth.Policy, evalZettel *usecase.Evaluate) *WebUI {
	loginoutBase := ab.NewURLBuilder('i')

	wui := &WebUI{
		log:      log,
		debug:    kernel.Main.GetConfig(kernel.CoreService, kernel.CoreDebug).(bool),
		ab:       ab,
		rtConfig: rtConfig,
		authz:    authz,
		token:    token,
		box:      mgr,
		policy:   pol,

		evalZettel: evalZettel,

		gentext: textenc.Create(),
		templateCache: make(map[id.Zid]sxeval.Expr, 32),

		tokenLifetime: kernel.Main.GetConfig(kernel.WebService, kernel.WebTokenLifetimeHTML).(time.Duration),
		cssBaseURL:    ab.NewURLBuilder('z').SetZid(api.ZidBaseCSS).String(),
		cssUserURL:    ab.NewURLBuilder('z').SetZid(api.ZidUserCSS).String(),
		homeURL:       ab.NewURLBuilder('/').String(),
		listZettelURL: ab.NewURLBuilder('h').String(),
		listRolesURL:  ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyRole).String(),
		listTagsURL:   ab.NewURLBuilder('h').AppendQuery(api.ActionSeparator + api.KeyTags).String(),
		refreshURL:    ab.NewURLBuilder('g').AppendKVQuery("_c", "r").String(),
		withAuth:      authz.WithAuth(),
		loginURL:      loginoutBase.String(),
		logoutURL:     loginoutBase.AppendKVQuery("logout", "").String(),
		searchURL:     ab.NewURLBuilder('h').String(),
		createNewURL:  ab.NewURLBuilder('c').String(),
	}

		zettelBinding: nil,
		genHTML:       sxhtml.NewGenerator().SetNewline(),
	}
	wui.rootBinding = wui.createRenderBinding()
	wui.observe(box.UpdateInfo{Box: mgr, Reason: box.OnReload, Zid: id.Invalid})
	mgr.RegisterObserver(wui.observe)
	return wui
}

func (wui *WebUI) getConfig(ctx context.Context, m *meta.Meta, key string) string {
	return wui.rtConfig.Get(ctx, m, key)
}
func (wui *WebUI) getUserLang(ctx context.Context) string {
	return wui.getConfig(ctx, nil, api.KeyLang)
}

var (
	symDetail     = sx.MakeSymbol("DETAIL")
	symMetaHeader = sx.MakeSymbol("META-HEADER")
)

func (wui *WebUI) observe(ci box.UpdateInfo) {
	wui.mxCache.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.BaseTemplateZid {
		wui.templateCache = make(map[id.Zid]*template.Template, len(wui.templateCache))
	if ci.Reason == box.OnReload {
		clear(wui.templateCache)
	} else {
		delete(wui.templateCache, ci.Zid)
	}
	wui.mxCache.Unlock()
	wui.mxRoleCSSMap.Lock()
	if ci.Reason == box.OnReload || ci.Zid == id.RoleCSSMapZid {
		wui.roleCSSMap = nil

	wui.mxZettelBinding.Lock()
	if ci.Reason == box.OnReload || wui.dag.HasVertex(ci.Zid) {
		wui.zettelBinding = nil
		wui.dag = nil
	}
	wui.mxRoleCSSMap.Unlock()
	wui.mxZettelBinding.Unlock()
}

func (wui *WebUI) cacheSetTemplate(zid id.Zid, t *template.Template) {
func (wui *WebUI) setSxnCache(zid id.Zid, expr sxeval.Expr) {
	wui.mxCache.Lock()
	wui.templateCache[zid] = t
	wui.templateCache[zid] = expr
	wui.mxCache.Unlock()
}

func (wui *WebUI) getSxnCache(zid id.Zid) sxeval.Expr {
func (wui *WebUI) cacheGetTemplate(zid id.Zid) (*template.Template, bool) {
	wui.mxCache.RLock()
	t, ok := wui.templateCache[zid]
	expr, found := wui.templateCache[zid]
	wui.mxCache.RUnlock()
	return t, ok
}

	if found {
func (wui *WebUI) retrieveCSSZidFromRole(ctx context.Context, m meta.Meta) (id.Zid, error) {
	wui.mxRoleCSSMap.RLock()
	if wui.roleCSSMap == nil {
		wui.mxRoleCSSMap.RUnlock()
		wui.mxRoleCSSMap.Lock()
		mMap, err := wui.box.GetMeta(ctx, id.RoleCSSMapZid)
		if err == nil {
			wui.roleCSSMap = createRoleCSSMap(mMap)
		}
		wui.mxRoleCSSMap.Unlock()
		if err != nil {
			return id.Invalid, err
		}
		wui.mxRoleCSSMap.RLock()
	}

	defer wui.mxRoleCSSMap.RUnlock()
	if role, found := m.Get("css-role"); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
		return expr
	}
	}
	if role, found := m.Get(api.KeyRole); found {
		if result, found2 := wui.roleCSSMap[role]; found2 {
			return result, nil
		}
	}
	return id.Invalid, nil
}

func createRoleCSSMap(mMap *meta.Meta) map[string]id.Zid {
	result := make(map[string]id.Zid)
	for _, p := range mMap.PairsRest() {
		key := p.Key
		if len(key) < 9 || !strings.HasPrefix(key, "css-") || !strings.HasSuffix(key, "-zid") {
			continue
		}
		zid, err2 := id.Parse(p.Value)
		if err2 != nil {
			continue
		}
		result[key[4:len(key)-4]] = zid
	}
	return result
	return nil
}

func (wui *WebUI) canCreate(ctx context.Context, user *meta.Meta) bool {
	m := meta.New(id.Invalid)
	return wui.policy.CanCreate(user, m) && wui.box.CanCreateZettel(ctx)
}

func (wui *WebUI) canWrite(
	ctx context.Context, user, meta *meta.Meta, content domain.Content) bool {
	ctx context.Context, user, meta *meta.Meta, content zettel.Content) bool {
	return wui.policy.CanWrite(user, meta, meta) &&
		wui.box.CanUpdateZettel(ctx, domain.Zettel{Meta: meta, Content: content})
		wui.box.CanUpdateZettel(ctx, zettel.Zettel{Meta: meta, Content: content})
}

func (wui *WebUI) canRename(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanRename(user, m) && wui.box.AllowRenameZettel(ctx, m.Zid)
}

func (wui *WebUI) canDelete(ctx context.Context, user, m *meta.Meta) bool {
	return wui.policy.CanDelete(user, m) && wui.box.CanDeleteZettel(ctx, m.Zid)
}

func (wui *WebUI) canRefresh(user *meta.Meta) bool {
	return wui.policy.CanRefresh(user)
}

func (wui *WebUI) getTemplate(
	ctx context.Context, templateID id.Zid) (*template.Template, error) {
	if t, ok := wui.cacheGetTemplate(templateID); ok {
		return t, nil
	}
	realTemplateZettel, err := wui.box.GetZettel(ctx, templateID)
	if err != nil {
		return nil, err
	}
	t, err := template.ParseString(realTemplateZettel.Content.AsString(), nil)
	if err == nil {
		// t.SetErrorOnMissing()
		wui.cacheSetTemplate(templateID, t)
	}
	return t, err
}

type simpleLink struct {
	Text string
	URL  string
}

type simpleLinks struct {
	Has   bool
	Links []simpleLink
}

func createSimpleLinks(ls []simpleLink) simpleLinks {
	return simpleLinks{
		Has:   len(ls) > 0,
		Links: ls,
	}
}

type baseData struct {
	Lang           string
	MetaHeader     string
	CSSBaseURL     string
	CSSUserURL     string
	CSSRoleURL     string
	Title          string
	HomeURL        string
	WithUser       bool
	WithAuth       bool
	UserIsValid    bool
	UserZettelURL  string
	UserIdent      string
	LoginURL       string
	LogoutURL      string
	ListZettelURL  string
	ListRolesURL   string
	ListTagsURL    string
	CanRefresh     bool
	RefreshURL     string
	NewZettelLinks simpleLinks
	SearchURL      string
	QueryKeyQuery  string
	QueryKeySeed   string
	CreateNewURL   string
	Content        string
	FooterHTML     string
	DebugMode      bool
}

func (wui *WebUI) makeBaseData(ctx context.Context, lang, title, roleCSSURL string, user *meta.Meta, data *baseData) {
	var userZettelURL string
	var userIdent string

	userIsValid := user != nil
	if userIsValid {
		userZettelURL = wui.NewURLBuilder('h').SetZid(api.ZettelID(user.Zid.String())).String()
		userIdent = user.GetDefault(api.KeyUserID, "")
	}

	data.Lang = lang
	data.CSSBaseURL = wui.cssBaseURL
	data.CSSUserURL = wui.cssUserURL
	data.CSSRoleURL = roleCSSURL
	data.Title = title
	data.HomeURL = wui.homeURL
	data.WithAuth = wui.withAuth
	data.WithUser = data.WithAuth
	data.UserIsValid = userIsValid
	data.UserZettelURL = userZettelURL
	data.UserIdent = userIdent
	data.LoginURL = wui.loginURL
	data.LogoutURL = wui.logoutURL
	data.ListZettelURL = wui.listZettelURL
	data.ListRolesURL = wui.listRolesURL
	data.ListTagsURL = wui.listTagsURL
	data.CanRefresh = wui.canRefresh(user)
	data.RefreshURL = wui.refreshURL
	data.NewZettelLinks = createSimpleLinks(wui.fetchNewTemplates(ctx, user))
	data.SearchURL = wui.searchURL
	data.QueryKeyQuery = api.QueryKeyQuery
	data.QueryKeySeed = api.QueryKeySeed
	data.CreateNewURL = wui.createNewURL
	data.FooterHTML = wui.calculateFooterHTML(ctx)
	data.DebugMode = wui.debug
}

func (wui *WebUI) getSimpleHTMLEncoder() *htmlGenerator { return createGenerator(wui, "") }
func (wui *WebUI) getSimpleHTMLEncoder(lang string) *htmlGenerator {
func (wui *WebUI) createZettelEncoder(ctx context.Context, m *meta.Meta) *htmlGenerator {
	return createGenerator(wui, wui.rtConfig.Get(ctx, m, config.KeyMarkerExternal))
}

// htmlAttrNewWindow returns HTML attribute string for opening a link in a new window.
// If hasURL is false an empty string is returned.
func htmlAttrNewWindow(hasURL bool) string {
	if hasURL {
		return ` target="_blank" rel="noopener noreferrer"`
	}
	return ""
	return wui.createGenerator(wui, lang)
}

func (wui *WebUI) fetchNewTemplates(ctx context.Context, user *meta.Meta) (result []simpleLink) {
	ctx = box.NoEnrichContext(ctx)
	if !wui.canCreate(ctx, user) {
		return nil
	}
	menu, err := wui.box.GetZettel(ctx, id.TOCNewTemplateZid)
	if err != nil {
		return nil
	}
	refs := collect.Order(parser.ParseZettel(ctx, menu, "", wui.rtConfig))
	for _, ref := range refs {
		zid, err2 := id.Parse(ref.URL.Path)
		if err2 != nil {
			continue
		}
		m, err2 := wui.box.GetMeta(ctx, zid)
		if err2 != nil {
			continue
		}
		if !wui.policy.CanRead(user, m) {
			continue
		}
		title := m.GetTitle()
		astTitle := parser.ParseMetadataNoLink(title)
		menuTitle := wui.getSimpleHTMLEncoder().InlinesString(&astTitle)
		result = append(result, simpleLink{
			Text: menuTitle,
			URL: wui.NewURLBuilder('c').SetZid(api.ZettelID(m.Zid.String())).
				AppendKVQuery(queryKeyAction, valueActionNew).String(),
		})
	}
	return result
}

func (wui *WebUI) calculateFooterHTML(ctx context.Context) string {
	if footerZid, err := id.Parse(wui.rtConfig.Get(ctx, nil, config.KeyFooterZettel)); err == nil {
		if zn, err2 := wui.evalZettel.Run(ctx, footerZid, ""); err2 == nil {
			htmlEnc := encoder.Create(api.EncoderHTML)
			var buf bytes.Buffer
			if _, err2 = htmlEnc.WriteBlocks(&buf, &zn.Ast); err2 == nil {
				return buf.String()
			}
		}
	}
	return ""
}

func (wui *WebUI) renderTemplate(
	ctx context.Context,
	w http.ResponseWriter,
	templateID id.Zid,
	base *baseData,
	data interface{}) {
	wui.renderTemplateStatus(ctx, w, http.StatusOK, templateID, base, data)
}

func (wui *WebUI) reportError(ctx context.Context, w http.ResponseWriter, err error) {
	code, text := adapter.CodeMessageFromError(err)
	if code == http.StatusInternalServerError {
		wui.log.Error().Msg(err.Error())
	}
	user := server.GetUser(ctx)
	var base baseData
	wui.makeBaseData(ctx, api.ValueLangEN, "Error", "", user, &base)
	wui.renderTemplateStatus(ctx, w, code, id.ErrorTemplateZid, &base, struct {
		ErrorTitle string
		ErrorText  string
	}{
		ErrorTitle: http.StatusText(code),
		ErrorText:  text,
	})
}

func (wui *WebUI) renderTemplateStatus(
	ctx context.Context,
	w http.ResponseWriter,
	code int,
	templateID id.Zid,
	base *baseData,
	data interface{}) {

	bt, err := wui.getTemplate(ctx, id.BaseTemplateZid)
	if err != nil {
		wui.log.IfErr(err).Zid(id.BaseTemplateZid).Msg("Unable to get template")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	t, err := wui.getTemplate(ctx, templateID)
	if err != nil {
		wui.log.IfErr(err).Zid(templateID).Msg("Unable to get template")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if user := server.GetUser(ctx); user != nil {
		if tok, err1 := wui.token.GetToken(user, wui.tokenLifetime, auth.KindHTML); err1 == nil {
			wui.setToken(w, tok)
		}
	}
	var content bytes.Buffer
	err = t.Render(&content, data)
	if err == nil {
		wui.prepareAndWriteHeader(w, code)
		base.Content = content.String()
		err = bt.Render(w, base)
	}
	if err != nil {
		wui.log.IfErr(err).Msg("Unable to write HTML via template")
	}
}

// GetURLPrefix returns the configured URL prefix of the web server.
func (wui *WebUI) GetURLPrefix() string { return wui.ab.GetURLPrefix() }

// NewURLBuilder creates a new URL builder object with the given key.
func (wui *WebUI) NewURLBuilder(key byte) *api.URLBuilder { return wui.ab.NewURLBuilder(key) }

Changes to web/content/content.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
1

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

-
+






+
+
+










-
-
-
+
+
+


+






-


+






-
+
+

-




















-




-
+

+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 content manages content handling within the web package.
// It translates syntax values into content types, and vice versa.
package content

import (
	"mime"
	"net/http"

	"zettelstore.de/c/api"
	"zettelstore.de/z/domain"
	"zettelstore.de/z/domain/meta"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/zettel"
	"zettelstore.de/z/zettel/meta"
)

// Some MIME encoding values.
const (
	UnknownMIME  = "application/octet-stream"
	mimeGIF      = "image/gif"
	mimeHTML     = "text/html; charset=utf-8"
	mimeJPEG     = "image/jpeg"
	mimeMarkdown = "text/markdown; charset=utf-8"
	JSON         = "application/json"
	PlainText    = "text/plain; charset=utf-8"
	mimePNG      = "image/png"
	SXPF         = PlainText
	mimeWEBP     = "image/webp"
)

var encoding2mime = map[api.EncodingEnum]string{
	api.EncoderHTML:  mimeHTML,
	api.EncoderMD:    mimeMarkdown,
	api.EncoderSexpr: PlainText,
	api.EncoderSz:    SXPF,
	api.EncoderSHTML: SXPF,
	api.EncoderText:  PlainText,
	api.EncoderZJSON: JSON,
	api.EncoderZmk:   PlainText,
}

// MIMEFromEncoding returns the MIME encoding for a given zettel encoding
func MIMEFromEncoding(enc api.EncodingEnum) string {
	if m, found := encoding2mime[enc]; found {
		return m
	}
	return UnknownMIME
}

var syntax2mime = map[string]string{
	meta.SyntaxCSS:      "text/css; charset=utf-8",
	meta.SyntaxDraw:     PlainText,
	meta.SyntaxGif:      mimeGIF,
	meta.SyntaxHTML:     mimeHTML,
	meta.SyntaxJPEG:     mimeJPEG,
	meta.SyntaxJPG:      mimeJPEG,
	meta.SyntaxMarkdown: mimeMarkdown,
	meta.SyntaxMD:       mimeMarkdown,
	meta.SyntaxMustache: PlainText,
	meta.SyntaxNone:     "",
	meta.SyntaxPlain:    PlainText,
	meta.SyntaxPNG:      mimePNG,
	meta.SyntaxSVG:      "image/svg+xml",
	meta.SyntaxTxt:      PlainText,
	meta.SyntaxSxn:      SXPF,
	meta.SyntaxText:     PlainText,
	meta.SyntaxTxt:      PlainText,
	meta.SyntaxWebp:     mimeWEBP,
	meta.SyntaxZmk:      "text/x-zmk; charset=utf-8",

	// Additional syntaxes that are parsed as plain text.
	"js":  "text/javascript; charset=utf-8",
	"pdf": "application/pdf",
	"xml": "text/xml; charset=utf-8",
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
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







+
+














-
+





	"text/plain":    meta.SyntaxText,

	// Additional syntaxes
	"application/pdf": "pdf",
	"text/javascript": "js",
}

// SyntaxFromMIME returns the syntax for a zettel based on MIME encoding value
// and the actual data.
func SyntaxFromMIME(m string, data []byte) string {
	mt, _, _ := mime.ParseMediaType(m)
	if syntax, found := mime2syntax[mt]; found {
		return syntax
	}
	if len(data) > 0 {
		ct := http.DetectContentType(data)
		mt, _, _ = mime.ParseMediaType(ct)
		if syntax, found := mime2syntax[mt]; found {
			return syntax
		}
		if ext, err := mime.ExtensionsByType(mt); err != nil && len(ext) > 0 {
			return ext[0][1:]
		}
		if domain.IsBinary(data) {
		if zettel.IsBinary(data) {
			return "binary"
		}
	}
	return "plain"
}

Changes to web/content/content_test.go.

1
2

3
4
5
6
7
8



9
10
11
12
13
14
15
1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

-
+






+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2022 Detlef Stern
// 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 content_test

import (
	"testing"

Changes to web/server/impl/http.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
1

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

-
+






+
+
+















-
+






-
+
+









-
+



-
+


-
+









+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 impl

import (
	"context"
	"net"
	"net/http"
	"time"
)

// Server timeout values
const (
	shutdownTimeout = 5 * time.Second
	readTimeout     = 5 * time.Second
	writeTimeout    = 10 * time.Second
	writeTimeout    = 20 * time.Second
	idleTimeout     = 120 * time.Second
)

// httpServer is a HTTP server.
type httpServer struct {
	http.Server
	waitStop chan struct{}

	origHandler http.Handler
}

// initializeHTTPServer creates a new HTTP server object.
func (srv *httpServer) initializeHTTPServer(addr string, handler http.Handler) {
	if addr == "" {
		addr = ":http"
	}
	srv.Server = http.Server{
		Addr:    addr,
		Handler: handler,
		Handler: http.TimeoutHandler(handler, writeTimeout, "Timeout"),

		// See: https://blog.cloudflare.com/exposing-go-on-the-internet/
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		WriteTimeout: writeTimeout + 500*time.Millisecond, // Give some time to detect timeout and to write an appropriate error message.
		IdleTimeout:  idleTimeout,
	}
	srv.waitStop = make(chan struct{})
	srv.origHandler = handler
}

// SetDebug enables debugging goroutines that are started by the server.
// Basically, just the timeout values are reset. This method should be called
// before running the server.
func (srv *httpServer) SetDebug() {
	srv.ReadTimeout = 0
	srv.WriteTimeout = 0
	srv.IdleTimeout = 0
	srv.Handler = srv.origHandler
}

// Run starts the web server, but does not wait for its completion.
func (srv *httpServer) Run() error {
	ln, err := net.Listen("tcp", srv.Addr)
	if err != nil {
		return err

Changes to web/server/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

37
38
39
40
41




42








43
44


45
46
47
48
49
50
51
1

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

-
+






+
+
+










-
+

-
-
-
+
+
+











+
+
+
+
+
+
+
+
+
+
+
+
+

-
+

-
-
-
-
+
+
+
+

+
+
+
+
+
+
+
+
-
-
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2021-2022 Detlef Stern
// 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 the Zettelstore web service.
package impl

import (
	"context"
	"net/http"
	"time"

	"zettelstore.de/c/api"
	"t73f.de/r/zsc/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/domain/meta"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
	"zettelstore.de/z/zettel/meta"
)

type myServer struct {
	log              *logger.Logger
	baseURL          string
	server           httpServer
	router           httpRouter
	persistentCookie bool
	secureCookie     bool
}

// ServerData contains the data needed to configure a server.
type ServerData struct {
	Log              *logger.Logger
	ListenAddr       string
	BaseURL          string
	URLPrefix        string
	MaxRequestSize   int64
	Auth             auth.TokenManager
	PersistentCookie bool
	SecureCookie     bool
	Profiling        bool
}

// New creates a new web server.
func New(log *logger.Logger, listenAddr, baseURL, urlPrefix string, persistentCookie, secureCookie bool, maxRequestSize int64, auth auth.TokenManager) server.Server {
func New(sd ServerData) server.Server {
	srv := myServer{
		log:              log,
		baseURL:          baseURL,
		persistentCookie: persistentCookie,
		secureCookie:     secureCookie,
		log:              sd.Log,
		baseURL:          sd.BaseURL,
		persistentCookie: sd.PersistentCookie,
		secureCookie:     sd.SecureCookie,
	}

	rd := routerData{
		log:            sd.Log,
		urlPrefix:      sd.URLPrefix,
		maxRequestSize: sd.MaxRequestSize,
		auth:           sd.Auth,
		profiling:      sd.Profiling,
	}
	srv.router.initializeRouter(log, urlPrefix, maxRequestSize, auth)
	srv.server.initializeHTTPServer(listenAddr, &srv.router)
	srv.router.initializeRouter(rd)
	srv.server.initializeHTTPServer(sd.ListenAddr, &srv.router)
	return &srv
}

func (srv *myServer) Handle(pattern string, handler http.Handler) {
	srv.router.Handle(pattern, handler)
}
func (srv *myServer) AddListRoute(key byte, method server.Method, handler http.Handler) {

Changes to web/server/impl/router.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
1

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

-
+






+
+
+







+

+


-

















-

















+
+
+
+
+
+
+
+

-
-
-
-
+
+
+
+




-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+







//-----------------------------------------------------------------------------
// Copyright (c) 2020-2022 Detlef Stern
// 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 impl

import (
	"io"
	"net/http"
	"net/http/pprof"
	"regexp"
	rtprf "runtime/pprof"
	"strings"

	"zettelstore.de/c/api"
	"zettelstore.de/z/auth"
	"zettelstore.de/z/kernel"
	"zettelstore.de/z/logger"
	"zettelstore.de/z/web/server"
)

type (
	methodHandler [server.MethodLAST]http.Handler
	routingTable  [256]*methodHandler
)

var mapMethod = map[string]server.Method{
	http.MethodHead:   server.MethodHead,
	http.MethodGet:    server.MethodGet,
	http.MethodPost:   server.MethodPost,
	http.MethodPut:    server.MethodPut,
	http.MethodDelete: server.MethodDelete,
	api.MethodMove:    server.MethodMove,
}

// httpRouter handles all routing for zettelstore.
type httpRouter struct {
	log         *logger.Logger
	urlPrefix   string
	auth        auth.TokenManager
	minKey      byte
	maxKey      byte
	reURL       *regexp.Regexp
	listTable   routingTable
	zettelTable routingTable
	ur          server.UserRetriever
	mux         *http.ServeMux
	maxReqSize  int64
}

type routerData struct {
	log            *logger.Logger
	urlPrefix      string
	maxRequestSize int64
	auth           auth.TokenManager
	profiling      bool
}

// initializeRouter creates a new, empty router with the given root handler.
func (rt *httpRouter) initializeRouter(log *logger.Logger, urlPrefix string, maxRequestSize int64, auth auth.TokenManager) {
	rt.log = log
	rt.urlPrefix = urlPrefix
	rt.auth = auth
func (rt *httpRouter) initializeRouter(rd routerData) {
	rt.log = rd.log
	rt.urlPrefix = rd.urlPrefix
	rt.auth = rd.auth
	rt.minKey = 255
	rt.maxKey = 0
	rt.reURL = regexp.MustCompile("^$")
	rt.mux = http.NewServeMux()
	rt.maxReqSize = maxRequestSize
	rt.maxReqSize = rd.maxRequestSize

	if rd.profiling {
		rt.setRuntimeProfiling()
	}
}

func (rt *httpRouter) setRuntimeProfiling() {
	rt.mux.HandleFunc("GET /rtp/", pprof.Index)
	for _, profile := range rtprf.Profiles() {
		name := profile.Name()
		rt.mux.Handle("GET /rtp/"+name, pprof.Handler(name))
	}
	rt.mux.HandleFunc("GET /rtp/cmdline", pprof.Cmdline)
	rt.mux.HandleFunc("GET /rtp/profile", pprof.Profile)
	rt.mux.HandleFunc("GET /rtp/symbol", pprof.Symbol)
	rt.mux.HandleFunc("GET /rtp/trace", pprof.Trace)
}

func (rt *httpRouter) addRoute(key byte, method server.Method, handler http.Handler, table *routingTable) {
	// Set minKey and maxKey; re-calculate regexp.
	if key < rt.minKey || rt.maxKey < key {
		if key < rt.minKey {
			rt.minKey = key
104
105
106
107
108
109
110
111

112
113

114
115
116
117
118
119
120
131
132
133
134
135
136
137

138
139

140
141
142
143
144
145
146
147







-
+

-
+







func (rt *httpRouter) Handle(pattern string, handler http.Handler) {
	rt.mux.Handle(pattern, handler)
}

func (rt *httpRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Something may panic. Ensure a kernel log.
	defer func() {
		if reco := recover(); reco != nil {
		if ri := recover(); ri != nil {
			rt.log.Error().Str("Method", r.Method).Str("URL", r.URL.String()).HTTPIP(r).Msg("Recover context")
			kernel.Main.LogRecover("Web", reco)
			kernel.Main.LogRecover("Web", ri)
		}
	}()

	var withDebug bool
	if msg := rt.log.Debug(); msg.Enabled() {
		withDebug = true
		w = &traceResponseWriter{original: w}
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
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







-
+



-
+








-
+





-
+







}

func (rt *httpRouter) addUserContext(r *http.Request) *http.Request {
	if rt.ur == nil {
		// No auth needed
		return r
	}
	k := auth.KindJSON
	k := auth.KindAPI
	t := getHeaderToken(r)
	if len(t) == 0 {
		rt.log.Debug().Msg("no jwt token found") // IP already logged: ServeHTTP
		k = auth.KindHTML
		k = auth.KindwebUI
		t = getSessionToken(r)
	}
	if len(t) == 0 {
		rt.log.Debug().Msg("no auth token found in request") // IP already logged: ServeHTTP
		return r
	}
	tokenData, err := rt.auth.CheckToken(t, k)
	if err != nil {
		rt.log.Sense().Err(err).HTTPIP(r).Msg("invalid auth token")
		rt.log.Info().Err(err).HTTPIP(r).Msg("invalid auth token")
		return r
	}
	ctx := r.Context()
	user, err := rt.ur.GetUser(ctx, tokenData.Zid, tokenData.Ident)
	if err != nil {
		rt.log.Sense().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found")
		rt.log.Info().Zid(tokenData.Zid).Str("ident", tokenData.Ident).Err(err).HTTPIP(r).Msg("auth user not found")
		return r
	}
	return r.WithContext(updateContext(ctx, user, &tokenData))
}

func getSessionToken(r *http.Request) []byte {
	cookie, err := r.Cookie(sessionName)

Changes to web/server/server.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
1

2
3
4
5
6
7
8
9
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-2022 Detlef Stern
// 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 server provides the Zettelstore web service.
package server

import (
	"context"
	"net/http"
	"time"

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

// UserRetriever allows to retrieve user data based on a given zettel identifier.
type UserRetriever interface {
	GetUser(ctx context.Context, zid id.Zid, ident string) (*meta.Meta, error)
}

// Method enumerates the allowed HTTP methods.
type Method uint8

// Values for method type
const (
	MethodGet Method = iota
	MethodHead
	MethodPost
	MethodPut
	MethodMove
	MethodDelete
	MethodLAST // must always be the last one
)

// Router allows to state routes for various URL paths.
type Router interface {
	Handle(pattern string, handler http.Handler)

Changes to www/build.md.

1

2

3
4
5
6
7
8


9
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






































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

+

+






+
+



-
+


-
+
+


-
+


-
+



-
-
-
+
-
-
-
+
+

-
+

-
-
+
-







-
+


-





-
-
+
-



-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
# How to build Zettelstore

## Prerequisites

You must install the following software:

* A current, supported [release of Go](https://go.dev/doc/devel/release),
* [staticcheck](https://staticcheck.io/),
* [shadow](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow),
* [unparam](https://mvdan.cc/unparam),
* [govulncheck](https://golang.org/x/vuln/cmd/govulncheck),
* [revive](https://revive.run/),
* [Fossil](https://fossil-scm.org/),
* [Git](https://git-scm.org) (so that Go can download some dependencies).

See folder <tt>docs/development</tt> (a zettel box) for details.
See folder `docs/development` (a zettel box) for details.

## Clone the repository
Most of this is covered by the excellent Fossil [documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).
Most of this is covered by the excellent Fossil
[documentation](https://fossil-scm.org/home/doc/trunk/www/quickstart.wiki).

1. Create a directory to store your Fossil repositories.
   Let's assume, you have created <tt>$HOME/fossils</tt>.
   Let's assume, you have created `$HOME/fossils`.
1. Clone the repository: `fossil clone https://zettelstore.de/ $HOME/fossils/zettelstore.fossil`.
1. Create a working directory.
   Let's assume, you have created <tt>$HOME/zettelstore</tt>.
   Let's assume, you have created `$HOME/zettelstore`.
1. Change into this directory: `cd $HOME/zettelstore`.
1. Open development: `fossil open $HOME/fossils/zettelstore.fossil`.

(If you are not able to use Fossil, you could try the GitHub mirror
<https://github.com/zettelstore/zettelstore>.)

## Tools to build, test, and manage
## The build tool
In directory <tt>tools</tt> there is a Go file called <tt>build.go</tt>.
It automates most aspects, (hopefully) platform-independent.
In the directory `tools` there are some Go files to automate most aspects of
building and testing, (hopefully) platform-independent.

The script is called as:
The build script is called as:

```
go run tools/build.go [-v] COMMAND
    go run tools/build/build.go [-v] COMMAND
```

The flag `-v` enables the verbose mode.
It outputs all commands called by the tool.

Some important `COMMAND`s are:

* `build`: builds the software with correct version information and puts it
  into a freshly created directory <tt>bin</tt>.
  into a freshly created directory `bin`.
* `check`: checks the current state of the working directory to be ready for
  release (or commit).
* `clean`: removes the build directories and cleans the Go cache.
* `version`: prints the current version information.

Therefore, the easiest way to build your own version of the Zettelstore
software is to execute the command

```
go run tools/build.go build
    go run tools/build/build.go build
```

In case of errors, please send the output of the verbose execution:

```
go run tools/build.go -v build
```
    go run tools/build/build.go -v build

Other tools are:

* `go run tools/clean/clean.go` cleans your Go development workspace.
* `go run tools/check/check.go` executes all linters and unit tests.
  If you add the option `-r` linters are more strict, to be used for a
  release version.
* `go run tools/devtools/devtools.go` install all needed software (see above).
* `go run tools/htmllint/htmllint.go [URL]` checks all generated HTML of a
  Zettelstore accessible at the given URL (default: http://localhost:23123).
* `go run tools/testapi/testapi.go` tests the API against a running
  Zettelstore, which is started automatically.

## A note on the use of Fossil

Zettelstore is managed by the Fossil version control system. Fossil is an
alternative to the ubiquitous Git version control system. However, Go seems to
prefer Git and popular platforms that just support Git.

Some dependencies of Zettelstore, namely [Zettelstore
client](https://t73f.de/r/zsc), [webs](https://t73f.de/r/webs),
[sx](https://t73f.de/r/sx), and [sxwebs](https://t73f.de/r/sxwebs) are also
managed by Fossil. Depending on your development setup, some error messages
might occur.

If the error message mentions an environment variable called `GOVCS` you should
set it to the value `GOVCS=zettelstore.de:fossil` (alternatively more generous
to `GOVCS=*:all`). Since the Go build system is coupled with Git and some
special platforms, you must allow Go to download a Fossil repository from the
host `zettelstore.de`. The build tool sets `GOVCS` to the right value, but you
may use other `go` commands that try to download a Fossil repository.

On some operating systems, namely Termux on Android, an error message might
state that an user cannot be determined (`cannot determine user`). In this
case, Fossil is allowed to download the repository, but cannot associate it
with an user name. Set the environment variable `USER` to any user name, like:
`USER=nobody go run tools/build.go build`.

Changes to www/changes.wiki.

1
2













































































































































































































































































































































































3
4


















































5
6

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


+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

-
+






-
-
-
+
+
+
+

-
-
+
+












-
+

-
+














-
-
+
+

-
+



-
+
+




-
-
+
+





-
-
+
+

-
-
-
-
-
-
-
+
+
+
+
+
+
+
+




-
+


-
-
+
+

-
-
+
+





-
+

-
-
-
+
+
+







<title>Change Log</title>

<a id="0_20"></a>
<h2>Changes for Version 0.20.0 (pending)</h2>
  *  Query aggregates <code>ATOM</code> and <code>RSS</code> are removed, as
     well as the accompanying <code>TITLE</code> query action (parameter).
     Were announced as deprecated in version 0.19.
     (major)

<a id="0_19"></a>
<h2>Changes for Version 0.19.0 (2024-12-13)</h2>
  *  Remove support for renaming zettel, i.e. changing zettel identifier. Was
     announced as deprecated in version 0.18.
     (breaking: api, webui)
  *  Format of zettel identifier will be not changed. The deprecation message
     from version 0.18 is no longer valid.
     (major)
  *  Zettel content for most predefined zettel (ID less or equal than
     0000099999899) is not indexed any more. If you search / query for zettel
     content, these zettel will not be returned. However, their metadata is
     still searchable. Content of predefined zettel with ID between
     00000999999900 and 00000999999999 will be indexed..
     (breaking: api, webui)
  *  Metadata keys <code>superior</code> and <code>subordinate</code> are not
     supported on the WebUI any more. They are still typed as a set of zettel
     identifier, but are treated as ordinary links. Zettel should not form a
     hierarchy, in most cases.
     (major: webui)
  *  Metadata keys <code>sequel</code> and <code>prequel</code> support a
     sequence of zettel. They are supported through the WebUI also.
     <code>sequel</code> is calculated based on <code>prequel</code>. While
     folge zettel are a train of thought, zettel sequences form different train
     of thoughts.
     (major)
  *  Query aggregates <code>ATOM</code> and <code>RSS</code> will be removed in
     the next version of Zettelstore. They were introduced in [#0_7|v0.7] and
     [#0_8|v0.8]. Both are not needed for a digital zettelkasten. Their current
     use is to transform Zettelstore into a blogging engine. RSS and Atom feed
     can be provided by external software, much better than Zettelstore will
     ever do.
     (deprecation)
  *  Fix wrong quote translation for markdown encoder.
     (minor)
  *  Generate <code>&lt;th></code> in table header (was: <code>&lt;td></code>).
     Also applies to SHTML encoder. (minor: webui, api)
  *  External links are now generated in shtml and html with attribute
     rel="external" (previously: class="external").
     (minor: webui, api)
  *  Allow to enable runtime profiling of the software, to be used by
     developers.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_18"></a>
<h2>Changes for Version 0.18.0 (2024-07-11)</h2>
  *  Remove Sx macro <code>defunconst</code>. Use <code>defun</code> instead.
     (breaking: webui)
  *  The sz encoding of zettel does not make use of <code>(SPACE)</code>
     elements any more. Instead, space characters are encoded within the
     <code>(TEXT "...")</code> element. This might affect any client that works
     with the sz encoding to produce some output.
     (breaking)
  *  Format of zettel identifier will be changed in the future to a new format,
     instead of the current timestamp-based format. The usage of zettel
     identifier that are before 1970-01-01T00:00:00 is not allowed any more
     (with the exception of predefined identifier)
     (deprecation)
  *  Due to the planned format of zettel identifier, the &ldquo;rename&rdquo;
     operation is deprecated. It will be removed in version 0.19 or later. If
     you have a significant use case for the rename operation, please contact
     the maintainer immediate.
     (deprecation)
  *  New zettel are now created with the permission for others to read/write
     them. This is important especially for Unix-like systems. If you want the
     previous behaviour, set <code>umask</code> accordingly, for example
     <code>umask 066</code>.
     (major: dirbox)
  *  Add expert-mode zettel &ldquo;Zettelstore Warnings&rdquo; to help
     identifying zettel to upgrade for future migration to planned new zettel
     identifier format.
     (minor: webui)
  *  Add expert-mode zettel &ldquo;Zettelstore Identifier Mapping&rdquo; to
     show a possible mapping from the old identifier format to the new one.
     This should help users to possibly rename some zettel for a metter
     mapping.
     (minor: webui)
  *  Add metadata key <code>created-missing</code> to list zettel without
     stored metadata key <code>created</code>. Needed for migration to planned
     new zettelstore identifier format, which is not based on timestamp of
     zettel creation date.
     (minor)
  *  Add zettel &ldquo;Zettelstore Application Directory&rdquo;, which contains
     identifier for application specific zettel. Needed for planned new
     identifier format.
     (minor: webui)
  *  Update Sx prelude: make macros more robust / more general. This might
     break your code in the future.
     (minor: webui)
  *  Add computed expert-mode zettel &ldquo;Zettelstore Memory&rdquo; with
     zettel identifier <code>00000000000008</code>. It shows some statistics
     about memory usage.
     (minor: webui)
  *  Add computed expert-mode zettel &ldquo;Zettelstore Sx Engine&rdquo; with
     zettel identifier <code>00000000000009</code>. It shows some statistics
     about the internal Sx engine. (Currently only the number of used symbols,
     but this will change in the future.)
     (minor: webui)
  *  Zettelstore client is now Go package t73f.de/r/zsc.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_17"></a>
<h2>Changes for Version 0.17.0 (2024-03-04)</h2>
  *  Context search operates only on explicit references. Add the directive
     <code>FULL</code> to follow zettel tags additionally.
     (breaking)
  *  Context cost calculation has been changed. Prepare to retrieve different
     result.
     (breaking)
  *  Remove metadata type WordSet. It was never implemented completely, and
     nobody complained about this.
     (breaking)
  *  Remove logging level &ldquo;sense&rdquo;, &ldquo;warn&rdquo;,
     &ldquo;fatal&rdquo;, and &ldquo;panic&rdquo;.
     (breaking)
  *  Add query action <code>REDIRECT</code> which redirects to zettel that is
     the first in the query result list.
     (minor: api, webui)
  *  Add link to <code>CONTEXT FULL</code> in the zettel info page.
     (minor: webui)
  *  When generating HTML code to query set based metadata (esp. tags), also
     generate a query that matches all values.
     (minor: webui)
  *  Show all metadata with key ending &ldquo;-url&rdquo; on zettel view.
     (minor: webui)
  *  Make WebUI form elements a little bit more accessible by using HTML
     <code>search</code> tag and <code>inputmode</code> attribute.
     (minor: webui)
  *  Add UI action for role zettel, similar to tag zettel. Obviously forgotten
     in release 0.16.0, but thanks to the bug fix v0.16.1 detected.
     (minor: webui)
  *  If an action, which is written in uppercase letters, results in an empty
     list, the list of selected zettel is returned instead. This allows some
     backward compatibility if a new action is introduced.
     (minor)
  *  Only when query list is not empty, allow to show data and plain encoding,
     an optionally show the &ldquo;Save As Zettel&rdquo; button.
     (minor: webui)
  *  If query list is greater than three elements, show the number of elements
     at bottom (before other encodings).
     (minor: webui)
  *  Zettel with syntax &ldquo;sxn&rdquo; are pretty-printed during evaluation.
     This allows to retrieve parsed zettel content, which checked for syntax,
     but is not pretty-printed.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_16"></a>
<h2>Changes for Version 0.16.1 (2023-12-28)</h2>
  *  Fix some Sxn definitions to allow role-based UI customizations.
     (minor: webui)

<h2>Changes for Version 0.16.0 (2023-11-30)</h2>
  *  Sx function <code>define</code> is removed, as announced for version
     0.15.0. Use <code>defvar</code> (to define variables) or
     <code>defun</code> (to define functions) instead. In addition
     <code>defunconst</code> defines a constant function, which ensures a fixed
     binding of its name to its function body (performance optimization).
     (breaking: webui)
  *  Allow to determine a role zettel for a given role.
     (major: api, webui)
  *  Present user the option to create a (missing) role zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090004, which
     is a template for new role zettel.
     (minor: webui)
  *  Timestamp values can be abbreviated by omitting most of its components.
     Previously, such values that are not in the format YYYYMMDDhhmmss were
     ignored. Now the following formats are also allowed: YYYY, YYYYMM,
     YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm. Querying and sorting work accordingly.
     Previously, only a sequences of zeroes were appended, resulting in illegal
     timestamps, e.g. for YYYY or YYYYMM.
     (minor)
  *  SHTML encoder fixed w.r.t inline quoting. Previously, an &lt;q&gt; tag was
     used, which is inappropriate. Restored smart quotes from version 0.3, but
     with new SxHTML infrastructure. This affect the html encoder and the WebUI
     too. Now, an empty quote should not result in a warning by HTML linters.
     (minor: api, webui)
  *  Add new zettelmarkup inline formatting: <code>##Text##</code> will mark /
     highlight the given Text. It is typically used to highlight some text,
     which is important for you, but not for the original author. When rendered
     as HTML, the &lt;mark&gt; tag is used.
     (minor: zettelmarkup)
  *  Add configuration keys to show, not to show, or show the closed list of
     referencing zettel in the web user interface. You can set these
     configurations system-wide, per user, or per zettel. Often it is used to
     ensure a &ldquo;clean&rdquo; home zettel. Affects the list of incoming
     / back links, folge zettel, subordinate zettel, and successor zettel.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_15"></a>
<h2>Changes for Version 0.15.0 (2023-10-26)</h2>
  *  Sx function <code>define</code> is now deprecated. It will be removed in
     version 0.16. Use <code>defvar</code> or <code>defun</code> instead.
     Otherwise the WebUI will not work in version 0.16.
     (major: webui, deprecated)
  *  Zettel can be re-indexed via WebUI or API query action
     <code>REINDEX</code>. The info page of a zettel contains a link to
     re-index the zettel. In a query transclusion, this action is ignored.
     (major: api, webui).
  *  Allow to determine a tag zettel for a given tag.
     (major: api, webui)
  *  Present user the option to create a (missing) tag zettel (in list view).
     Results in a new predefined zettel with identifier 00000000090003, which
     is a template for new tag zettel.
     (minor: webui)
  *  ZIP file with manual now contains a zettel 00001000000000 that contains
     its build date (metadata key <code>created</code>) and version (in the
     zettel content)
     (minor)
  *  If an error page cannot be created due to template errors (or similar), a
     plain text error page is delivered instead. It shows the original error
     and the error that occurred during rendering the original error page.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a id="0_14"></a>
<h2>Changes for Version 0.14.0 (2023-09-22)</h2>
  *  Remove support for JSON. This was marked deprecated in version 0.12.0. Use
     the <code>data</code> encoding instead, a form of symbolic expressions.
     (breaking: api; minor: webui)
  *  Remove deprecated syntax for a context list: <code>CONTEXT zid</code>. Use
     <code>zid CONTEXT</code> instead. It was deprecated in version 0.13.0.
     (breaking: api, webui, zettelmarkup)
  *  Replace CSS-role-map mechanism with a more general Sx-based one: user
     specific code may generates parts of resulting HTML document.
     (breaking: webui)
  *  Allow meta-tags, i.e. zettel for a specific tag. Meta-tags have the tag
     name as a title and specify the role "tag".
     (major: webui)
  *  Allow to load sx code from multiple zettel; dependencies are specified
     using <code>precursor</code> metadata.
     (major: webui)
  *  Allow sx code to change WebUI for zettel with specified role.
     (major: webui)
  *  Some minor usability improvements.
     (minor: webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

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

<a id="0_12"></a>
<h2>Changes for Version 0.12.0 (2023-06-05)</h2>
  *  Syntax of templates for the web user interface are changed from Mustache
     to Sxn (S-Expressions). Mustache is no longer supported, nowhere in the
     software. Mustache was marked deprecated in version 0.11.0. If you
     modified the template zettel, you must adapt to the new syntax.
     (breaking: webui)
  *  Query expression is allowed to search for the "context" of a zettel.
     Previously, this was a separate call, without adding a search expression
     / action expression.
     (breaking)
  *  "sexpr" encoding is renamed to "sz" encoding. This will affect mostly the
     API. Additionally, all string "sexpr" are renamed to "sz" also. "Sz" is
     the short form for "symbolic expression for zettel", similar to "shtml"
     that is the short form for "symbolic expression for HTML".
     (breaking)
  *  Render footer zettel on all WebUI pages.
     (fix: webui)
  *  Query search operator "=" now compares for equality, ":" compares
     depending on the value type.
     (minor: api, webui)
  *  Search term <code>PICK</code> now respects the original sort order. This
     makes it more useful and orthogonal to <code>RANDOM</code> and
     <code>LIMIT</code>. As a side effect, zettel lists retrieved via the API
     are no longer sorted. In case you want a specific order, you must specify
     it explicit.
     (minor: api, webui)
  *  New metadata key <code>expire</code> records a timestamp when a zettel
     should be treated as, well, expired.
     (minor)
  *  New metadata keys <code>superior</code> and <code>subordinate</code>
     (calculated from <code>superior</code>) allow to specify a hierarchy
     between zettel.
     (minor)
  *  Metadata keys with suffix <code>-date</code> and <code>-time</code> are
     treated as
     timestamp values.
     (minor)
  *  <code>sexpr</code> zettel encoding is now documented in the manual.
     (minor: manual)
  *  Build tool allows to install / update external Go tools needed to build
     the software.
     (minor)
  *  Show only useful metadata on WebUI, not the internal metadata.
     (minor: webui)
  *  The use of the <code>json</code> zettel encoding is deprecated. Support
     for this encoding may be removed in future versions. Please use the new
     <code>data</code> encoding instead.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_11"></a>
<h2>Changes for Version 0.11.0 (pending)</h2>
<a id="0_11"></a>
<h2>Changes for Version 0.11.2 (2023-04-16)</h2>
  *  Render footer zettel on all WebUI pages. Backported from 0.12.0. Many
     thanks to HK for reporting it!
     (fix: webui)

<h2>Changes for Version 0.11.1 (2023-03-28)</h2>
  *  Make <code>PICK</code> search term a little bit more deterministic so that
     the &ldquo;Save As Zettel&rdquo; button produces the same list.
     (fix: webui)

<h2>Changes for Version 0.11.0 (2023-03-27)</h2>
  *  Remove ZJSON encoding. It was announced in version 0.10.0. Use Sexpr
     encoding instead.
     (breaking)
  *  Title of a zettel is no longer interpreted as Zettelmarkup text. Now it is
     just a plain string, possibly empty. Therefore, no inline formatting (like
     bold text), no links, no footnotes, no citations (the latter made
     rendering the title often questionable, in some contexts). If you used
     special entities, please use the Unicode characters directly. However, as
     a good practice, it is often the best to printable ASCII characters.
     (breaking)
  *  Remove runtime configuration <code>marker-external</code>. It was added in
     version [#0_0_6|0.0.6] and updated in [#0_0_10|0.0.10]. If you want to
     change the marker for an external URL, you could modify zettel
     00000000020001 (Zettelstore Base CSS) or zettel 00000000025001
     (Zettelstore User CSS, preferred) by changing / adding a rule to add some
     content after an external <code><a ...></code> tag.
     (breaking: webui)
  *  Add SHTML encoding. This allows to ensure the quality of generated HTML
     code. In addition, clients might use it, because it is easier to parse and
     manipulate than ordinary HTML. In the future, HTML template zettel will
     probably also use SHTML, deprecating the current Mustache syntax (which
     was added in [#0_0_9|0.0.9]).
     (major)
  *  Search term <code>PICK n</code>, where <code>n</code> is an integer value
     greater zero, will pick randomly <code>n</code> elements from the search
     result list. Somehow similar (and faster) as <code>RANDOM LIMIT n</code>,
     but allows also later ordering of the resulting list.
     (minor)
  *  Changed cost model for zettel context: a zettel with more
     outgoing/incoming references has higher cost than a zettel with less
     references. Also added support for traversing tags, with a similar cost
     model. As an effect, zettel hubs (in many cases your home zettel) will
     less likely add its references. Same for often used tags. The cost model
     might change in some details in the future, but the idea of a penalty
     applied to zettel / tags with many references will hold.
     (minor)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_10"></a>
<a id="0_10"></a>
<h2>Changes for Version 0.10.1 (2023-01-30)</h2>
  *  Show button to save a query into a zettel only when the current user has
     authorization to do it.
     (fix: webui)

<h2>Changes for Version 0.10.0 (2023-01-24)</h2>
  *  Remove support for endpoints <tt>/j, /m, /q, /p, /v</tt>. Their functions
     are merged into endpoint <tt>/z</tt>. This was announced in version 0.9.0.
     Please use only client library with at least version 0.10.0 too.
  *  Remove support for endpoints <code>/j, /m, /q, /p, /v</code>. Their
     functions are merged into endpoint <code>/z</code>. This was announced in
     version 0.9.0. Please use only client library with at least version 0.10.0
     too.
     (breaking: api)
  *  Remove support for runtime configuration key <tt>footer-html</tt>. Use
     <tt>footer-zettel</tt> instead. Deprecated in version 0.9.0.
  *  Remove support for runtime configuration key <code>footer-html</code>. Use
     <code>footer-zettel</code> instead. Deprecated in version 0.9.0.
     (breaking: webui)
  *  Save a query into a zettel to freeze it.
     (major: webui)
  *  Allow to show all used metadata keys, linked with their occurrences and
     their values.
     (minor: webui)
  *  Mark ZJSON encoding as deprecated for v0.11.0. Please use Sexpr encoding
     instead.
     (deprecated)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_9"></a>
<a id="0_9"></a>
<h2>Changes for Version 0.9.0 (2022-12-12)</h2>
  *  Remove support syntax <tt>pikchr</tt>. Although it was a nice idea to
  *  Remove support syntax <code>pikchr</code>. Although it was a nice idea to
     include it into Zettelstore, the implementation is too brittle (w.r.t. the
     expected long lifetime of Zettelstore). There should be other ways to
     support SVG front-ends.
     (breaking)
  *  Allow to upload content when creating / updating a zettel.
     (major: webui)
  *  Add syntax &ldquo;draw&rdquo; (again)
     (minor: zettelmarkup)
  *  Allow to encode zettel in Markdown. Please note: not every aspect of
     a zettel can be encoded in Markdown. Those aspects will be ignored.
     (minor: api)
  *  Enhance zettel context by raising the importance of folge zettel (and
     similar).
     (minor: api, webui)
  *  Interpret zettel files with extension <tt>.webp</tt> as an binary image
     file format.
  *  Interpret zettel files with extension <code>.webp</code> as an binary
     image file format.
     (minor)
  *  Allow to specify service specific log level via statup configuration and
  *  Allow to specify service specific log level via startup configuration and
     via command line.
     (minor)
  *  Allow to specify a zettel to serve footer content via runtime
     comfiguration <tt>footer-zettel</tt>. Can be overwritten by user zettel.
     configuration <code>footer-zettel</code>. Can be overwritten by user
     zettel.
     (minor: webui)
  *  Footer data is automatically separated by a thematic break / horizontal
     rule. If you do not like it, you have to update the base template.
     (minor: webui)
  *  Allow to set runtime configuration <tt>home-zettel</tt> in the user zettel
     to make it user-specific.
  *  Allow to set runtime configuration <code>home-zettel</code> in the user
     zettel to make it user-specific.
     (minor: webui)
  *  Serve favicon.ico from the asset directory.
     (minor: webui)
  *  Zettelmarkup cheat sheet
     (minor: manual)
  *  Runtime configuration key <tt>footer-html</tt> will be removed in Version
     0.10.0. Please use <tt>footer-zettel</tt> instead.
  *  Runtime configuration key <code>footer-html</code> will be removed in
     Version 0.10.0. Please use <code>footer-zettel</code> instead.
     (deprecated: webui)
  *  In the next version 0.10.0, the API endpoints for a zettel (<tt>/j</tt>,
     <tt>/p</tt>, <tt>/v</tt>) will be merged with endpoint <tt>/z</tt>. Basically,
     the previous endpoint will be refactored as query parameter of endpoint
     <tt>/z</tt>. To reduce errors, there will be no version, where the previous
     endpoint are still available and the new funnctionality is still there.
     This is a warning to prepare for some breaking changes in v0.10.0. This
     also affects the API client implementation.
  *  In the next version 0.10.0, the API endpoints for a zettel
     (<code>/j</code>, <code>/p</code>, <code>/v</code>) will be merged with
     endpoint <code>/z</code>. Basically, the previous endpoint will be
     refactored as query parameter of endpoint <code>/z</code>. To reduce
     errors, there will be no version, where the previous endpoint are still
     available and the new functionality is still there. This is a warning to
     prepare for some breaking changes in v0.10.0. This also affects the API
     client implementation.
     (warning: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_8"></a>
<a id="0_8"></a>
<h2>Changes for Version 0.8.0 (2022-10-20)</h2>
  *  Remove support for tags within zettel content. Removes also property
     metadata keys <tt>all-tags</tt> and <tt>computed-tags</tt>. Deprecated in
     version 0.7.0.
     metadata keys <code>all-tags</code> and <code>computed-tags</code>.
     Deprecated in version 0.7.0.
     (breaking: zettelmarkup, api, webui)
  *  Remove API endpoint <tt>/m</tt>, which retrieve aggregated (tags, roles)
     zettel identifier. Deprecated in version 0.7.0.
  *  Remove API endpoint <code>/m</code>, which retrieve aggregated (tags,
     roles) zettel identifier. Deprecated in version 0.7.0.
     (breaking: api)
  *  Remove support for URL query parameter starting with an underscore.
     Deprecated in version 0.7.0.
     (breaking: api, webui)
  *  Ignore HTML content by default, and allow HTML gradually by setting
     startup value <tt>insecure-html</tt>.
     startup value <code>insecure-html</code>.
     (breaking: markup)
  *  Endpoint <tt>/q</tt> returns list of full metadata, if no query action is
     specified. A HTTP call <tt>GET /z</tt> (retrieving metadata of all or some
     zettel) is now an alias for <tt>GET /q</tt>.
  *  Endpoint <code>/q</code> returns list of full metadata, if no query action
     is specified. A HTTP call <code>GET /z</code> (retrieving metadata of all
     or some zettel) is now an alias for <code>GET /q</code>.
     (major: api)
  *  Allow to create a zettel that acts as the new version of an existing
     zettel. Useful if you want to have access to older, outdated content.
     (minor: webui)
  *  Allow transclusion to reference local image via URL.
     (minor: zettelmarkup, webui)
  *  Add categories in RSS feed, based on zettel tags.
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
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
674







-
+






-
+









-
-
-
-
-
+
+
+
+
+


-
-
+
+
+

-
-
+
+








-
-
-
-
+
+
+
+

-
+


-
-
+
+









-
+

-
-
-
+
+
+
+

-
-
+
+

-
-
+
+





-
-
-
-
-
-
-
+
+
+
+
+
+
+








-
+













-
+

















-
+








-
+







     enabled.
     (minor: server)
  *  Fix error that a manual zettel deletion was not always detected.
     (bug: dirbox)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_7"></a>
<a id="0_7"></a>
<h2>Changes for Version 0.7.1 (2022-09-18)</h2>
  *  Produce a RSS feed compatible to Miniflux.
     (minor)
  *  Make sure to always produce a pubdata in RSS feed.
     (bug)
  *  Prefix search for data that looks like a zettel identifier may end with a
     <tt>0</tt>.
     <code>0</code>.
     (bug)
  *  Fix glitch on manual zettel.
     (bug)

<h2>Changes for Version 0.7.0 (2022-09-17)</h2>
  *  Removes support for URL query parameter to search for metadata values,
     sorting, offset, and limit a zettel list. Deprecated in version 0.6.0
     (breaking: api, webui)
  *  Allow to search for the existence / non-existence of a metadata key with
     the "?" operator: <tt>key?</tt> and <tt>key!?</tt>. Previously, the ":"
     operator was used for this by specifying an empty search value. Now you
     can use the ":" operator to find empty / non-empty metadata values. If you
     specify a search operator for metadata, the specified key is assumed to
     exist.
     the "?" operator: <code>key?</code> and <code>key!?</code>. Previously,
     the ":" operator was used for this by specifying an empty search value.
     Now you can use the ":" operator to find empty / non-empty metadata
     values. If you specify a search operator for metadata, the specified key
     is assumed to exist.
     (breaking: api, webui)
  *  Rename &ldquo;search expression&rdquo; into &ldquo;query
     expressions&rdquo;. Similar, the reference prefix <tt>search:</tt> to
     specify a query link or a query transclusion is renamed to <tt>query:</tt>
     expressions&rdquo;. Similar, the reference prefix <code>search:</code> to
     specify a query link or a query transclusion is renamed to
     <code>query:</code>
     (breaking: zettelmarkup)
  *  Rename query parameter for query expression from <tt>_s</tt> to
     <tt>q</tt>.
  *  Rename query parameter for query expression from <code>_s</code> to
     <code>q</code>.
     (breaking: api, webui)
  *  Cleanup names for HTTP query parameters in WebUI. Update your bookmarks
     if you used them. (For API: see below)
     (breaking: webui)
  *  Allow search terms to be OR-ed. This allows to specify any search
     expression in disjunctive normal form. Therefore, the NEGATE term is not
     needed any more.
     (breaking: api, webui)
  *  Replace runtime configuration <tt>default-lang</tt> with <tt>lang</tt>.
     Additionally, <tt>lang</tt> set at the zettel of the current user, will
     provide a default value for the current user, overwriting the global
     default value.
  *  Replace runtime configuration <code>default-lang</code> with
     <code>lang</code>. Additionally, <code>lang</code> set at the zettel of
     the current user, will provide a default value for the current user,
     overwriting the global default value.
     (breaking)
  *  Add new syntax <tt>pikchr</tt>, a markup language for diagrams in
  *  Add new syntax <code>pikchr</code>, a markup language for diagrams in
     technical documentation.
     (major)
  *  Add endpoint <tt>/q</tt> to query the zettelstore and aggregate resulting
     values. This is done by extending the query syntax.
  *  Add endpoint <code>/q</code> to query the zettelstore and aggregate
     resulting values. This is done by extending the query syntax.
     (major: api)
  *  Add support for query actions. Actions may aggregate w.r.t. some metadata
     keys, or produce an RSS feed.
     (major: api, webui)
  *  Query results can be ordered for more than one metadata key. Ordering by
     zettel identifier is an implicit last order expression to produce stable
     results.
     (minor: api, webui)
  *  Add support for an asset directory, accessible via URL prefix
     <tt>/assests/</tt>.
     <code>/assests/</code>.
     (minor: server)
  *  Add support for metadata key <tt>created</tt>, a timestamp when the zettel
     was created. Since key <tt>published</tt> is now either <tt>created</tt>
     or <tt>modified</tt>, it will now always contains a valid time stamp.
  *  Add support for metadata key <code>created</code>, a time stamp when the
     zettel was created. Since key <code>published</code> is now either
     <code>created</code> or <code>modified</code>, it will now always contains
     a valid time stamp.
     (minor)
  *  Add support for metadata key <tt>author</tt>. It will be displayed on a
     zettel, if set.
  *  Add support for metadata key <code>author</code>. It will be displayed on
     a zettel, if set.
     (minor: webui)
  *  Remove CSS for lists. The browsers default value for <tt>padding-left</tt>
     will be used.
  *  Remove CSS for lists. The browsers default value for
     <code>padding-left</code> will be used.
     (minor: webui)
  *  Removed templates for rendering roles and tags lists. This is now done by
     query actions.
     (minor: webui)
  *  Tags within zettel content are deprecated in version 0.8. This affects the
     computed metadata keys <tt>content-tags</tt> and <tt>all-tags</tt>. They
     will be removed. The number sign of a content tag introduces unintended
     tags, esp. in the english language; content tags may occur within links
     &rarr; links within links, when rendered as HTML; content tags may occur
     in the title of a zettel; naming of content tags, zettel tags, and their
     union is confusing for many. Migration: use zettel tags or replace content
     tag with a search.
     computed metadata keys <code>content-tags</code> and
     <code>all-tags</code>. They will be removed. The number sign of a content
     tag introduces unintended tags, esp. in the English language; content tags
     may occur within links &rarr; links within links, when rendered as HTML;
     content tags may occur in the title of a zettel; naming of content tags,
     zettel tags, and their union is confusing for many. Migration: use zettel
     tags or replace content tag with a search.
     (deprecated: zettelmarkup)
  *  Cleanup names for HTTP query parameter for API calls. Essentially,
     underscore characters in front are removed. Please use new names, old
     names will be deprecated in version 0.8.
     (deprecated: api)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_6"></a>
<a id="0_6"></a>
<h2>Changes for Version 0.6.2 (2022-08-22)</h2>
  *  Recognize renaming of zettel file external to Zettelstore.
     (bug)

<h2>Changes for Version 0.6.1 (2022-08-22)</h2>
  *  Ignore empty tags when reading metadata.
     (bug)

<h2>Changes for Version 0.6.0 (2022-08-11)</h2>
  *  Translating of "..." into horizontal ellipsis is no longer supported. Use
     &amp;hellip; instead.
     (breaking: zettelmarkup)
  *  Allow to specify search expressions, which allow to specify search
     criterias by using a simple syntax. Can be specified in WebUI's search box
     criteria by using a simple syntax. Can be specified in WebUI's search box
     and via the API by using query parameter "_s".
     (major: api, webui)
  *  A link reference is allowed to be a search expression. The WebUI will
     render this as a link to a list of zettel that satisfy the search
     expression.
     (major: zettelmarkup, webui)
  *  A block transclusion is allowed to specify a search expression. When
     evaluated, the transclusion is replaced by a list of zettel that satisfy
     the search expression.
     (major: zettelmarkup)
  *  When presenting a zettel list, allow to change the search expression.
     (minor: webui)
  *  When evaluating a zettel, ignore transclusions if current user is not
     allowed to read transcluded zettel.
     (minor)
  *  Added a small tutorial for Zettelmarkup.
     (minor: manual)
  *  Using URL query parameter to search for metdata values, specify an
  *  Using URL query parameter to search for metadata values, specify an
     ordering, an offset, and a limit for the resulting list, will be removed
     in version 0.7. Replace these with the more useable search expressions.
     Please be aware that the = search operator is also deprecated. It was only
     introduced to help the migration.
     (deprecated: api, webui)
  *  Some smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_5_0"></a>
<a id="0_5_0"></a>
<h2>Changes for Version 0.5.1 (2022-08-02)</h2>
  *  Log missing authentication tokens in debug level (was: sense level)
     (major)
  *  Allow to use empty metadata values of string and zmk types.
     (minor)
  *  Add IP address to some log messages, esp. when authentication fails.
     (minor)
264
265
266
267
268
269
270
271
272
273
274




275
276
277
278
279
280
281
682
683
684
685
686
687
688




689
690
691
692
693
694
695
696
697
698
699







-
-
-
-
+
+
+
+







  *  If authentication is enabled, a secret of at least 16 bytes must be set in
     the startup configuration.
     (breaking)
  *  &ldquo;Sexpr&rdquo; encoding replaces &ldquo;Native&rdquo; encoding. Sexpr
     encoding is much easier to parse, compared with native and ZJSON encoding.
     In most cases it is smaller than ZJSON.
     (breaking: api)
  *  Endpoint <tt>/r</tt> is changed to <tt>/m?_key=role</tt> and returns now
     a map of role names to the list of zettel having this role. Endpoint
     <tt>/t</tt> is changed to <tt>/m?_key=tags</tt>. It already returned
     mapping described before.
  *  Endpoint <code>/r</code> is changed to <code>/m?_key=role</code> and
     returns now a map of role names to the list of zettel having this role.
     Endpoint <code>/t</code> is changed to <code>/m?_key=tags</code>. It
     already returned mapping described before.
     (breaking: api)
  *  Remove support for a default value for metadata key title, role, and
     syntax. Title and role are now allowed to be empty, an empty syntax value
     defaults to &ldquo;plain&rdquo;.
     (breaking)
  *  Add support for an &ldquo;evaluation block&rdquo; syntax in Zettelmarkup
     to allow interpretation of content by external software.
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
713
714
715
716
717
718
719

720
721
722
723
724
725
726
727
728
729
730
731

732
733
734
735
736


737
738
739
740


741
742
743
744
745
746
747
748
749







-
+











-
+




-
-
+
+


-
-
+
+







     Zettelstore WebUI.
     (minor: webui)
  *  A zettel can be saved while creating / editing it. There is no need to
     manually re-edit it by using the 'e' endpoint.
     (minor: webui)
  *  Zettel role and zettel syntax are backed by a HTML5 data list element
     which lists supported and used values to help to enter a valid value.
     (mirnor: webui)
     (minor: webui)
  *  Allow to use startup configuration, even if started in simple mode.
     (minor)
  *  Log authentication issues in level "sense"; add caller IP address to some
     web server log messages.
     (minor: web server)
  *  New startup configuration key <kbd>max-request-size</kbd> to limit a web
     request body to prevent client sending too large requests.
     (minor: web server)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_4"></a>
<a id="0_4"></a>
<h2>Changes for Version 0.4 (2022-03-08)</h2>
  *  Encoding &ldquo;djson&rdquo; renamed to &ldquo;zjson&rdquo; (<em>zettel
     json</em>).
     (breaking: api; minor: webui)
  *  Remove inline quotation syntax <tt>&lt;&lt;...&lt;&lt;</tt>. Now,
     <tt>&quot;&quot;...&quot;&quot;</tt> generates the equivalent code.
  *  Remove inline quotation syntax <code>&lt;&lt;...&lt;&lt;</code>. Now,
     <code>&quot;&quot;...&quot;&quot;</code> generates the equivalent code.
     Typographical quotes are generated by the browser, not by Zettelstore.
     (breaking: Zettelmarkup)
  *  Remove inline formatting for monospace. Its syntax is now used by the
     similar syntax element of literal computer input. Monospace was just
  *  Remove inline formatting for mono space. Its syntax is now used by the
     similar syntax element of literal computer input. Mono space was just
     a visual element with no semantic association. Now, the syntax
     <kbd>++...++</kbd> is obsolete.
     (breaking: Zettelmarkup).
  *  Remove API call to parse Zettelmarkup texts and encode it as text and
     HTML. Was call &ldquo;POST /v&rdquo;. It was needed to separately encode
     the titles of zettel. The same effect can be achieved by fetching the
     ZJSON representation and encode it using the function in the Zettelstore
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
762
763
764
765
766
767
768



769
770
771
772
773
774

775
776
777
778
779
780
781
782
783
784
785
786
787

788
789


790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809

810
811
812

813
814
815
816
817
818






819
820
821
822
823
824
825
826
827
828
829

830
831
832
833
834
835



836
837
838
839
840
841
842
843
844
845

846
847
848
849


850
851
852

853
854
855
856
857
858
859
860
861
862
863

864
865
866

867
868


869
870
871

872
873
874
875
876

877
878
879
880
881
882
883

884
885
886








887
888
889
890
891
892
893
894
895
896

897
898

899

900
901
902
903
904

905
906
907
908
909

910
911
912
913
914

915
916
917



918
919
920
921
922
923
924
925
926
927







-
-
-
+
+
+



-
+












-
+

-
-
+
+


















-
+


-
+





-
-
-
-
-
-
+
+
+
+
+
+





-
+





-
-
-
+
+
+
+






-
+



-
-
+
+

-
+










-
+


-
+

-
-
+
+

-
+




-
+






-
+


-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+

-
+

-
+
-





-
+




-
+




-
+
+

-
-
-
+
+
+







     interpreted as Zettelmarkup. Similar, the suffix <kbd>-set</kbd> denotes
     a set/list of words and the suffix <kbd>-zids</kbd> a set/list of zettel
     identifier.
     (minor: api, webui)
  *  Change generated URLs for zettel-creation forms. If you have bookmarked
     them, e.g. to create a new zettel, you should update.
     (minor: webui)
  *  Remove support for metadata key <tt>no-index</tt> to suppress indexing
     selected zettel. It was introduced in <a href="#0_0_11">v0.0.11</a>, but
     disallows some future optimizations for searching zettel.
  *  Remove support for metadata key <code>no-index</code> to suppress indexing
     selected zettel. It was introduced in [#0_0_11|v0.0.11], but disallows
     some future optimizations for searching zettel.
     (minor: api, webui)
  *  Make some metadata-based searches a little bit faster by executing
     a (in-memory-based) full-text search first. Now only those zettel are
     loaded from file that contain the metdata value.
     loaded from file that contain the metadata value.
     (minor: api, webui)
  *  Add an API call to retrieve the version of the Zettelstore.
     (minor: api)
  *  Limit the amount of zettel and bytes to be stored in a memory box. Allows
     to use it with public access.
     (minor: box)
  *  Disallow to cache the authentication cookie. Will remove most unexpected
     log-outs when using a mobile device.
     (minor: webui)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_3"></a>
<a id="0_3"></a>
<h2>Changes for Version 0.3 (2022-02-09)</h2>
  *  Zettel files with extension <tt>.meta</tt> are now treated as content
     files. Previoulsy, they were interpreted as metadata files. The
  *  Zettel files with extension <code>.meta</code> are now treated as content
     files. Previously, they were interpreted as metadata files. The
     interpretation as metadata files was deprecated in version 0.2.
     (breaking: directory and file/zip box)
  *  Add syntax &ldquo;draw&rdquo; to produce some graphical representations.
     (major)
  *  Add Zettelmarkup syntax to specify full transclusion of other zettel.
     (major: Zettelmarkup)
  *  Add Zettelmarkup syntax to specify inline-zettel, both for
     block-structured and for inline-structured elements.
     (major: Zettelmarkup)
  *  Metadata-returning API calls additionally return an indication about
     access rights for the given zettel.
     (minor: api)
  *  A previously duplicate file that is now useful (because another file was
     deleted) is now logged as such.
     (minor: directory and file/zip box)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_2"></a>
<a id="0_2"></a>
<h2>Changes for Version 0.2 (2022-01-19)</h2>
  *  v0.2.1 (2021-02-01) updates the license year in some documents
  *  Remove support for <tt>;;small text;;</tt> Zettelmarkup.
  *  Remove support for <code>;;small text;;</code> Zettelmarkup.
     (breaking: Zettelmarkup)
  *  On macOS, the downloadable executable program is now called
     &ldquo;zettelstore&rdquo;, as on all other Unix-like platforms.
     (possibly breaking: macOS)
  *  External metadata (e.g. for zettel with file extension other than
     <tt>.zettel</tt>) are stored in files without an extension. Metadata files
     with extension <tt>.meta</tt> are still recognized, but result in
     a warning message. In a future version (probably v0.3), <tt>.meta</tt>
     files will be treated as ordinary content files, possibly resulting in
     duplicate content. In other words: usage of <tt>.meta</tt> files for
     storing metadata is deprecated.
     <code>.zettel</code>) are stored in files without an extension. Metadata
     files with extension <code>.meta</code> are still recognized, but result
     in a warning message. In a future version (probably v0.3),
     <code>.meta</code> files will be treated as ordinary content files,
     possibly resulting in duplicate content. In other words: usage of
     <code>.meta</code> files for storing metadata is deprecated.
     (possibly breaking: directory and file box)
  *  Show unlinked references in info page of each zettel. Unlinked references
     are phrases within zettel content that might reference another zettel with
     the same title as the phase.
     (major: webui)
  *  Add endpoint <tt>/u/{ID}</tt> to retrieve unlinked references.
  *  Add endpoint <code>/u/{ID}</code> to retrieve unlinked references.
     (major: api)
  *  Provide a logging facility.
     Log messages are written to standard output. Messages with level
     &ldquo;information&rdquo; are also written to a circular buffer (of length
     8192) which can be retrieved via a computed zettel. There is a command
     line flag <tt>-l LEVEL</tt> to specify an application global logging level
     on startup (default: &ldquo;information&rdquo;). Logging level can also be
     changed via the administrator console, even for specific (sub-) services.
     line flag <code>-l LEVEL</code> to specify an application global logging
     level on startup (default: &ldquo;information&rdquo;). Logging level can
     also be changed via the administrator console, even for specific (sub-)
     services.
     (major)
  *  The internal handling of zettel files is rewritten. This allows less
     reloads ands detects when the directory containing the zettel files is
     removed. The API, WebUI, and the admin console allow to manually refresh
     the internal state on demand.
     (major: box, webui)
  *  <tt>.zettel</tt> files with YAML header are now correctly written.
  *  <code>.zettel</code> files with YAML header are now correctly written.
     (bug)
  *  Selecting zettel based on their metadata allows the same syntax as
     searching for zettel content. For example, you can list all zettel that
     have an identifier not ending with <tt>00</tt> by using the query
     <tt>id=!&lt;00</tt>.
     have an identifier not ending with <code>00</code> by using the query
     <code>id=!&lt;00</code>.
     (minor: api, webui)
  *  Remove support for <tt>//deprecated emphasized//</tt> Zettelmarkup.
  *  Remove support for <code>//deprecated emphasized//</code> Zettelmarkup.
     (minor: Zettelmarkup)
  *  Add options to profile the software. Profiling can be enabled at the
     command line or via the administrator console.
     (minor)
  *  Add computed zettel that lists all supported parser / recognized zettel
     syntaxes.
     (minor)
  *  Add API call to check for enabled authentication.
     (minor: api)
  *  Renewing an API access token works even if authentication is not enabled.
     This corresponds to the behaviour of optaining an access token.
     This corresponds to the behaviour of obtaining an access token.
     (minor: api)
  *  If there is nothing to return, use HTTP status code 204, instead of 200 +
     <tt>Content-Length: 0</tt>.
     <code>Content-Length: 0</code>.
     (minor: api)
  *  Metadata key <tt>duplicates</tt> stores the duplicate file names, instead
     of just a boolean value that there were duplicate file names.
  *  Metadata key <code>duplicates</code> stores the duplicate file names,
     instead of just a boolean value that there were duplicate file names.
     (minor)
  *  Document autostarting Zettelstore on Windows, macOS, and Linux.
  *  Document auto starting Zettelstore on Windows, macOS, and Linux.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_1"></a><a name="0_1_0"></a>
<a id="0_1"></a><a id="0_1_0"></a>
<h2>Changes for Version 0.1 (2021-11-11)</h2>
  *  v0.1.3 (2021-12-15) fixes a bug where the modification date could be set
     when a new zettel is created.
  *  v0.1.2 (2021-11-18) fixes a bug when selecting zettel from a list when
     more than one comparison is negated.
  *  v0.1.1 (2021-11-12) updates the documentation, mostly related to the
     deprecation of the <tt>//</tt> markup.
     deprecation of the <code>//</code> markup.
  *  Remove visual Zettelmarkup (italic, underline). Semantic Zettelmarkup
     (emphasize, insert) is still allowed, but got a different syntax. The new
     syntax for <ins>inserted text</ins> is <tt>&gt;&gt;inserted&gt;&gt;</tt>,
     while its previous syntax now denotes <em>emphasized text</em>:
     <tt>__emphasized__</tt>. The previous syntax for emphasized text is now
     deprecated: <tt>//deprecated emphasized//</tt>. Starting with
     Version&nbsp;0.2.0, the deprecated syntax will not be supported. The
     reason is the collision with URLs that also contain the characters
     <tt>//</tt>. The ZMK encoding of a zettel may help with the transition
     (<tt>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</tt>, on the Info page of
     syntax for <ins>inserted text</ins> is
     <code>&gt;&gt;inserted&gt;&gt;</code>, while its previous syntax now
     denotes <em>emphasized text</em>: <code>__emphasized__</code>. The
     previous syntax for emphasized text is now deprecated: <code>//deprecated
     emphasized//</code>. Starting with Version&nbsp;0.2.0, the deprecated
     syntax will not be supported. The reason is the collision with URLs that
     also contain the characters <code>//</code>. The ZMK encoding of a zettel
     may help with the transition
     (<code>/v/{ZettelID}?_part=zettel&amp;_enc=zmk</code>, on the Info page of
     each zettel in the WebUI). Additionally, all deprecated uses of
     <tt>//</tt> will be rendered with a dashed box within the WebUI.
     <code>//</code> will be rendered with a dashed box within the WebUI.
     (breaking: Zettelmarkup).
  *  API client software is now a [https://zettelstore.de/client/|separate]
  *  API client software is now a separate project.
     project.
     (breaking)
  *  Initial support for HTTP security headers (Content-Security-Policy,
     Permissions-Policy, Referrer-Policy, X-Content-Type-Options,
     X-Frame-Options). Header values are currently some constant values.
     (possibly breaking: api, webui)
  *  Remove visual Zettelmarkup (bold, striketrough). Semantic Zettelmarkup
  *  Remove visual Zettelmarkup (bold, strike through). Semantic Zettelmarkup
     (strong, delete) is still allowed and replaces the visual elements
     syntactically. The visual appearance should not change (depends on your
     changes / additions to CSS zettel).
     (possibly breaking: Zettelmarkup).
  *  Add API endpoint <tt>POST /v</tt> to retrieve HTMl and text encoded
  *  Add API endpoint <code>POST /v</code> to retrieve HTMl and text encoded
     strings from given ZettelMarkup encoded values. This will be used to
     render a HTML page from a given zettel: in many cases the title of
     a zettel must be treated separately.
     (minor: api)
  *  Add API endpoint <tt>/m</tt> to retrieve only the metadata of a zettel.
  *  Add API endpoint <code>/m</code> to retrieve only the metadata of
     a zettel.
     (minor: api)
  *  New metadata value <tt>content-tags</tt> contains the tags that were given
     in the zettel content. To put it simply, <tt>all-tags</tt> = <tt>tags</tt>
     + <tt>content-tags</tt>.
  *  New metadata value <code>content-tags</code> contains the tags that were
     given in the zettel content. To put it simply, <code>all-tags</code>
     = <code>tags</code> + <code>content-tags</code>.
     (minor)
  *  Calculating the context of a zettel stops at the home zettel.
     (minor: api, webui)
  *  When renaming or deleting a zettel, a warning will be given, if other
     zettel references the given zettel, or when &ldquo;deleting&rdquo; will
     uncover zettel in overlay box.
     (minor: webui)
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
674
675
676
677
678
679
680
681
682
683
684
685



686
687
688
689
690

691
692
693
694
695
696
697
698
699
700
701
702

703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722


723
724
725
726
727
728


729
730
731

732
733
734
735
736




737
738
739
740




741
742
743
744
745




746
747
748
749
750
751
752







753
754
755
756
757
758







759
760
761


762
763


764
765

766
767
768
769


770
771


772
773


774
775

776
777
778
779
780
781
782
783

784
785
786
787
788
789


790
791
792
793
794
795
796
797
798
799
800







801
802
803
804
805
806
807

808
809
810

811
812
813

814
815
816
817


818
819
820
821
822
823
824
825
826
827
828
829



830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849



850
851
852
853
854
855

856
857
858
859
860



861
862
863
864


865
866

867
868
869
870
871
872





873
874
875
876
877

878
879
880
881
882
883
884
885




886
887
888

889
890
891



892
893
894
895
896
897
898
899
900
901






902
903
904
905
906


907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927


928
929
930
931

932
933
934
935
936
937
938
939
940
941
942
943

944
945
946
947
948
949
950
951

952
953
954
955
956
957
958





959
960
961
962
963
964





965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981

982
983
984
985
986
987
988
936
937
938
939
940
941
942

943
944
945





946
947
948
949
950
951

952


953
954
955



956
957
958
959
960

961
962
963
964
965
966
967
968







969
970
971
972
973
974
975
976
977
978
979
980
981
982
983



984
985
986
987
988
989


990
991
992
993
994
995
996
997

998
999
1000



1001
1002
1003
1004
1005
1006
1007
1008
1009


1010
1011
1012



1013
1014
1015
1016
1017
1018


1019
1020
1021
1022

1023
1024
1025
1026
1027
1028

1029
1030


1031
1032
1033

1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047

1048
1049



1050
1051
1052
1053


1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065


1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086


1087
1088
1089
1090
1091
1092

1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104



1105
1106
1107
1108
1109
1110
1111

1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123

1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143

1144
1145
1146
1147
1148
1149
1150

1151
1152
1153
1154

1155
1156
1157



1158
1159
1160
1161
1162



1163
1164
1165
1166
1167




1168
1169
1170
1171
1172






1173
1174
1175
1176
1177
1178
1179
1180





1181
1182
1183
1184
1185
1186
1187
1188
1189

1190
1191
1192

1193
1194
1195

1196
1197
1198
1199

1200
1201
1202

1203
1204
1205

1206
1207
1208

1209
1210
1211
1212
1213
1214
1215
1216

1217
1218
1219
1220
1221


1222
1223
1224
1225
1226
1227







1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240

1241
1242
1243

1244
1245
1246

1247
1248
1249


1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261


1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281



1282
1283
1284
1285
1286
1287
1288
1289

1290
1291
1292



1293
1294
1295
1296
1297


1298
1299
1300

1301
1302





1303
1304
1305
1306
1307
1308
1309
1310
1311

1312
1313
1314
1315
1316




1317
1318
1319
1320
1321
1322

1323
1324


1325
1326
1327
1328
1329
1330
1331
1332





1333
1334
1335
1336
1337
1338
1339
1340
1341


1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362


1363
1364
1365
1366
1367

1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379

1380
1381
1382
1383
1384
1385
1386
1387

1388
1389
1390
1391




1392
1393
1394
1395
1396
1397





1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418

1419
1420
1421
1422
1423
1424
1425
1426







-
+


-
-
-
-
-
+
+
+
+
+
+
-

-
-
+
+

-
-
-
+
+
+


-
+







-
-
-
-
-
-
-
+
+
+
+
+
+
+








-
-
-
+
+
+



-
-
+
+






-
+


-
-
-
+
+
+






-
-
+
+

-
-
-
+
+
+
+


-
-
+
+


-
+





-
+

-
-
+
+

-
+













-
+

-
-
-
+
+
+

-
-
+
+










-
-
+
+
+


















-
-
+
+




-
+











-
-
-
+
+
+




-
+











-
+



















-
+
+





-
+
+


-
+


-
-
-
+
+
+
+

-
-
-
+
+
+
+

-
-
-
-
+
+
+
+

-
-
-
-
-
-
+
+
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+
+
+


-
+
+

-
+
+

-
+



-
+
+

-
+
+

-
+
+

-
+







-
+




-
-
+
+




-
-
-
-
-
-
-
+
+
+
+
+
+
+






-
+


-
+


-
+


-
-
+
+










-
-
+
+
+

















-
-
-
+
+
+





-
+


-
-
-
+
+
+


-
-
+
+

-
+

-
-
-
-
-
+
+
+
+
+




-
+




-
-
-
-
+
+
+
+


-
+

-
-
+
+
+





-
-
-
-
-
+
+
+
+
+
+



-
-
+
+



















-
-
+
+



-
+











-
+







-
+



-
-
-
-
+
+
+
+
+

-
-
-
-
-
+
+
+
+
+
















-
+







  *  Separate repository for [https://zettelstore.de/contrib/|contributed]
     software. First entry is a software for creating a presentation by using
     zettel.
     (info)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_15"></a>
<a id="0_0_15"></a>
<h2>Changes for Version 0.0.15 (2021-09-17)</h2>
  *  Move again endpoint characters for authentication to make room for future
     features. WebUI authentication moves from <tt>/a</tt> to <tt>/i</tt>
     (login) and <tt>/i?logout</tt> (logout). API authentication moves from
     <tt>/v</tt> to </tt>/a</tt>. JSON-based basic zettel handling moves from
     <tt>/z</tt> to <tt>/j</tt> and <tt>/z/{ID}</tt> to <tt>/j/{ID}</tt>. Since
     the API client is updated too, this should not be a breaking change for
     features. WebUI authentication moves from <code>/a</code> to
     <code>/i</code> (login) and <code>/i?logout</code> (logout). API
     authentication moves from <code>/v</code> to </code>/a</code>. JSON-based
     basic zettel handling moves from <code>/z</code> to <code>/j</code> and
     <code>/z/{ID}</code> to <code>/j/{ID}</code>. Since the API client is
     updated too, this should not be a breaking change for most users.
     most users.
     (minor: api, webui; possibly breaking)
  *  Add API endpoint <tt>/v/{ID}</tt> to retrieve an evaluated zettel in
     various encodings. Mostly replaces endpoint <tt>/z/{ID}</tt> for other
  *  Add API endpoint <code>/v/{ID}</code> to retrieve an evaluated zettel in
     various encodings. Mostly replaces endpoint <code>/z/{ID}</code> for other
     encodings except &ldquo;json&rdquo; and &ldquo;raw&rdquo;. Endpoint
     <tt>/j/{ID}</tt> now only returns JSON data, endpoint <tt>/z/{ID}</tt> is
     used to retrieve plain zettel data (previously called &ldquo;raw&rdquo;).
     See documentation for details.
     <code>/j/{ID}</code> now only returns JSON data, endpoint
     <code>/z/{ID}</code> is used to retrieve plain zettel data (previously
     called &ldquo;raw&rdquo;). See documentation for details.
     (major: api; breaking)
  *  Metadata values of type <em>tag set</em> (the metadata with key
     <tt>tags</tt> is its most prominent example), are now compared in
     <code>tags</code> is its most prominent example), are now compared in
     a case-insensitive manner. Tags that only differ in upper / lower case
     character are now treated identical. This might break your workflow, if
     you depend on case-sensitive comparison of tag values. Tag values are
     translated to their lower case equivalent before comparing them and when
     you edit a zettel through Zettelstore. If you just modify the zettel
     files, your tag values remain unchanged.
     (major; breaking)
  *  Endpoint <tt>/z/{ID}</tt> allows the same methods as endpoint
     <tt>/j/{ID}</tt>: <tt>GET</tt> retrieves zettel (see above), <tt>PUT</tt>
     updates a zettel, <tt>DELETE</tt> deletes a zettel, <tt>MOVE</tt> renames
     a zettel. In addtion, <tt>POST /z</tt> will create a new zettel. When
     zettel data must be given, the format is plain text, with metadata
     separated from content by an empty line. See documentation for more
     details.
  *  Endpoint <code>/z/{ID}</code> allows the same methods as endpoint
     <code>/j/{ID}</code>: <code>GET</code> retrieves zettel (see above),
     <code>PUT</code> updates a zettel, <code>DELETE</code> deletes a zettel,
     <code>MOVE</code> renames a zettel. In addition, <code>POST /z</code> will
     create a new zettel. When zettel data must be given, the format is plain
     text, with metadata separated from content by an empty line. See
     documentation for more details.
     (major: api (plus WebUI for some details))
  *  Allows to transclude / expand the content of another zettel into a target
     zettel when the zettel is rendered. By using the syntax of embedding an
     image (which is some kind of expansion too), the first top-level paragraph
     of a zettel may be transcluded into the target zettel. Endless recursion
     is checked, as well as a possible &ldquo;transclusion bomb &rdquo;
     (similar to a XML bomb). See manual for details.
     (major: zettelmarkup)
  *  The endpoint <tt>/z</tt> allows to list zettel in a simpler format than
     endpoint <tt>/j</tt>: one line per zettel, and only zettel identifier plus
     zettel title.
  *  The endpoint <code>/z</code> allows to list zettel in a simpler format
     than endpoint <code>/j</code>: one line per zettel, and only zettel
     identifier plus zettel title.
     (minor: api)
  *  Folgezettel are now displayed with full title at the bottom of a page.
     (minor: webui)
  *  Add API endpoint <tt>/p/{ID}</tt> to retrieve a parsed, but not evaluated
     zettel in various encodings.
  *  Add API endpoint <code>/p/{ID}</code> to retrieve a parsed, but not
     evaluated zettel in various encodings.
     (minor: api)
  *  Fix: do not list a shadowed zettel that matches the select criteria.
     (minor)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_14"></a>
<a id="0_0_14"></a>
<h2>Changes for Version 0.0.14 (2021-07-23)</h2>
  *  Rename &ldquo;place&rdquo; into &ldquo;box&rdquo;. This also affects the
     configuration keys to specify boxes <tt>box-uri<em>X</em></tt> (previously
     <tt>place-uri-<em>X</em></tt>. Older changes documented here are renamed
     too.
     configuration keys to specify boxes <code>box-uri<em>X</em></code>
     (previously <code>place-uri-<em>X</em></code>. Older changes documented
     here are renamed too.
     (breaking)
  *  Add API for creating, updating, renaming, and deleting zettel.
     (major: api)
  *  Initial API client for Go.
     (major: api)
  *  Remove support for paging of WebUI list. Runtime configuration key
     <tt>list-page-size</tt> is removed. If you still specify it, it will be
     ignored.
     <code>list-page-size</code> is removed. If you still specify it, it will
     be ignored.
     (major: webui)
  *  Use endpoint <tt>/v</tt> for user authentication via API. Endpoint
     <tt>/a</tt> is now used for the web user interface only. Similar, endpoint
     <tt>/y</tt> (&ldquo;zettel context&rdquo;) is renamed to <tt>/x</tt>.
  *  Use endpoint <code>/v</code> for user authentication via API. Endpoint
     <code>/a</code> is now used for the web user interface only. Similar,
     endpoint <code>/y</code> (&ldquo;zettel context&rdquo;) is renamed to
     <code>/x</code>.
     (minor, possibly breaking)
  *  Type of used-defined metadata is determined by suffix of key:
     <tt>-number</tt>, <tt>-url</tt>, <tt>-zid</tt> will result the values to
     be interpreted as a number, an URL, or a zettel identifier.
     <code>-number</code>, <code>-url</code>, <code>-zid</code> will result the
     values to be interpreted as a number, an URL, or a zettel identifier.
     (minor, but possibly breaking if you already used a metadata key with
     above suffixes, but as a string type)
  *  New <tt>user-role</tt> &ldquo;creator&rdquo;, which is only allowed to
  *  New <code>user-role</code> &ldquo;creator&rdquo;, which is only allowed to
     create new zettel (except user zettel). This role may only read and update
     public zettel or its own user zettel. Added to support future client
     software (e.g. on a mobile device) that automatically creates new zettel
     but, in case of a password loss, should not allow to read existing zettel.
     (minor, possibly breaking, because new zettel template zettel must always
     prepend the string <tt>new-</tt> before metdata keys that should be
     prepend the string <code>new-</code> before metadata keys that should be
     transferred to the new zettel)
  *  New suported metadata key <tt>box-number</tt>, which gives an indication
     from which box the zettel was loaded.
  *  New supported metadata key <code>box-number</code>, which gives an
     indication from which box the zettel was loaded.
     (minor)
  *  New supported syntax <tt>html</tt>.
  *  New supported syntax <code>html</code>.
     (minor)
  *  New predefined zettel &ldquo;User CSS&rdquo; that can be used to redefine
     some predefined CSS (without modifying the base CSS zettel).
     (minor: webui)
  *  When a user moves a zettel file with additional characters into the box
     directory, these characters are preserved when zettel is updated.
     (bug)
  *  The phase &ldquo;filtering a zettel list&rdquo; is more precise
     &ldquo;selecting zettel&rdquo;
     (documentation)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_13"></a>
<a id="0_0_13"></a>
<h2>Changes for Version 0.0.13 (2021-06-01)</h2>
  *  Startup configuration <tt>box-<em>X</em>-uri</tt> (where <em>X</em> is a
     number greater than zero) has been renamed to
     <tt>box-uri-<em>X</em></tt>.
  *  Startup configuration <code>box-<em>X</em>-uri</code> (where <em>X</em> is
     a number greater than zero) has been renamed to
     <code>box-uri-<em>X</em></code>.
     (breaking)
  *  Web server processes startup configuration <tt>url-prefix</tt>. There is
     no need for stripping the prefix by a front-end web server any more.
  *  Web server processes startup configuration <code>url-prefix</code>. There
     is no need for stripping the prefix by a front-end web server any more.
     (breaking: webui, api)
  *  Administrator console (only optional accessible locally). Enable it only
     on systems with a single user or with trusted users. It is disabled by
     default.
     (major: core)
  *  Remove visibility value &ldquo;simple-expert&rdquo; introduced in
     [#0_0_8|version 0.0.8]. It was too complicated, esp. authorization. There
     was a name collision with the &ldquo;simple&rdquo; directory box sub-type.
     (major)
  *  For security reasons, HTML blocks are not encoded as HTML if they contain
     certain snippets, such as <tt>&lt;script</tt> or <tt>&lt;iframe</tt>.
     These may be caused by using CommonMark as a zettel syntax.
     certain snippets, such as <code>&lt;script</code> or
     <code>&lt;iframe</code>. These may be caused by using CommonMark as
     a zettel syntax.
     (major)
  *  Full-text search can be a prefix search or a search for equal words, in
     addition to the search whether a word just contains word of the search
     term.
     (minor: api, webui)
  *  Full-text search for URLs, with above additional operators.
     (minor: api, webui)
  *  Add system zettel about license, contributors, and dependencies (and their
     license).
     For a nicer layout of zettel identifier, the zettel about environment
     values and about runtime metrics got new zettel identifier. This affects
     only user that referenced those zettel.
     (minor)
  *  Local images that cannot be read (not found or no access rights) are
     substituted with the new default image, a spinning emoji.
     See [/file?name=box/constbox/emoji_spin.gif].
     (minor: webui)
  *  Add zettelmarkup syntax for a table row that should be ignored:
     <tt>|%</tt>. This allows to paste output of the administrator console into
     a zettel.
     <code>|%</code>. This allows to paste output of the administrator console
     into a zettel.
     (minor: zmk)
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_12"></a>
<a id="0_0_12"></a>
<h2>Changes for Version 0.0.12 (2021-04-16)</h2>
  *  Raise the per-process limit of open files on macOS to 1.048.576. This
     allows most macOS users to use at least 500.000 zettel. That should be
     enough for the near future.
     (major)
  *  Mitigate the shortcomings of the macOS version by introducing types of
     directory boxes. The original directory box type is now called "notify"
     (the default value). There is a new type called "simple". This new type
     does not notify Zettelstore when some of the underlying Zettel files
     change.
     (major)
  *  Add new startup configuration <tt>default-dir-box-type</tt>, which gives
     the default value for specifying a directory box type. The default value
     is &ldquo;notify&rdquo;. On macOS, the default value may be changed
  *  Add new startup configuration <code>default-dir-box-type</code>, which
     gives the default value for specifying a directory box type. The default
     value is &ldquo;notify&rdquo;. On macOS, the default value may be changed
     &ldquo;simple&rdquo; if some errors occur while raising the per-process
     limit of open files.
     (minor)

<a name="0_0_11"></a>
<a id="0_0_11"></a>
<h2>Changes for Version 0.0.11 (2021-04-05)</h2>
  *  New box schema "file" allows to read zettel from a ZIP file.
     A zettel collection can now be packaged and distributed easier.
     (major: server)
  *  Non-restricted search is a full-text search. The search string will be
     normalized according to Unicode NFKD. Every character that is not a letter
     or a number will be ignored for the search. It is sufficient if the words
     to be searched are part of words inside a zettel, both content and
     metadata.
     (major: api, webui)
  *  A zettel can be excluded from being indexed (and excluded from being found
     in a search) if it contains the metadata <tt>no-index: true</tt>.
     in a search) if it contains the metadata <code>no-index: true</code>.
     (minor: api, webui)
  *  Menu bar is shown when displaying error messages.
     (minor: webui)
  *  When selecting zettel, it can be specified that a given value should
     <em>not</em> match. Previously, only the whole select criteria could be
     negated (which is still possible).
     (minor: api, webui)
  *  You can select a zettel by specifying that specific metadata keys must
     (or must not) be present.
     (minor: api, webui)
  *  Context of a zettel (introduced in version 0.0.10) does not take tags into
     account any more. Using some tags for determining the context resulted
     into erratic, non-deterministic context lists.
     (minor: api, webui)
  *  Selecting zettel depending on tag values can be both by comparing only the
     prefix or the whole string. If a search value begins with '#', only zettel
     with the exact tag will be returned. Otherwise a zettel will be returned
     if the search string just matches the prefix of only one of its tags.
     (minor: api, webui)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

A note for users of macOS: in the current release and with macOS's default
values, a zettel directory must not contain more than approx. 250 files. There
are three options to mitigate this limitation temporarily:
  #  You update the per-process limit of open files on macOS.
  #  You setup a virtualization environment to run Zettelstore on Linux or Windows.
  #  You setup a virtualisation environment to run Zettelstore on Linux or
     Windows.
  #  You wait for version 0.0.12 which addresses this issue.

<a name="0_0_10"></a>
<a id="0_0_10"></a>
<h2>Changes for Version 0.0.10 (2021-02-26)</h2>
  *  Menu item &ldquo;Home&rdquo; now redirects to a home zettel.
     Its default identifier is <tt>000100000000</tt>.
     The identifier can be changed with configuration key <tt>home-zettel</tt>, which supersedes key <tt>start</tt>.
     The default home zettel contains some welcoming information for the new user.
     Its default identifier is <code>000100000000</code>. The identifier can be
     changed with configuration key <code>home-zettel</code>, which supersedes
     key <code>start</code>. The default home zettel contains some welcoming
     information for the new user.
     (major: webui)
  *  Show context of a zettel by following all backward and/or forward reference
     up to a defined depth and list the resulting zettel. Additionally, some zettel
     with similar tags as the initial zettel are also taken into account.
  *  Show context of a zettel by following all backward and/or forward
     reference up to a defined depth and list the resulting zettel.
     Additionally, some zettel with similar tags as the initial zettel are also
     taken into account.
     (major: api, webui)
  *  A zettel that references other zettel within first-level list items, can act
     as a &ldquo;table of contents&rdquo; zettel.
     The API endpoint <tt>/o/{ID}</tt> allows to retrieve the referenced zettel in
     the same order as they occur in the zettel.
  *  A zettel that references other zettel within first-level list items, can
     act as a &ldquo;table of contents&rdquo; zettel. The API endpoint
     <code>/o/{ID}</code> allows to retrieve the referenced zettel in the same
     order as they occur in the zettel.
     (major: api)
  *  The zettel &ldquo;New Menu&rdquo; with identifier <tt>00000000090000</tt> contains
     a list of all zettel that should act as a template for new zettel.
     They are listed in the WebUIs &rdquo;New&ldquo; menu.
     This is an application of the previous item.
     It supersedes the usage of a role <tt>new-template</tt> introduced in [#0_0_6|version 0.0.6].
     <b>Please update your zettel if you make use of the now deprecated feature.</b>
  *  The zettel &ldquo;New Menu&rdquo; with identifier
     <code>00000000090000</code> contains a list of all zettel that should act
     as a template for new zettel. They are listed in the WebUIs
     &rdquo;New&ldquo; menu. This is an application of the previous item. It
     supersedes the usage of a role <code>new-template</code> introduced in
     [#0_0_6|version 0.0.6]. <b>Please update your zettel if you make use of
     the now deprecated feature.</b>
     (major: webui)
  *  A reference that starts with two slash characters (&ldquo;<code>//</code>&rdquo;)
     it will be interpreted relative to the value of <code>url-prefix</code>.
     For example, if <code>url-prefix</code> has the value <code>/manual/</code>,
     the reference <code>&lbrack;&lbrack;Zettel list|//h]]</code> will render as
     <code>&lt;a href="/manual/h">Zettel list&lt;/a></code>. (minor: syntax)
  *  A reference that starts with two slash characters
     (&ldquo;<code>//</code>&rdquo;) it will be interpreted relative to the
     value of <code>url-prefix</code>. For example, if <code>url-prefix</code>
     has the value <code>/manual/</code>, the reference
     <code>&lbrack;&lbrack;Zettel list|//h]]</code> will render as <code>&lt;a
     href="/manual/h">Zettel list&lt;/a></code>.
     (minor: syntax)
  *  Searching/selecting ignores the leading '#' character of tags.
     (minor: api, webui)
  *  When result of selecting or searching is presented, the query is written as the page heading.
  *  When result of selecting or searching is presented, the query is written
     as the page heading.
     (minor: webui)
  *  A reference to a zettel that contains a URL fragment, will now be processed by the indexer.
  *  A reference to a zettel that contains a URL fragment, will now be
     processed by the indexer.
     (bug: server)
  *  Runtime configuration key <tt>marker-external</tt> now defaults to
  *  Runtime configuration key <code>marker-external</code> now defaults to
     &ldquo;&amp;#10138;&rdquo; (&ldquo;&#10138;&rdquo;). It is more beautiful
     than the previous &ldquo;&amp;#8599;&amp;#xfe0e;&rdquo;
     (&ldquo;&#8599;&#65038;&rdquo;), which also needed the additional
     &ldquo;&amp;#xfe0e;&rdquo; to disable the conversion to an emoji on iPadOS.
     &ldquo;&amp;#xfe0e;&rdquo; to disable the conversion to an emoji on
     iPadOS.
     (minor: webui)
  *  A pre-build binary for macOS ARM64 (also known as Apple silicon) is available.
  *  A pre-build binary for macOS ARM64 (also known as Apple silicon) is
     available.
     (minor: infrastructure)
  *  Many smaller bug fixes and improvements, to the software and to the documentation.
  *  Many smaller bug fixes and improvements, to the software and to the
     documentation.

<a name="0_0_9"></a>
<a id="0_0_9"></a>
<h2>Changes for Version 0.0.9 (2021-01-29)</h2>
This is the first version that is managed by [https://fossil-scm.org|Fossil]
instead of GitHub. To access older versions, use the Git repository under
[https://github.com/zettelstore/zettelstore-github|zettelstore-github].

<h3>Server / API</h3>
  *  (major) Support for property metadata.
             Metadata key <tt>published</tt> is the first example of such
             Metadata key <code>published</code> is the first example of such
             a property.
  *  (major) A background activity (called <i>indexer</i>) continuously
             monitors zettel changes to establish the reverse direction of
             found internal links. This affects the new metadata keys
             <tt>precursor</tt> and <tt>folge</tt>. A user specifies the
             precursor of a zettel and the indexer computes the property
             <code>precursor</code> and <code>folge</code>. A user specifies
             the precursor of a zettel and the indexer computes the property
             metadata for
             [https://forum.zettelkasten.de/discussion/996/definition-folgezettel|Folgezettel].
             Metadata keys with type &ldquo;Identifier&rdquo; or
             &ldquo;IdentifierSet&rdquo; that have no inverse key (like
             <tt>precursor</tt> and <tt>folge</tt> with add to the key
             <tt>forward</tt> that also collects all internal links within the
             content. The computed inverse is <tt>backward</tt>, which provides
             all backlinks. The key <tt>back</tt> is computed as the value of
             <tt>backward</tt>, but without forward links. Therefore,
             <tt>back</tt> is something like the list of &ldquo;smart
             backlinks&rdquo;.
             <code>precursor</code> and <code>folge</code> with add to the key
             <code>forward</code> that also collects all internal links within
             the content. The computed inverse is <code>backward</code>, which
             provides all backlinks. The key <code>back</code> is computed as
             the value of <code>backward</code>, but without forward links.
             Therefore, <code>back</code> is something like the list of
             &ldquo;smart backlinks&rdquo;.
  *  (minor) If Zettelstore is being stopped, an appropriate message is written
             in the console log.
  *  (minor) New computed zettel with environmental data, the list of supported
             meta data keys, and statistics about all configured zettel boxes.
             Some other computed zettel got a new identifier (to make room for
             other variant).
  *  (minor) Remove zettel <tt>00000000000004</tt>, which contained the Go
  *  (minor) Remove zettel <code>00000000000004</code>, which contained the Go
             version that produced the Zettelstore executable. It was too
             specific to the current implementation. This information is now
             included in zettel <tt>00000000000006</tt> (<i>Zettelstore
             included in zettel <code>00000000000006</code> (<i>Zettelstore
             Environment Values</i>).
  *  (minor) Predefined templates for new zettel do not contain any value for
             attribute <tt>visibility</tt> any more.
             attribute <code>visibility</code> any more.
  *  (minor) Add a new metadata key type called &ldquo;Zettelmarkup&rdquo;.
             It is a non-empty string, that will be formatted with
             Zettelmarkup. <tt>title</tt> and <tt>default-title</tt> have this
             type.
             Zettelmarkup. <code>title</code> and <code>default-title</code>
             have this type.
  *  (major) Rename zettel syntax &ldquo;meta&rdquo; to &ldquo;none&rdquo;.
             Please update the <i>Zettelstore Runtime Configuration</i> and all
             other zettel that previously used the value &ldquo;meta&rdquo;.
             Other zettel are typically user zettel, used for authentication.
             However, there is no real harm, if you do not update these zettel.
             In this case, the metadata is just not presented when rendered.
             Zettelstore will still work.
  *  (minor) Login will take at least 500 milliseconds to mitigate login
             attacks. This affects both the API and the WebUI.
  *  (minor) Add a sort option &ldquo;_random&rdquo; to produce a zettel list
             in random order. <tt>_order</tt> / <tt>order</tt> are now an
             aliases for the query parameters <tt>_sort</tt> / <tt>sort</tt>.
             in random order. <code>_order</code> / <code>order</code> are now
             an aliases for the query parameters <code>_sort</code>
             / <code>sort</code>.

<h3>WebUI</h3>
  *  (major) HTML template zettel for WebUI now use
             [https://mustache.github.io/|Mustache] syntax instead of
             previously used [https://golang.org/pkg/html/template/|Go
             template] syntax. This allows these zettel to be used, even when
             there is another Zettelstore implementation, in another
             programming language. Mustache is available for approx. 48
             programming languages, instead of only one for Go templates. <b>If
             you modified your templates, you <i>must</i> adapt them to the new
             syntax. Otherwise the WebUI will not work.</b>
  *  (major) Show zettel identifier of folgezettel and precursor zettel in the
             header of a rendered zettel. If a zettel has real backlinks, they
             are shown at the botton of the page (&ldquo;Additional links to
             this zettel&rdquo;).
  *  (minor) All property metadata, even computed metadata is shown in the info
             page of a zettel.
  *  (minor) Rendering of metadata keys <tt>title</tt> and
             <tt>default-title</tt> in info page changed to a full HTML output
             for these Zettelmarkup encoded values.
  *  (minor) Rendering of metadata keys <code>title</code> and
             <code>default-title</code> in info page changed to a full HTML
             output for these Zettelmarkup encoded values.
  *  (minor) Always show the zettel identifier on the zettel detail view.
             Previously, the identifier was not shown if the zettel was not
             editable.
  *  (minor) Do not show computed metadata in edit forms anymore.

<a name="0_0_8"></a>
<a id="0_0_8"></a>
<h2>Changes for Version 0.0.8 (2020-12-23)</h2>
<h3>Server / API</h3>
  *  (bug) Zettel files with extension <tt>.jpg</tt> and without metadata will
           get a <tt>syntax</tt> value &ldquo;jpg&rdquo;. The internal data
           structure got the same value internally, instead of
  *  (bug) Zettel files with extension <code>.jpg</code> and without metadata
           will get a <code>syntax</code> value &ldquo;jpg&rdquo;. The internal
           data structure got the same value internally, instead of
           &ldquo;jpeg&rdquo;. This has been fixed for all possible alternative
           syntax values.
  *  (bug) If a file, e.g. an image file like <tt>20201130190200.jpg</tt>, is
           added to the directory box, its metadata are just calculated from
  *  (bug) If a file, e.g. an image file like <code>20201130190200.jpg</code>,
           is added to the directory box, its metadata are just calculated from
           the information available. Updated metadata did not find its way
           into the zettel box, because the <tt>.meta</tt> file was not
           into the zettel box, because the <code>.meta</code> file was not
           written.
  *  (bug) If just the <tt>.meta</tt> file was deleted manually, the zettel was
           assumed to be missing. A workaround is to restart the software. If
           the <tt>.meta</tt> file is deleted, metadata is now calculated in
           the same way when the <tt>.meta</tt> file is non-existing at the
           start of the software.
  *  (bug) If just the <code>.meta</code> file was deleted manually, the zettel
           was assumed to be missing. A workaround is to restart the software.
           If the <code>.meta</code> file is deleted, metadata is now
           calculated in the same way when the <code>.meta</code> file is
           non-existing at the start of the software.
  *  (bug) A link to the current zettel, only using a fragment (e.g.
           <code>&#91;&#91;Title|#title]]</code>) is now handled correctly as
           a zettel link (and not as a link to external material).
  *  (minor) Allow zettel to be marked as &ldquo;read only&rdquo;.
             This is done through the metadata key <tt>read-only</tt>.
             This is done through the metadata key <code>read-only</code>.
  *  (bug) When renaming a zettel, check all boxes for the new zettel
           identifier, not just the first one. Otherwise it will be possible to
           shadow a read-only zettel from a next box, effectively modifying it.
  *  (minor) Add support for a configurable default value for metadata key
             <tt>visibility</tt>.
  *  (bug) If <tt>list-page-size</tt> is set to a relatively small value and
           the authenticated user is <i>not</i> the owner, some zettel were not
           shown in the list of zettel or were not returned by the API.
             <code>visibility</code>.
  *  (bug) If <code>list-page-size</code> is set to a relatively small value
           and the authenticated user is <i>not</i> the owner, some zettel were
           not shown in the list of zettel or were not returned by the API.
  *  (minor) Add support for new visibility &ldquo;expert&rdquo;.
             An owner becomes an expert, if the runtime configuration key
             <tt>expert-mode</tt> is set to true.
             <code>expert-mode</code> is set to true.
  *  (major) Add support for computed zettel.
             These zettel have an identifier less than <tt>0000000000100</tt>.
             Most of them are only visible, if <tt>expert-mode</tt> is enabled.
             These zettel have an identifier less than
             <code>0000000000100</code>. Most of them are only visible, if
             <code>expert-mode</code> is enabled.
  *  (bug)   Fixes a memory leak that results in too many open files after
             approx. 125 reload operations.
  *  (major) Predefined templates for new zettel got an explicit value for
             visibility: &ldquo;login&rdquo;. Please update these zettel if you
             modified them.
  *  (major) Rename key <tt>readonly</tt> of <i>Zettelstore Startup
             Configuration</i> to <tt>read-only-mode</tt>. This was done to
             avoid some confusion with the the zettel metadata key
             <tt>read-only</tt>. <b>Please adapt your startup configuration.
             Otherwise your Zettelstore will be accidentally writable.</b>
  *  (major) Rename key <code>readonly</code> of <i>Zettelstore Startup
             Configuration</i> to <code>read-only-mode</code>. This was done to
             avoid some confusion with the zettel metadata key
             <code>read-only</code>. <b>Please adapt your startup
             configuration. Otherwise your Zettelstore will be accidentally
             writable.</b>
  *  (minor) References starting with &ldquo;./&rdquo; and &ldquo;../&rdquo;
             are treated as a local reference. Previously, only the prefix
             &ldquo;/&rdquo; was treated as a local reference.
  *  (major) Metadata key <tt>modified</tt> will be set automatically to the
             current local time if a zettel is updated through Zettelstore.
  *  (major) Metadata key <code>modified</code> will be set automatically to
             the current local time if a zettel is updated through Zettelstore.
             <b>If you used that key previously for your own, you should rename
             it before you upgrade.</b>
  *  (minor) The new visibility value &ldquo;simple-expert&rdquo; ensures that
             many computed zettel are shown for new users. This is to enable
             them to send useful bug reports.
  *  (minor) When a zettel is stored as a file, its identifier is additionally
             stored within the metadata. This helps for better robustness in
             case the file names were corrupted. In addition, there could be
             a tool that compares the identifier with the file name.

<h3>WebUI</h3>
  *  (minor) Remove list of tags in &ldquo;List Zettel&rdquo; and search
             results. There was some feedback that the additional tags were not
             helpful.
  *  (minor) Move zettel field "role" above "tags" and move "syntax" more to
             "content".
  *  (minor) Rename zettel operation &ldquo;clone&rdquo; to &ldquo;copy&rdquo;.
  *  (major) All predefined HTML templates have now a visibility value
             &ldquo;expert&rdquo;. If you want to see them as an non-expert
             owner, you must temporary enable <tt>expert-mode</tt> and change
             the <tt>visibility</tt> metadata value.
             owner, you must temporary enable <code>expert-mode</code> and
             change the <code>visibility</code> metadata value.
  *  (minor) Initial support for
             [https://zettelkasten.de/posts/tags/folgezettel/|Folgezettel]. If
             you click on &ldquo;Folge&rdquo; (detail view or info view), a new
             zettel is created with a reference (<tt>precursor</tt>) to the
             zettel is created with a reference (<code>precursor</code>) to the
             original zettel. Title, role, tags, and syntax are copied from the
             original zettel.
  *  (major) Most predefined zettel have a title prefix of
             &ldquo;Zettelstore&rdquo;.
  *  (minor) If started in simple mode, e.g. via double click or without any
             command, some information for the new user is presented. In the
             terminal, there is a hint about opening the web browser and use
             a specific URL. A <i>Welcome zettel</i> is created, to give some
             more information. (This change also applies to the server itself,
             but it is more suited to the WebUI user.)

<a name="0_0_7"></a>
<a id="0_0_7"></a>
<h2>Changes for Version 0.0.7 (2020-11-24)</h2>
  *  With this version, Zettelstore and this manual got a new license, the
     [https://joinup.ec.europa.eu/collection/eupl|European Union Public
     Licence] (EUPL), version 1.2 or later. Nothing else changed. If you want
     to stay with the old licenses (AGPLv3+, CC BY-SA 4.0), you are free to
     fork from the previous version.

<a name="0_0_6"></a>
<a id="0_0_6"></a>
<h2>Changes for Version 0.0.6 (2020-11-23)</h2>
<h3>Server</h3>
  *  (major) Rename identifier of <i>Zettelstore Runtime Configuration</i> to
             <tt>00000000000100</tt> (previously <tt>00000000000001</tt>). This
             is done to gain some free identifier with smaller number to be
             used internally. <b>If you customized this zettel, please make
             sure to rename it to the new identifier.</b>
             <code>00000000000100</code> (previously
             <code>00000000000001</code>). This is done to gain some free
             identifier with smaller number to be used internally. <b>If you
             customized this zettel, please make sure to rename it to the new
             identifier.</b>
  *  (major) Rename the two essential metadata keys of a user zettel to
             <tt>credential</tt> and <tt>user-id</tt>. The previous values were
             <tt>cred</tt> and <tt>ident</tt>. <b>If you enabled user
             authentication and added some user zettel, make sure to change
             them accordingly. Otherwise these users will not authenticated any
             more.</b>
             <code>credential</code> and <code>user-id</code>. The previous
             values were <code>cred</code> and <code>ident</code>. <b>If you
             enabled user authentication and added some user zettel, make sure
             to change them accordingly. Otherwise these users will not
             authenticated any more.</b>
  *  (minor) Rename the scheme of the box URL where predefined zettel are
             stored to &ldquo;const&rdquo;. The previous value was
             &ldquo;globals&rdquo;.

<h3>Zettelmarkup</h3>
  *  (bug) Allow to specify a <i>fragment</i> in a reference to a zettel.
           Used to link to an internal position within a zettel.
           This applies to CommonMark too.

<h3>API</h3>
  *  (bug)   Encoding binary content in format &ldquo;json&rdquo; now results
             in valid JSON content.
  *  (bug)   All query parameters of selecting zettel must be true, regardless
             if a specific key occurs more than one or not.
  *  (minor) Encode all inherited meta values in all formats except
             &ldquo;raw&rdquo;. A meta value is called <i>inherited</i> if
             there is a key starting with <tt>default-</tt> in the
             there is a key starting with <code>default-</code> in the
             <i>Zettelstore Runtime Configuration</i>. Applies to WebUI also.
  *  (minor) Automatic calculated identifier for headings (only for
             &ldquo;html&rdquo;, &ldquo;djson&rdquo;, &ldquo;native&rdquo;
             format and for the Web user interface). You can use this to
             provide a zettel reference that links to the heading, without
             specifying an explicit mark (<code>&#91;!mark]</code>).
  *  (major) Allow to retrieve all references of a given zettel.
998
999
1000
1001
1002
1003
1004
1005
1006


1007
1008
1009



1010
1011
1012
1013
1014
1015

1016
1017
1018
1019
1020
1021
1022



1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034



1035
1036
1037
1038
1039

1040
1041
1042
1043
1044
1045
1046

1047
1048
1049
1050



1051
1052

1053
1054
1055
1056
1057
1058

1059
1060
1061
1062



1063
1064
1065
1066
1067



1068
1069

1070
1071
1436
1437
1438
1439
1440
1441
1442


1443
1444
1445


1446
1447
1448
1449
1450
1451
1452
1453

1454
1455
1456
1457
1458



1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470



1471
1472
1473
1474
1475
1476
1477

1478
1479
1480
1481
1482
1483
1484

1485
1486
1487


1488
1489
1490
1491

1492
1493
1494
1495
1496
1497

1498
1499



1500
1501
1502
1503
1504
1505


1506
1507
1508
1509

1510
1511
1512







-
-
+
+

-
-
+
+
+





-
+




-
-
-
+
+
+









-
-
-
+
+
+




-
+






-
+


-
-
+
+
+

-
+





-
+

-
-
-
+
+
+



-
-
+
+
+

-
+


             nor host name, are considered &ldquo;local references&rdquo; (in
             contrast to &ldquo;zettel references&rdquo; and &ldquo;external
             references&rdquo;). When a local reference is displayed as an URL
             on the WebUI, it will not opened in a new window/tab. They will
             receive a <i>local</i> marker, when encoded as &ldquo;djson&rdquo;
             or &ldquo;native&rdquo;. Local references are listed on the
             <i>Info page</i> of each zettel.
  *  (minor) Change the default value for some visual sugar putd after an
             external URL to <tt>&\#8599;&\#xfe0e;</tt>
  *  (minor) Change the default value for some visual sugar put after an
             external URL to <code>&\#8599;&\#xfe0e;</code>
             (&ldquo;&#8599;&#xfe0e;&rdquo;). This affects the former key
             <tt>icon-material</tt> of the <i>Zettelstore Runtime
             Configuration</i>, which is renamed to <tt>marker-external</tt>.
             <code>icon-material</code> of the <i>Zettelstore Runtime
             Configuration</i>, which is renamed to
             <code>marker-external</code>.
  *  (major) Allow multiple zettel to act as templates for creating new zettel.
             All zettel with a role value &ldquo;new-template&rdquo; act as
             a template to create a new zettel. The WebUI menu item
             &ldquo;New&rdquo; changed to a drop-down list with all those
             zettel, ordered by their identifier. All metadata keys with the
             prefix <tt>new-</tt> will be translated to a new or updated
             prefix <code>new-</code> will be translated to a new or updated
             keys/value without that prefix. You can use this mechanism to
             specify a role for the new zettel, or a different title. The title
             of the template zettel is used in the drop-down list. The initial
             template zettel &ldquo;New Zettel&rdquo; has now a different
             zettel identifier (now: <tt>00000000091001</tt>, was:
             <tt>00000000040001</tt>). <b>Please update it, if you changed that
             zettel.</b>
             zettel identifier (now: <code>00000000091001</code>, was:
             <code>00000000040001</code>). <b>Please update it, if you changed
             that zettel.</b>
             <br>Note: this feature was superseded in [#0_0_10|version 0.0.10]
             by the &ldquo;New Menu&rdquo; zettel.
  *  (minor) When a page should be opened in a new windows (e.g. for external
             references), the web browser is instructed to decouple the new
             page from the previous one for privacy and security reasons. In
             detail, the web browser is instructed to omit referrer information
             and to omit a JS object linking to the page that contained the
             external link.
  *  (minor) If the value of the <i>Zettelstore Runtime Configuration</i> key
             <tt>list-page-size</tt> is greater than zero, the number of WebUI
             list elements will be restricted and it is possible to change to
             the next/previous page to list more elements.
             <code>list-page-size</code> is greater than zero, the number of
             WebUI list elements will be restricted and it is possible to
             change to the next/previous page to list more elements.
  *  (minor) Change CSS to enhance reading: make <code>line-height</code>
             a little smaller (previous: 1.6, now 1.4) and move list items to
             the left.

<a name="0_0_5"></a>
<a id="0_0_5"></a>
<h2>Changes for Version 0.0.5 (2020-10-22)</h2>
  *  Application Programming Interface (API) to allow external software to
     retrieve zettel data from the Zettelstore.
  *  Specify boxes, where zettel are stored, via an URL.
  *  Add support for a custom footer.

<a name="0_0_4"></a>
<a id="0_0_4"></a>
<h2>Changes for Version 0.0.4 (2020-09-11)</h2>
  *  Optional user authentication/authorization.
  *  New sub-commands <tt>file</tt> (use Zettelstore as a command line filter),
     <tt>password</tt> (for authentication), and <tt>config</tt>.
  *  New sub-commands <code>file</code> (use Zettelstore as a command line
     filter), <code>password</code> (for authentication), and
     <code>config</code>.

<a name="0_0_3"></a>
<a id="0_0_3"></a>
<h2>Changes for Version 0.0.3 (2020-08-31)</h2>
  *  Starting Zettelstore has been changed by introducing sub-commands.
     This change is also reflected on the server installation procedures.
  *  Limitations on renaming zettel has been relaxed.

<a name="0_0_2"></a>
<a id="0_0_2"></a>
<h2>Changes for Version 0.0.2 (2020-08-28)</h2>
  *  Configuration zettel now has ID <tt>00000000000001</tt> (previously:
     <tt>00000000000000</tt>).
  *  The zettel with ID <tt>00000000000000</tt> is no longer shown in any
  *  Configuration zettel now has ID <code>00000000000001</code> (previously:
     <code>00000000000000</code>).
  *  The zettel with ID <code>00000000000000</code> is no longer shown in any
     zettel list. If you changed the configuration zettel, you should rename it
     manually in its file directory.
  *  Creating a new zettel is now done by cloning an existing zettel.
     To mimic the previous behaviour, a zettel with ID <tt>00000000040001</tt>
     is introduced. You can change it if you need a different template zettel.
     To mimic the previous behaviour, a zettel with ID
     <code>00000000040001</code> is introduced. You can change it if you need
     a different template zettel.

<a name="0_0_1"></a>
<a id="0_0_1"></a>
<h2>Changes for Version 0.0.1 (2020-08-21)</h2>
  *  Initial public release.

Changes to www/download.wiki.

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

13
14
15
16
17
18





19
20
21
22
23
24

25
26
1
2
3
4
5
6
7
8
9
10
11

12
13





14
15
16
17
18
19
20
21
22
23

24
25
26











-
+

-
-
-
-
-
+
+
+
+
+





-
+


<title>Download</title>
<h1>Download of Zettelstore Software</h1>
<h2>Foreword</h2>
  *  Zettelstore is free/libre open source software, licensed under EUPL-1.2-or-later.
  *  The software is provided as-is.
  *  There is no guarantee that it will not damage your system.
  *  However, it is in use by the main developer since March 2020 without any damage.
  *  It may be useful for you. It is useful for me.
  *  Take a look at the [https://zettelstore.de/manual/|manual] to know how to start and use it.

<h2>ZIP-ped Executables</h2>
Build: <code>v0.10.1</code> (2023-01-30).
Build: <code>v0.19.0</code> (2024-12-13).

  *  [/uv/zettelstore-0.10.1-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.10.1-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.10.1-windows-amd64.zip|Windows] (amd64)
  *  [/uv/zettelstore-0.10.1-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.10.1-darwin-arm64.zip|macOS] (arm64, aka Apple silicon)
  *  [/uv/zettelstore-0.19.0-linux-amd64.zip|Linux] (amd64)
  *  [/uv/zettelstore-0.19.0-linux-arm.zip|Linux] (arm6, e.g. Raspberry Pi)
  *  [/uv/zettelstore-0.19.0-darwin-arm64.zip|macOS] (arm64)
  *  [/uv/zettelstore-0.19.0-darwin-amd64.zip|macOS] (amd64)
  *  [/uv/zettelstore-0.19.0-windows-amd64.zip|Windows] (amd64)

Unzip the appropriate file, install and execute Zettelstore according to the manual.

<h2>Zettel for the manual</h2>
As a starter, you can download the zettel for the manual
[/uv/manual-0.10.1.zip|here].
[/uv/manual-0.19.0.zip|here].
Just unzip the contained files and put them into your zettel folder or
configure a file box to read the zettel directly from the ZIP file.

Changes to www/impri.wiki.

10
11
12
13
14
15
16
17

18
10
11
12
13
14
15
16

17
18







-
+

If you do not log into this site, or login as the user &quot;anonymous&quot;,
the only personal data this web service will process is your IP adress. It will
be used to send the data of the website you requested to you and to mitigate
possible attacks against this website.

This website is hosted by [https://ionos.de|1&amp;1 IONOS SE].
According to
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-11-ionos-produktes/andere-11-ionos-produkte/|their information],
[https://www.ionos.de/hilfe/datenschutz/datenverarbeitung-von-webseitenbesuchern-ihres-ionos-produktes/andere-ionos-produkte/|their information],
no processing of personal data is done by them.

Changes to www/index.wiki.

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







-
-
-
-
-
+
+
+
+
+
+
+

-
+
-

-
+

-
-
-
-
-
+
+
+
+
+





+
-
+





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

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

[https://zettelstore.de/client|Zettelstore Client] provides client software to
access Zettelstore via its API more easily,
[https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
software, which often connects to Zettelstore via its API. Some of the software
packages may be experimental.
  *  [https://t73f.de/r/zsc|Zettelstore Client] provides client software to
     access Zettelstore via its API more easily.
  *  [https://zettelstore.de/contrib|Zettelstore Contrib] contains contributed
     software, which often connects to Zettelstore via its API. Some of the
     software packages may be experimental.
  *  [https://t73f.de/r/sx|Sx] provides an evaluator for symbolic
     expressions, which is used for HTML templates and more.

[https://twitter.com/zettelstore|Stay]
[https://mastodon.social/tags/Zettelstore|Stay tuned]&nbsp;&hellip;
<a rel="me" href="https://nerdculture.de/@zettelstore">tuned</a>&nbsp;&hellip;
<hr>
<h3>Latest Release: 0.10.1 (2023-01-30)</h3>
<h3>Latest Release: 0.19.0 (2024-12-13)</h3>
  *  [./download.wiki|Download]
  *  [./changes.wiki#0_10|Change summary]
  *  [/timeline?p=v0.10.1&bt=v0.9.0&y=ci|Check-ins for version 0.10.1],
     [/vdiff?to=v0.10.1&from=v0.9.0|content diff]
  *  [/timeline?df=v0.10.1&y=ci|Check-ins derived from the 0.10.1 release],
     [/vdiff?from=v0.10.1&to=trunk|content diff]
  *  [./changes.wiki#0_19|Change summary]
  *  [/timeline?p=v0.19.0&bt=v0.18.0&y=ci|Check-ins for version 0.19],
     [/vdiff?to=v0.19.0&from=v0.18.0|content diff]
  *  [/timeline?df=v0.19.0&y=ci|Check-ins derived from the 0.19 release],
     [/vdiff?from=v0.19.0&to=trunk|content diff]
  *  [./plan.wiki|Limitations and planned improvements]
  *  [/timeline?t=release|Timeline of all past releases]

<hr>
<h2>Build instructions</h2>

Just install [https://golang.org/dl/|Go] and some Go-based tools. Please read
Just install [https://go.dev/dl/|Go] and some Go-based tools. Please read
the [./build.md|instructions] for details.

  *  [/dir?ci=trunk|Source code]
  *  [/download|Download the source code] as a tarball or a ZIP file
     (you must [/login|login] as user &quot;anonymous&quot;).

Changes to www/plan.wiki.

1
2
3
4
5
6

7
8

9
10
11
12
13
14
15
16
1
2
3
4
5

6


7

8
9
10
11
12
13
14





-
+
-
-
+
-







<title>Limitations and planned improvements</title>

Here is a list of some shortcomings of Zettelstore.
They are planned to be solved.

<h3>Serious limitations</h3>
  *  Zettelstore must have indexed all zettel to make use of queries.
  *  &hellip;

     Otherwise not all zettel may be returned.
<h3>Smaller limitations</h3>
  *  Quoted attribute values are not yet supported in Zettelmarkup:
     <code>{key="value with space"}</code>.
  *  The horizontal tab character (<tt>U+0009</tt>) is not supported.
  *  Missing support for citation keys.
  *  Changing the content syntax is not reflected in file extension.
  *  File names with additional text besides the zettel identifier are not
     always preserved.

Added zettel/content.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 zettel

import (
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"unicode"
	"unicode/utf8"

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

// Content is just the content of a zettel.
type Content struct {
	data     []byte
	isBinary bool
}

// NewContent creates a new content from a string.
func NewContent(data []byte) Content {
	return Content{data: data, isBinary: IsBinary(data)}
}

// Length returns the number of bytes stored.
func (zc *Content) Length() int { return len(zc.data) }

// Equal compares two content values.
func (zc *Content) Equal(o *Content) bool {
	if zc == nil {
		return o == nil
	}
	if zc.isBinary != o.isBinary {
		return false
	}
	return bytes.Equal(zc.data, o.data)
}

// Write it to a Writer
func (zc *Content) Write(w io.Writer) (int, error) {
	return w.Write(zc.data)
}

// AsString returns the content itself is a string.
func (zc *Content) AsString() string { return string(zc.data) }

// AsBytes returns the content itself is a byte slice.
func (zc *Content) AsBytes() []byte { return zc.data }

// IsBinary returns true if the content contains non-unicode values or is,
// interpreted a text, with a high probability binary content.
func (zc *Content) IsBinary() bool { return zc.isBinary }

// TrimSpace remove some space character in content, if it is not binary content.
func (zc *Content) TrimSpace() {
	if zc.isBinary {
		return
	}
	inp := input.NewInput(zc.data)
	pos := inp.Pos
	for inp.Ch != input.EOS {
		if input.IsEOLEOS(inp.Ch) {
			inp.Next()
			pos = inp.Pos
			continue
		}
		if !inp.IsSpace() {
			break
		}
		inp.Next()
	}
	zc.data = bytes.TrimRightFunc(inp.Src[pos:], unicode.IsSpace)
}

// Encode content for future transmission.
func (zc *Content) Encode() (data, encoding string) {
	if !zc.isBinary {
		return zc.AsString(), ""
	}
	return base64.StdEncoding.EncodeToString(zc.data), "base64"
}

// SetDecoded content to the decoded value of the given string.
func (zc *Content) SetDecoded(data, encoding string) error {
	switch encoding {
	case "":
		zc.data = []byte(data)
	case "base64":
		decoded, err := base64.StdEncoding.DecodeString(data)
		if err != nil {
			return err
		}
		zc.data = decoded
	default:
		return errors.New("unknown encoding " + encoding)
	}
	zc.isBinary = IsBinary(zc.data)
	return nil
}

// IsBinary returns true if the given data appears to be non-text data.
func IsBinary(data []byte) bool {
	if !utf8.Valid(data) {
		return true
	}
	for i := range len(data) {
		if data[i] == 0 {
			return true
		}
	}
	return false
}

Added zettel/content_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 zettel_test

import (
	"testing"

	"zettelstore.de/z/zettel"
)

func TestContentIsBinary(t *testing.T) {
	t.Parallel()
	td := []struct {
		s   string
		exp bool
	}{
		{"abc", false},
		{"äöü", false},
		{"", false},
		{string([]byte{0}), true},
	}
	for i, tc := range td {
		content := zettel.NewContent([]byte(tc.s))
		got := content.IsBinary()
		if got != tc.exp {
			t.Errorf("TC=%d: expected %v, got %v", i, tc.exp, got)
		}
	}
}

func TestTrimSpace(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in, exp string
	}{
		{"", ""},
		{" ", ""},
		{"abc", "abc"},
		{" abc", " abc"},
		{"abc ", "abc"},
		{"abc \n", "abc"},
		{"abc\n ", "abc"},
		{"\nabc", "abc"},
		{" \nabc", "abc"},
		{" \n abc", " abc"},
		{" \n\n abc", " abc"},
		{" \n \n abc", " abc"},
		{" \n \n abc \n \n ", " abc"},
	}
	for _, tc := range testcases {
		c := zettel.NewContent([]byte(tc.in))
		c.TrimSpace()
		got := c.AsString()
		if got != tc.exp {
			t.Errorf("TrimSpace(%q) should be %q, but got %q", tc.in, tc.exp, got)
		}
	}
}

Added zettel/id/digraph.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id

import (
	"maps"
	"slices"
)

// Digraph relates zettel identifier in a directional way.
type Digraph map[Zid]*Set

// AddVertex adds an edge / vertex to the digraph.
func (dg Digraph) AddVertex(zid Zid) Digraph {
	if dg == nil {
		return Digraph{zid: nil}
	}
	if _, found := dg[zid]; !found {
		dg[zid] = nil
	}
	return dg
}

// RemoveVertex removes a vertex and all its edges from the digraph.
func (dg Digraph) RemoveVertex(zid Zid) {
	if len(dg) > 0 {
		delete(dg, zid)
		for vertex, closure := range dg {
			dg[vertex] = closure.Remove(zid)
		}
	}
}

// AddEdge adds a connection from `zid1` to `zid2`.
// Both vertices must be added before. Otherwise the function may panic.
func (dg Digraph) AddEdge(fromZid, toZid Zid) Digraph {
	if dg == nil {
		return Digraph{fromZid: (*Set)(nil).Add(toZid), toZid: nil}
	}
	dg[fromZid] = dg[fromZid].Add(toZid)
	return dg
}

// AddEgdes adds all given `Edge`s to the digraph.
//
// In contrast to `AddEdge` the vertices must not exist before.
func (dg Digraph) AddEgdes(edges EdgeSlice) Digraph {
	if dg == nil {
		if len(edges) == 0 {
			return nil
		}
		dg = make(Digraph, len(edges))
	}
	for _, edge := range edges {
		dg = dg.AddVertex(edge.From)
		dg = dg.AddVertex(edge.To)
		dg = dg.AddEdge(edge.From, edge.To)
	}
	return dg
}

// Equal returns true if both digraphs have the same vertices and edges.
func (dg Digraph) Equal(other Digraph) bool {
	return maps.EqualFunc(dg, other, func(cg, co *Set) bool { return cg.Equal(co) })
}

// Clone a digraph.
func (dg Digraph) Clone() Digraph {
	if len(dg) == 0 {
		return nil
	}
	copyDG := make(Digraph, len(dg))
	for vertex, closure := range dg {
		copyDG[vertex] = closure.Clone()
	}
	return copyDG
}

// HasVertex returns true, if `zid` is a vertex of the digraph.
func (dg Digraph) HasVertex(zid Zid) bool {
	if len(dg) == 0 {
		return false
	}
	_, found := dg[zid]
	return found
}

// Vertices returns the set of all vertices.
func (dg Digraph) Vertices() *Set {
	if len(dg) == 0 {
		return nil
	}
	verts := NewSetCap(len(dg))
	for vert := range dg {
		verts.Add(vert)
	}
	return verts
}

// Edges returns an unsorted slice of the edges of the digraph.
func (dg Digraph) Edges() (es EdgeSlice) {
	for vert, closure := range dg {
		closure.ForEach(func(next Zid) {
			es = append(es, Edge{From: vert, To: next})
		})
	}
	return es
}

// Originators will return the set of all vertices that are not referenced
// a the to-part of an edge.
func (dg Digraph) Originators() *Set {
	if len(dg) == 0 {
		return nil
	}
	origs := dg.Vertices()
	for _, closure := range dg {
		origs.ISubstract(closure)
	}
	return origs
}

// Terminators returns the set of all vertices that does not reference
// other vertices.
func (dg Digraph) Terminators() (terms *Set) {
	for vert, closure := range dg {
		if closure.IsEmpty() {
			terms = terms.Add(vert)
		}
	}
	return terms
}

// TransitiveClosure calculates the sub-graph that is reachable from `zid`.
func (dg Digraph) TransitiveClosure(zid Zid) (tc Digraph) {
	if len(dg) == 0 {
		return nil
	}
	var marked *Set
	stack := Slice{zid}
	for pos := len(stack) - 1; pos >= 0; pos = len(stack) - 1 {
		curr := stack[pos]
		stack = stack[:pos]
		if marked.Contains(curr) {
			continue
		}
		tc = tc.AddVertex(curr)
		dg[curr].ForEach(func(next Zid) {
			tc = tc.AddVertex(next)
			tc = tc.AddEdge(curr, next)
			stack = append(stack, next)
		})
		marked = marked.Add(curr)
	}
	return tc
}

// ReachableVertices calculates the set of all vertices that are reachable
// from the given `zid`.
func (dg Digraph) ReachableVertices(zid Zid) (tc *Set) {
	if len(dg) == 0 {
		return nil
	}
	stack := dg[zid].SafeSorted()
	for last := len(stack) - 1; last >= 0; last = len(stack) - 1 {
		curr := stack[last]
		stack = stack[:last]
		if tc.Contains(curr) {
			continue
		}
		closure, found := dg[curr]
		if !found {
			continue
		}
		tc = tc.Add(curr)
		closure.ForEach(func(next Zid) {
			stack = append(stack, next)
		})
	}
	return tc
}

// IsDAG returns a vertex and false, if the graph has a cycle containing the vertex.
func (dg Digraph) IsDAG() (Zid, bool) {
	for vertex := range dg {
		if dg.ReachableVertices(vertex).Contains(vertex) {
			return vertex, false
		}
	}
	return Invalid, true
}

// Reverse returns a graph with reversed edges.
func (dg Digraph) Reverse() (revDg Digraph) {
	for vertex, closure := range dg {
		revDg = revDg.AddVertex(vertex)
		closure.ForEach(func(next Zid) {
			revDg = revDg.AddVertex(next)
			revDg = revDg.AddEdge(next, vertex)
		})
	}
	return revDg
}

// SortReverse returns a deterministic, topological, reverse sort of the
// digraph.
//
// Works only if digraph is a DAG. Otherwise the algorithm will not terminate
// or returns an arbitrary value.
func (dg Digraph) SortReverse() (sl Slice) {
	if len(dg) == 0 {
		return nil
	}
	tempDg := dg.Clone()
	for len(tempDg) > 0 {
		terms := tempDg.Terminators()
		if terms.IsEmpty() {
			break
		}
		termSlice := terms.SafeSorted()
		slices.Reverse(termSlice)
		sl = append(sl, termSlice...)
		terms.ForEach(func(t Zid) {
			tempDg.RemoveVertex(t)
		})
	}
	return sl
}

Added zettel/id/digraph_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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id_test

import (
	"testing"

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

type zps = id.EdgeSlice

func createDigraph(pairs zps) (dg id.Digraph) {
	return dg.AddEgdes(pairs)
}

func TestDigraphOriginators(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		orig *id.Set
		term *id.Set
	}{
		{"empty", nil, nil, nil},
		{"single", zps{{0, 1}}, id.NewSet(0), id.NewSet(1)},
		{"chain", zps{{0, 1}, {1, 2}, {2, 3}}, id.NewSet(0), id.NewSet(3)},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.dg)
			if got := dg.Originators(); !tc.orig.Equal(got) {
				t.Errorf("Originators: expected:\n%v, but got:\n%v", tc.orig, got)
			}
			if got := dg.Terminators(); !tc.term.Equal(got) {
				t.Errorf("Termintors: expected:\n%v, but got:\n%v", tc.orig, got)
			}
		})
	}
}

func TestDigraphReachableVertices(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs id.EdgeSlice
		start id.Zid
		exp   *id.Set
	}{
		{"nil", nil, 0, nil},
		{"0-2", zps{{1, 2}, {2, 3}}, 1, id.NewSet(2, 3)},
		{"1,2", zps{{1, 2}, {2, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, id.NewSet(2, 3)},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 2, id.NewSet(3)},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 3, nil},
		{"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, id.NewSet(2, 3)},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.pairs)
			if got := dg.ReachableVertices(tc.start); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}

		})
	}
}

func TestDigraphTransitiveClosure(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name  string
		pairs id.EdgeSlice
		start id.Zid
		exp   id.EdgeSlice
	}{
		{"nil", nil, 0, nil},
		{"1-3", zps{{1, 2}, {2, 3}}, 1, zps{{1, 2}, {2, 3}}},
		{"1,2", zps{{1, 1}, {2, 3}}, 2, zps{{2, 3}}},
		{"0-2,1-2", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
		{"0-2,1-2/1", zps{{1, 2}, {2, 3}, {1, 3}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
		{"0-2,1-2/2", zps{{1, 2}, {2, 3}, {1, 3}}, 2, zps{{2, 3}}},
		{"0-2,1-2,3*", zps{{1, 2}, {2, 3}, {1, 3}, {4, 4}}, 1, zps{{1, 2}, {1, 3}, {2, 3}}},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.pairs)
			if got := dg.TransitiveClosure(tc.start).Edges().Sort(); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

func TestIsDAG(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		exp  bool
	}{
		{"empty", nil, true},
		{"single-edge", zps{{1, 2}}, true},
		{"single-loop", zps{{1, 1}}, false},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, false},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			if zid, got := createDigraph(tc.dg).IsDAG(); got != tc.exp {
				t.Errorf("expected %v, but got %v (%v)", tc.exp, got, zid)
			}
		})
	}
}

func TestDigraphReverse(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		exp  id.EdgeSlice
	}{
		{"empty", nil, nil},
		{"single-edge", zps{{1, 2}}, zps{{2, 1}}},
		{"single-loop", zps{{1, 1}}, zps{{1, 1}}},
		{"end-loop", zps{{1, 2}, {2, 2}}, zps{{2, 1}, {2, 2}}},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, zps{{2, 1}, {2, 5}, {3, 2}, {4, 3}, {5, 4}}},
		{"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, zps{{2, 1}, {2, 4}, {3, 2}, {4, 3}, {5, 4}}},
		{"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, zps{{2, 1}, {3, 2}, {5, 4}}},
		{"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, zps{{2, 1}, {2, 3}, {3, 1}}},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			dg := createDigraph(tc.dg)
			if got := dg.Reverse().Edges().Sort(); !got.Equal(tc.exp) {
				t.Errorf("\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

func TestDigraphSortReverse(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name string
		dg   id.EdgeSlice
		exp  id.Slice
	}{
		{"empty", nil, nil},
		{"single-edge", zps{{1, 2}}, id.Slice{2, 1}},
		{"single-loop", zps{{1, 1}}, nil},
		{"end-loop", zps{{1, 2}, {2, 2}}, id.Slice{}},
		{"long-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {5, 2}}, id.Slice{}},
		{"sect-loop", zps{{1, 2}, {2, 3}, {3, 4}, {4, 5}, {4, 2}}, id.Slice{5}},
		{"two-islands", zps{{1, 2}, {2, 3}, {4, 5}}, id.Slice{5, 3, 4, 2, 1}},
		{"direct-indirect", zps{{1, 2}, {1, 3}, {3, 2}}, id.Slice{2, 3, 1}},
	}
	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			if got := createDigraph(tc.dg).SortReverse(); !got.Equal(tc.exp) {
				t.Errorf("expected:\n%v, but got:\n%v", tc.exp, got)
			}
		})
	}
}

Added zettel/id/edge.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id

import "slices"

// Edge is a pair of to vertices.
type Edge struct {
	From, To Zid
}

// EdgeSlice is a slice of Edges
type EdgeSlice []Edge

// Equal return true if both slices are the same.
func (es EdgeSlice) Equal(other EdgeSlice) bool {
	return slices.Equal(es, other)
}

// Sort the slice.
func (es EdgeSlice) Sort() EdgeSlice {
	slices.SortFunc(es, func(e1, e2 Edge) int {
		if e1.From < e2.From {
			return -1
		}
		if e1.From > e2.From {
			return 1
		}
		if e1.To < e2.To {
			return -1
		}
		if e1.To > e2.To {
			return 1
		}
		return 0
	})
	return es
}

Added zettel/id/id.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id provides zettel specific types, constants, and functions about
// zettel identifier.
package id

import (
	"strconv"
	"time"

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

// Zid is the internal identifier of a zettel. Typically, it is a
// time stamp of the form "YYYYMMDDHHmmSS" converted to an unsigned integer.
// A zettelstore implementation should try to set the last two digits to zero,
// e.g. the seconds should be zero,
type Zid uint64

// Some important ZettelIDs.
const (
	Invalid = Zid(0) // Invalid is a Zid that will never be valid
)

// ZettelIDs that are used as Zid more than once.
//
// Note: if you change some values, ensure that you also change them in the
// Constant box. They are mentioned there literally, because these
// constants are not available there.
var (
	ConfigurationZid  = MustParse(api.ZidConfiguration)
	BaseTemplateZid   = MustParse(api.ZidBaseTemplate)
	LoginTemplateZid  = MustParse(api.ZidLoginTemplate)
	ListTemplateZid   = MustParse(api.ZidListTemplate)
	ZettelTemplateZid = MustParse(api.ZidZettelTemplate)
	InfoTemplateZid   = MustParse(api.ZidInfoTemplate)
	FormTemplateZid   = MustParse(api.ZidFormTemplate)
	DeleteTemplateZid = MustParse(api.ZidDeleteTemplate)
	ErrorTemplateZid  = MustParse(api.ZidErrorTemplate)
	StartSxnZid       = MustParse(api.ZidSxnStart)
	BaseSxnZid        = MustParse(api.ZidSxnBase)
	PreludeSxnZid     = MustParse(api.ZidSxnPrelude)
	EmojiZid          = MustParse(api.ZidEmoji)
	TOCNewTemplateZid = MustParse(api.ZidTOCNewTemplate)
	DefaultHomeZid    = MustParse(api.ZidDefaultHome)
)

const maxZid = 99999999999999

// ParseUint interprets a string as a possible zettel identifier
// and returns its integer value.
func ParseUint(s string) (uint64, error) {
	res, err := strconv.ParseUint(s, 10, 47)
	if err != nil {
		return 0, err
	}
	if res == 0 || res > maxZid {
		return res, strconv.ErrRange
	}
	return res, nil
}

// Parse interprets a string as a zettel identification and
// returns its value.
func Parse(s string) (Zid, error) {
	if len(s) != 14 {
		return Invalid, strconv.ErrSyntax
	}
	res, err := ParseUint(s)
	if err != nil {
		return Invalid, err
	}
	return Zid(res), nil
}

// MustParse tries to interpret a string as a zettel identifier and returns
// its value or panics otherwise.
func MustParse(s api.ZettelID) Zid {
	zid, err := Parse(string(s))
	if err == nil {
		return zid
	}
	panic(err)
}

// String converts the zettel identification to a string of 14 digits.
// Only defined for valid ids.
func (zid Zid) String() string {
	var result [14]byte
	zid.toByteArray(&result)
	return string(result[:])
}

// ZettelID return the zettel identification as a api.ZettelID.
func (zid Zid) ZettelID() api.ZettelID { return api.ZettelID(zid.String()) }

// Bytes converts the zettel identification to a byte slice of 14 digits.
// Only defined for valid ids.
func (zid Zid) Bytes() []byte {
	var result [14]byte
	zid.toByteArray(&result)
	return result[:]
}

// toByteArray converts the Zid into a fixed byte array, usable for printing.
//
// Based on idea by Daniel Lemire: "Converting integers to fix-digit representations quickly"
// https://lemire.me/blog/2021/11/18/converting-integers-to-fix-digit-representations-quickly/
func (zid Zid) toByteArray(result *[14]byte) {
	date := uint64(zid) / 1000000
	fullyear := date / 10000
	century, year := fullyear/100, fullyear%100
	monthday := date % 10000
	month, day := monthday/100, monthday%100
	time := uint64(zid) % 1000000
	hmtime, second := time/100, time%100
	hour, minute := hmtime/100, hmtime%100

	result[0] = byte(century/10) + '0'
	result[1] = byte(century%10) + '0'
	result[2] = byte(year/10) + '0'
	result[3] = byte(year%10) + '0'
	result[4] = byte(month/10) + '0'
	result[5] = byte(month%10) + '0'
	result[6] = byte(day/10) + '0'
	result[7] = byte(day%10) + '0'
	result[8] = byte(hour/10) + '0'
	result[9] = byte(hour%10) + '0'
	result[10] = byte(minute/10) + '0'
	result[11] = byte(minute%10) + '0'
	result[12] = byte(second/10) + '0'
	result[13] = byte(second%10) + '0'
}

// IsValid determines if zettel id is a valid one, e.g. consists of max. 14 digits.
func (zid Zid) IsValid() bool { return 0 < zid && zid <= maxZid }

// TimestampLayout to transform a date into a Zid and into other internal dates.
const TimestampLayout = "20060102150405"

// New returns a new zettel id based on the current time.
func New(withSeconds bool) Zid {
	now := time.Now().Local()
	var s string
	if withSeconds {
		s = now.Format(TimestampLayout)
	} else {
		s = now.Format("20060102150400")
	}
	res, err := Parse(s)
	if err != nil {
		panic(err)
	}
	return res
}

Added zettel/id/id_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id_test provides unit tests for testing zettel id specific functions.
package id_test

import (
	"testing"

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

func TestIsValid(t *testing.T) {
	t.Parallel()
	validIDs := []string{
		"00000000000001",
		"00000000000020",
		"00000000000300",
		"00000000004000",
		"00000000050000",
		"00000000600000",
		"00000007000000",
		"00000080000000",
		"00000900000000",
		"00001000000000",
		"00020000000000",
		"00300000000000",
		"04000000000000",
		"50000000000000",
		"99999999999999",
		"00001007030200",
		"20200310195100",
		"12345678901234",
	}

	for i, sid := range validIDs {
		zid, err := id.Parse(sid)
		if err != nil {
			t.Errorf("i=%d: sid=%q is not valid, but should be. err=%v", i, sid, err)
		}
		s := zid.String()
		if s != sid {
			t.Errorf(
				"i=%d: zid=%v does not format to %q, but to %q", i, zid, sid, s)
		}
	}

	invalidIDs := []string{
		"", "0", "a",
		"00000000000000",
		"0000000000000a",
		"000000000000000",
		"20200310T195100",
		"+1234567890123",
	}

	for i, sid := range invalidIDs {
		if zid, err := id.Parse(sid); err == nil {
			t.Errorf("i=%d: sid=%q is valid (zid=%s), but should not be", i, sid, zid)
		}
	}
}

var sResult string // to disable compiler optimization in loop below

func BenchmarkString(b *testing.B) {
	var s string
	for range b.N {
		s = id.Zid(12345678901200).String()
	}
	sResult = s
}

var bResult []byte // to disable compiler optimization in loop below

func BenchmarkBytes(b *testing.B) {
	var bs []byte
	for range b.N {
		bs = id.Zid(12345678901200).Bytes()
	}
	bResult = bs
}

Added zettel/id/set.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id

import (
	"slices"
	"strings"
)

// Set is a set of zettel identifier
type Set struct {
	seq []Zid
}

// String returns a string representation of the set.
func (s *Set) String() string {
	return "{" + s.MetaString() + "}"
}

// MetaString returns a string representation of the set to be stored as metadata.
func (s *Set) MetaString() string {
	if s == nil || len(s.seq) == 0 {
		return ""
	}
	var sb strings.Builder
	for i, zid := range s.seq {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.Write(zid.Bytes())
	}
	return sb.String()
}

// NewSet returns a new set of identifier with the given initial values.
func NewSet(zids ...Zid) *Set {
	switch l := len(zids); l {
	case 0:
		return &Set{seq: nil}
	case 1:
		return &Set{seq: []Zid{zids[0]}}
	default:
		result := Set{seq: make(Slice, 0, l)}
		result.AddSlice(zids)
		return &result
	}
}

// NewSetCap returns a new set of identifier with the given capacity and initial values.
func NewSetCap(c int, zids ...Zid) *Set {
	result := Set{seq: make(Slice, 0, max(c, len(zids)))}
	result.AddSlice(zids)
	return &result
}

// IsEmpty returns true, if the set conains no element.
func (s *Set) IsEmpty() bool {
	return s == nil || len(s.seq) == 0
}

// Length returns the number of elements in this set.
func (s *Set) Length() int {
	if s == nil {
		return 0
	}
	return len(s.seq)
}

// Clone returns a copy of the given set.
func (s *Set) Clone() *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	return &Set{seq: slices.Clone(s.seq)}
}

// Add adds a Add to the set.
func (s *Set) Add(zid Zid) *Set {
	if s == nil {
		return NewSet(zid)
	}
	s.add(zid)
	return s
}

// Contains return true if the set is non-nil and the set contains the given Zettel identifier.
func (s *Set) Contains(zid Zid) bool { return s != nil && s.contains(zid) }

// ContainsOrNil return true if the set is nil or if the set contains the given Zettel identifier.
func (s *Set) ContainsOrNil(zid Zid) bool { return s == nil || s.contains(zid) }

// AddSlice adds all identifier of the given slice to the set.
func (s *Set) AddSlice(sl Slice) *Set {
	if s == nil {
		return NewSet(sl...)
	}
	s.seq = slices.Grow(s.seq, len(sl))
	for _, zid := range sl {
		s.add(zid)
	}
	return s
}

// SafeSorted returns the set as a new sorted slice of zettel identifier.
func (s *Set) SafeSorted() Slice {
	if s == nil {
		return nil
	}
	return slices.Clone(s.seq)
}

// IntersectOrSet removes all zettel identifier that are not in the other set.
// Both sets can be modified by this method. One of them is the set returned.
// It contains the intersection of both, if s is not nil.
//
// If s == nil, then the other set is always returned.
func (s *Set) IntersectOrSet(other *Set) *Set {
	if s == nil || other == nil {
		return other
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			spos++
			continue
		}
		if sz > oz {
			opos++
			continue
		}
		s.seq[topos] = sz
		topos++
		spos++
		opos++
	}
	s.seq = s.seq[:topos]
	return s
}

// IUnion adds the elements of set other to s.
func (s *Set) IUnion(other *Set) *Set {
	if other == nil || len(other.seq) == 0 {
		return s
	}
	// TODO: if other is large enough (and s is not too small) -> optimize by swapping and/or loop through both
	return s.AddSlice(other.seq)
}

// ISubstract removes all zettel identifier from 's' that are in the set 'other'.
func (s *Set) ISubstract(other *Set) {
	if s == nil || len(s.seq) == 0 || other == nil || len(other.seq) == 0 {
		return
	}
	topos, spos, opos := 0, 0, 0
	for spos < len(s.seq) && opos < len(other.seq) {
		sz, oz := s.seq[spos], other.seq[opos]
		if sz < oz {
			s.seq[topos] = sz
			topos++
			spos++
			continue
		}
		if sz == oz {
			spos++
		}
		opos++
	}
	for spos < len(s.seq) {
		s.seq[topos] = s.seq[spos]
		topos++
		spos++
	}
	s.seq = s.seq[:topos]
}

// Diff returns the difference sets between the two sets: the first difference
// set is the set of elements that are in other, but not in s; the second
// difference set is the set of element that are in s but not in other.
//
// in other words: the first result is the set of elements from other that must
// be added to s; the second result is the set of elements that must be removed
// from s, so that s would have the same elemest as other.
func (s *Set) Diff(other *Set) (newS, remS *Set) {
	if s == nil || len(s.seq) == 0 {
		return other.Clone(), nil
	}
	if other == nil || len(other.seq) == 0 {
		return nil, s.Clone()
	}
	seqS, seqO := s.seq, other.seq
	var newRefs, remRefs Slice
	npos, opos := 0, 0
	for npos < len(seqO) && opos < len(seqS) {
		rn, ro := seqO[npos], seqS[opos]
		if rn == ro {
			npos++
			opos++
			continue
		}
		if rn < ro {
			newRefs = append(newRefs, rn)
			npos++
			continue
		}
		remRefs = append(remRefs, ro)
		opos++
	}
	if npos < len(seqO) {
		newRefs = append(newRefs, seqO[npos:]...)
	}
	if opos < len(seqS) {
		remRefs = append(remRefs, seqS[opos:]...)
	}
	return newFromSlice(newRefs), newFromSlice(remRefs)
}

// Remove the identifier from the set.
func (s *Set) Remove(zid Zid) *Set {
	if s == nil || len(s.seq) == 0 {
		return nil
	}
	if pos, found := s.find(zid); found {
		copy(s.seq[pos:], s.seq[pos+1:])
		s.seq = s.seq[:len(s.seq)-1]
	}
	if len(s.seq) == 0 {
		return nil
	}
	return s
}

// Equal returns true if the other set is equal to the given set.
func (s *Set) Equal(other *Set) bool {
	if s == nil {
		return other == nil
	}
	if other == nil {
		return false
	}
	return slices.Equal(s.seq, other.seq)
}

// ForEach calls the given function for each element of the set.
//
// Every element is bigger than the previous one.
func (s *Set) ForEach(fn func(zid Zid)) {
	if s != nil {
		for _, zid := range s.seq {
			fn(zid)
		}
	}
}

// Pop return one arbitrary element of the set.
func (s *Set) Pop() (Zid, bool) {
	if s != nil {
		if l := len(s.seq); l > 0 {
			zid := s.seq[l-1]
			s.seq = s.seq[:l-1]
			return zid, true
		}
	}
	return Invalid, false
}

// Optimize the amount of memory to store the set.
func (s *Set) Optimize() {
	if s != nil {
		s.seq = slices.Clip(s.seq)
	}
}

// ----- unchecked base operations

func newFromSlice(seq Slice) *Set {
	if l := len(seq); l == 0 {
		return nil
	}
	return &Set{seq: seq}
}

func (s *Set) add(zid Zid) {
	if pos, found := s.find(zid); !found {
		s.seq = slices.Insert(s.seq, pos, zid)
	}
}

func (s *Set) contains(zid Zid) bool {
	_, found := s.find(zid)
	return found
}

func (s *Set) find(zid Zid) (int, bool) {
	hi := len(s.seq)
	for lo := 0; lo < hi; {
		m := lo + (hi-lo)/2
		if z := s.seq[m]; z == zid {
			return m, true
		} else if z < zid {
			lo = m + 1
		} else {
			hi = m
		}
	}
	return hi, false
}

Added zettel/id/set_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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id_test

import (
	"testing"

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

func TestSetContainsOrNil(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s   *id.Set
		zid id.Zid
		exp bool
	}{
		{nil, id.Invalid, true},
		{nil, 14, true},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(), 1, false},
		{id.NewSet(), id.Invalid, false},
		{id.NewSet(1), 1, true},
	}
	for i, tc := range testcases {
		got := tc.s.ContainsOrNil(tc.zid)
		if got != tc.exp {
			t.Errorf("%d: %v.ContainsOrNil(%v) == %v, but got %v", i, tc.s, tc.zid, tc.exp, got)
		}
	}
}

func TestSetAdd(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1, 2}},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IUnion(tc.s2).SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Add(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetSafeSorted(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		set *id.Set
		exp id.Slice
	}{
		{nil, nil},
		{id.NewSet(), nil},
		{id.NewSet(9, 4, 6, 1, 7), id.Slice{1, 4, 6, 7, 9}},
	}
	for i, tc := range testcases {
		got := tc.set.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.SafeSorted() should be %v, but got %v", i, tc.set, tc.exp, got)
		}
	}
}

func TestSetIntersectOrSet(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, nil},
		{nil, id.NewSet(1), id.Slice{1}},
		{id.NewSet(1), id.NewSet(), nil},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), nil},
		{id.NewSet(2), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), id.Slice{1}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := tc.s1.IntersectOrSet(tc.s2).SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IntersectOrSet(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetIUnion(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    *id.Set
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.NewSet(1)},
		{nil, id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1), id.NewSet(), id.NewSet(1)},
		{id.NewSet(), id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1), id.NewSet(2), id.NewSet(1, 2)},
		{id.NewSet(2), id.NewSet(1), id.NewSet(2, 1)},
		{id.NewSet(1), id.NewSet(1), id.NewSet(1)},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3, 4), id.NewSet(1, 2, 3, 4)},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		got := s1.IUnion(tc.s2)
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.IUnion(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetISubtract(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{nil, id.NewSet(), nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{nil, id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(2), id.NewSet(1), id.Slice{2}},
		{id.NewSet(1), id.NewSet(1), nil},
		{id.NewSet(1, 2, 3), id.NewSet(1), id.Slice{2, 3}},
		{id.NewSet(1, 2, 3), id.NewSet(2), id.Slice{1, 3}},
		{id.NewSet(1, 2, 3), id.NewSet(3), id.Slice{1, 2}},
		{id.NewSet(1, 2, 3), id.NewSet(1, 2), id.Slice{3}},
		{id.NewSet(1, 2, 3), id.NewSet(1, 3), id.Slice{2}},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3), id.Slice{1}},
	}
	for i, tc := range testcases {
		s1 := tc.s1.Clone()
		sl1 := s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		s1.ISubstract(tc.s2)
		got := s1.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.ISubstract(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func TestSetDiff(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in1, in2   *id.Set
		exp1, exp2 *id.Set
	}{
		{nil, nil, nil, nil},
		{id.NewSet(1), nil, nil, id.NewSet(1)},
		{nil, id.NewSet(1), id.NewSet(1), nil},
		{id.NewSet(1), id.NewSet(1), nil, nil},
		{id.NewSet(1, 2), id.NewSet(1), nil, id.NewSet(2)},
		{id.NewSet(1), id.NewSet(1, 2), id.NewSet(2), nil},
		{id.NewSet(1, 2), id.NewSet(1, 3), id.NewSet(3), id.NewSet(2)},
		{id.NewSet(1, 2, 3), id.NewSet(2, 3, 4), id.NewSet(4), id.NewSet(1)},
		{id.NewSet(2, 3, 4), id.NewSet(1, 2, 3), id.NewSet(1), id.NewSet(4)},
	}
	for i, tc := range testcases {
		gotN, gotO := tc.in1.Diff(tc.in2)
		if !tc.exp1.Equal(gotN) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp1, gotN)
		}
		if !tc.exp2.Equal(gotO) {
			t.Errorf("%d: expected %v, but got: %v", i, tc.exp2, gotO)
		}
	}
}

func TestSetRemove(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 *id.Set
		exp    id.Slice
	}{
		{nil, nil, nil},
		{id.NewSet(), nil, nil},
		{id.NewSet(), id.NewSet(), nil},
		{id.NewSet(1), nil, id.Slice{1}},
		{id.NewSet(1), id.NewSet(), id.Slice{1}},
		{id.NewSet(1), id.NewSet(2), id.Slice{1}},
		{id.NewSet(1), id.NewSet(1), id.Slice{}},
	}
	for i, tc := range testcases {
		sl1 := tc.s1.SafeSorted()
		sl2 := tc.s2.SafeSorted()
		newS1 := id.NewSet(sl1...)
		newS1.ISubstract(tc.s2)
		got := newS1.SafeSorted()
		if !got.Equal(tc.exp) {
			t.Errorf("%d: %v.Remove(%v) should be %v, but got %v", i, sl1, sl2, tc.exp, got)
		}
	}
}

func BenchmarkSet(b *testing.B) {
	s := id.NewSetCap(b.N)
	for i := range b.N {
		s.Add(id.Zid(i))
	}
}

Added zettel/id/slice.go.
















































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 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 id

import (
	"slices"
	"strings"
)

// Slice is a sequence of zettel identifier. A special case is a sorted slice.
type Slice []Zid

// Sort a slice of Zids.
func (zs Slice) Sort() { slices.Sort(zs) }

// Clone a zettel identifier slice
func (zs Slice) Clone() Slice { return slices.Clone(zs) }

// Equal reports whether zs and other are the same length and contain the samle zettel
// identifier. A nil argument is equivalent to an empty slice.
func (zs Slice) Equal(other Slice) bool { return slices.Equal(zs, other) }

// MetaString returns the slice as a string to be store in metadata.
func (zs Slice) MetaString() string {
	if len(zs) == 0 {
		return ""
	}
	var sb strings.Builder
	for i, zid := range zs {
		if i > 0 {
			sb.WriteByte(' ')
		}
		sb.WriteString(zid.String())
	}
	return sb.String()
}

Added zettel/id/slice_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 id_test

import (
	"testing"

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

func TestSliceSort(t *testing.T) {
	t.Parallel()
	zs := id.Slice{9, 4, 6, 1, 7}
	zs.Sort()
	exp := id.Slice{1, 4, 6, 7, 9}
	if !zs.Equal(exp) {
		t.Errorf("Slice.Sort did not work. Expected %v, got %v", exp, zs)
	}
}

func TestCopy(t *testing.T) {
	t.Parallel()
	var orig id.Slice
	got := orig.Clone()
	if got != nil {
		t.Errorf("Nil copy resulted in %v", got)
	}
	orig = id.Slice{9, 4, 6, 1, 7}
	got = orig.Clone()
	if !orig.Equal(got) {
		t.Errorf("Slice.Copy did not work. Expected %v, got %v", orig, got)
	}
}

func TestSliceEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		s1, s2 id.Slice
		exp    bool
	}{
		{nil, nil, true},
		{nil, id.Slice{}, true},
		{nil, id.Slice{1}, false},
		{id.Slice{1}, id.Slice{1}, true},
		{id.Slice{1}, id.Slice{2}, false},
		{id.Slice{1, 2}, id.Slice{2, 1}, false},
		{id.Slice{1, 2}, id.Slice{1, 2}, true},
	}
	for i, tc := range testcases {
		got := tc.s1.Equal(tc.s2)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s1, tc.s2, tc.exp, got)
		}
		got = tc.s2.Equal(tc.s1)
		if got != tc.exp {
			t.Errorf("%d/%v.Equal(%v)==%v, but got %v", i, tc.s2, tc.s1, tc.exp, got)
		}
	}
}

func TestSliceMetaString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		in  id.Slice
		exp string
	}{
		{nil, ""},
		{id.Slice{}, ""},
		{id.Slice{1}, "00000000000001"},
		{id.Slice{1, 2}, "00000000000001 00000000000002"},
	}
	for i, tc := range testcases {
		got := tc.in.MetaString()
		if got != tc.exp {
			t.Errorf("%d/%v: expected %q, but got %q", i, tc.in, tc.exp, got)
		}
	}
}

Added zettel/meta/collection.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta

import (
	"slices"
	"strings"
)

// Arrangement stores metadata within its categories.
// Typecally a category might be a tag name, a role name, a syntax value.
type Arrangement map[string][]*Meta

// CreateArrangement by inspecting a given key and use the found
// value as a category.
func CreateArrangement(metaList []*Meta, key string) Arrangement {
	if len(metaList) == 0 {
		return nil
	}
	descr := Type(key)
	if descr == nil {
		return nil
	}
	if descr.IsSet {
		return createSetArrangement(metaList, key)
	}
	return createSimplearrangement(metaList, key)
}

func createSetArrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		if vals, ok := m.GetList(key); ok {
			for _, val := range vals {
				a[val] = append(a[val], m)
			}
		}
	}
	return a
}

func createSimplearrangement(metaList []*Meta, key string) Arrangement {
	a := make(Arrangement)
	for _, m := range metaList {
		if val, ok := m.Get(key); ok && val != "" {
			a[val] = append(a[val], m)
		}
	}
	return a
}

// Counted returns the list of categories, together with the number of
// metadata for each category.
func (a Arrangement) Counted() CountedCategories {
	if len(a) == 0 {
		return nil
	}
	result := make(CountedCategories, 0, len(a))
	for cat, metas := range a {
		result = append(result, CountedCategory{Name: cat, Count: len(metas)})
	}
	return result
}

// CountedCategory contains of a name and the number how much this name occured
// somewhere.
type CountedCategory struct {
	Name  string
	Count int
}

// CountedCategories is the list of CountedCategories.
// Every name must occur only once.
type CountedCategories []CountedCategory

// SortByName sorts the list by the name attribute.
// Since each name must occur only once, two CountedCategories cannot have
// the same name.
func (ccs CountedCategories) SortByName() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int { return strings.Compare(i.Name, j.Name) })
}

// SortByCount sorts the list by the count attribute, descending.
// If two counts are equal, elements are sorted by name.
func (ccs CountedCategories) SortByCount() {
	slices.SortFunc(ccs, func(i, j CountedCategory) int {
		iCount, jCount := i.Count, j.Count
		if iCount > jCount {
			return -1
		}
		if iCount == jCount {
			return strings.Compare(i.Name, j.Name)
		}
		return 1
	})
}

// Categories returns just the category names.
func (ccs CountedCategories) Categories() []string {
	result := make([]string, len(ccs))
	for i, cc := range ccs {
		result[i] = cc.Name
	}
	return result
}

Added zettel/meta/meta.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta provides the zettel specific type 'meta'.
package meta

import (
	"regexp"
	"slices"
	"strings"
	"unicode"
	"unicode/utf8"

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

type keyUsage int

const (
	_             keyUsage = iota
	usageUser              // Key will be manipulated by the user
	usageComputed          // Key is computed by zettelstore
	usageProperty          // Key is computed and not stored by zettelstore
)

// DescriptionKey formally describes each supported metadata key.
type DescriptionKey struct {
	Name    string
	Type    *DescriptionType
	usage   keyUsage
	Inverse string
}

// IsComputed returns true, if metadata is computed and not set by the user.
func (kd *DescriptionKey) IsComputed() bool { return kd.usage >= usageComputed }

// IsProperty returns true, if metadata is a computed property.
func (kd *DescriptionKey) IsProperty() bool { return kd.usage >= usageProperty }

var registeredKeys = make(map[string]*DescriptionKey)

func registerKey(name string, t *DescriptionType, usage keyUsage, inverse string) {
	if _, ok := registeredKeys[name]; ok {
		panic("Key '" + name + "' already defined")
	}
	if inverse != "" {
		if t != TypeID && t != TypeIDSet {
			panic("Inversable key '" + name + "' is not identifier type, but " + t.String())
		}
		inv, ok := registeredKeys[inverse]
		if !ok {
			panic("Inverse Key '" + inverse + "' not found")
		}
		if !inv.IsComputed() {
			panic("Inverse Key '" + inverse + "' is not computed.")
		}
		if inv.Type != TypeIDSet {
			panic("Inverse Key '" + inverse + "' is not an identifier set, but " + inv.Type.String())
		}
	}
	registeredKeys[name] = &DescriptionKey{name, t, usage, inverse}
}

// IsComputed returns true, if key denotes a computed metadata key.
func IsComputed(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsComputed()
	}
	return false
}

// IsProperty returns true, if key denotes a property metadata value.
func IsProperty(name string) bool {
	if kd, ok := registeredKeys[name]; ok {
		return kd.IsProperty()
	}
	return false
}

// Inverse returns the name of the inverse key.
func Inverse(name string) string {
	if kd, ok := registeredKeys[name]; ok {
		return kd.Inverse
	}
	return ""
}

// GetDescription returns the key description object of the given key name.
func GetDescription(name string) DescriptionKey {
	if d, ok := registeredKeys[name]; ok {
		return *d
	}
	return DescriptionKey{Type: Type(name)}
}

// GetSortedKeyDescriptions delivers all metadata key descriptions as a slice, sorted by name.
func GetSortedKeyDescriptions() []*DescriptionKey {
	keys := maps.Keys(registeredKeys)
	result := make([]*DescriptionKey, 0, len(keys))
	for _, n := range keys {
		result = append(result, registeredKeys[n])
	}
	return result
}

// KeyCreatedMissing is temporary until migration to B36 has ended.
// It is not an "official" key to be designed to last long.
const KeyCreatedMissing = "created-missing"

// Supported keys.
func init() {
	registerKey(api.KeyID, TypeID, usageComputed, "")
	registerKey(api.KeyTitle, TypeEmpty, usageUser, "")
	registerKey(api.KeyRole, TypeWord, usageUser, "")
	registerKey(api.KeyTags, TypeTagSet, usageUser, "")
	registerKey(api.KeySyntax, TypeWord, usageUser, "")

	// Properties that are inverse keys
	registerKey(api.KeyFolge, TypeIDSet, usageProperty, "")
	registerKey(api.KeySequel, TypeIDSet, usageProperty, "")
	registerKey(api.KeySuccessors, TypeIDSet, usageProperty, "")
	registerKey(api.KeySubordinates, TypeIDSet, usageProperty, "")

	// Non-inverse keys
	registerKey(api.KeyAuthor, TypeString, usageUser, "")
	registerKey(api.KeyBack, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBackward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyBoxNumber, TypeNumber, usageProperty, "")
	registerKey(api.KeyCopyright, TypeString, usageUser, "")
	registerKey(api.KeyCreated, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyCredential, TypeCredential, usageUser, "")
	registerKey(KeyCreatedMissing, TypeWord, usageProperty, "")
	registerKey(api.KeyDead, TypeIDSet, usageProperty, "")
	registerKey(api.KeyExpire, TypeTimestamp, usageUser, "")
	registerKey(api.KeyFolgeRole, TypeWord, usageUser, "")
	registerKey(api.KeyForward, TypeIDSet, usageProperty, "")
	registerKey(api.KeyLang, TypeWord, usageUser, "")
	registerKey(api.KeyLicense, TypeEmpty, usageUser, "")
	registerKey(api.KeyModified, TypeTimestamp, usageComputed, "")
	registerKey(api.KeyPrecursor, TypeIDSet, usageUser, api.KeyFolge)
	registerKey(api.KeyPredecessor, TypeID, usageUser, api.KeySuccessors)
	registerKey(api.KeyPrequel, TypeIDSet, usageUser, api.KeySequel)
	registerKey(api.KeyPublished, TypeTimestamp, usageProperty, "")
	registerKey(api.KeyQuery, TypeEmpty, usageUser, "")
	registerKey(api.KeyReadOnly, TypeWord, usageUser, "")
	registerKey(api.KeySummary, TypeZettelmarkup, usageUser, "")
	registerKey(api.KeySuperior, TypeIDSet, usageUser, api.KeySubordinates)
	registerKey(api.KeyURL, TypeURL, usageUser, "")
	registerKey(api.KeyUselessFiles, TypeString, usageProperty, "")
	registerKey(api.KeyUserID, TypeWord, usageUser, "")
	registerKey(api.KeyUserRole, TypeWord, usageUser, "")
	registerKey(api.KeyVisibility, TypeWord, usageUser, "")
}

// NewPrefix is the prefix for metadata key in template zettel for creating new zettel.
const NewPrefix = "new-"

// Meta contains all meta-data of a zettel.
type Meta struct {
	Zid     id.Zid
	pairs   map[string]string
	YamlSep bool
}

// New creates a new chunk for storing metadata.
func New(zid id.Zid) *Meta {
	return &Meta{Zid: zid, pairs: make(map[string]string, 5)}
}

// NewWithData creates metadata object with given data.
func NewWithData(zid id.Zid, data map[string]string) *Meta {
	pairs := make(map[string]string, len(data))
	for k, v := range data {
		pairs[k] = v
	}
	return &Meta{Zid: zid, pairs: pairs}
}

// Length returns the number of bytes stored for the metadata.
func (m *Meta) Length() int {
	if m == nil {
		return 0
	}
	result := 6 // storage needed for Zid
	for k, v := range m.pairs {
		result += len(k) + len(v) + 1 // 1 because separator
	}
	return result
}

// Clone returns a new copy of the metadata.
func (m *Meta) Clone() *Meta {
	return &Meta{
		Zid:     m.Zid,
		pairs:   m.Map(),
		YamlSep: m.YamlSep,
	}
}

// Map returns a copy of the meta data as a string map.
func (m *Meta) Map() map[string]string {
	pairs := make(map[string]string, len(m.pairs))
	for k, v := range m.pairs {
		pairs[k] = v
	}
	return pairs
}

var reKey = regexp.MustCompile("^[0-9a-z][-0-9a-z]{0,254}$")

// KeyIsValid returns true, if the string is a valid metadata key.
func KeyIsValid(s string) bool { return reKey.MatchString(s) }

// Pair is one key-value-pair of a Zettel meta.
type Pair struct {
	Key   string
	Value string
}

var firstKeys = []string{api.KeyTitle, api.KeyRole, api.KeyTags, api.KeySyntax}
var firstKeySet strfun.Set

func init() {
	firstKeySet = strfun.NewSet(firstKeys...)
}

// Set stores the given string value under the given key.
func (m *Meta) Set(key, value string) {
	if key != api.KeyID {
		m.pairs[key] = trimValue(value)
	}
}

// SetNonEmpty stores the given value under the given key, if the value is non-empty.
// An empty value will delete the previous association.
func (m *Meta) SetNonEmpty(key, value string) {
	if value == "" {
		delete(m.pairs, key)
	} else {
		m.Set(key, trimValue(value))
	}
}

func trimValue(value string) string {
	return strings.TrimFunc(value, input.IsSpace)
}

// Get retrieves the string value of a given key. The bool value signals,
// whether there was a value stored or not.
func (m *Meta) Get(key string) (string, bool) {
	if m == nil {
		return "", false
	}
	if key == api.KeyID {
		return m.Zid.String(), true
	}
	value, ok := m.pairs[key]
	return value, ok
}

// GetDefault retrieves the string value of the given key. If no value was
// stored, the given default value is returned.
func (m *Meta) GetDefault(key, def string) string {
	if value, found := m.Get(key); found {
		return value
	}
	return def
}

// GetTitle returns the title of the metadata. It is the only key that has a
// defined default value: the string representation of the zettel identifier.
func (m *Meta) GetTitle() string {
	if title, found := m.Get(api.KeyTitle); found {
		return title
	}
	return m.Zid.String()
}

// Pairs returns not computed key/values pairs stored, in a specific order.
// First come the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
// MetaContextKey. Then all other pairs are append to the list, ordered by key.
func (m *Meta) Pairs() []Pair {
	return m.doPairs(m.getFirstKeys(), notComputedKey)
}

// ComputedPairs returns all key/values pairs stored, in a specific order. First come
// the pairs with predefined keys: MetaTitleKey, MetaTagsKey, MetaSyntaxKey,
// MetaContextKey. Then all other pairs are append to the list, ordered by key.
func (m *Meta) ComputedPairs() []Pair {
	return m.doPairs(m.getFirstKeys(), anyKey)
}

// PairsRest returns not computed key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) PairsRest() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	return m.doPairs(result, notComputedKey)
}

// ComputedPairsRest returns all key/values pairs stored, except the values with
// predefined keys. The pairs are ordered by key.
func (m *Meta) ComputedPairsRest() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	return m.doPairs(result, anyKey)
}

func notComputedKey(key string) bool { return !IsComputed(key) }
func anyKey(string) bool             { return true }

func (m *Meta) doPairs(firstKeys []Pair, addKeyPred func(string) bool) []Pair {
	keys := m.getKeysRest(addKeyPred)
	for _, k := range keys {
		firstKeys = append(firstKeys, Pair{k, m.pairs[k]})
	}
	return firstKeys
}

func (m *Meta) getFirstKeys() []Pair {
	result := make([]Pair, 0, len(m.pairs))
	for _, key := range firstKeys {
		if value, ok := m.pairs[key]; ok {
			result = append(result, Pair{key, value})
		}
	}
	return result
}

func (m *Meta) getKeysRest(addKeyPred func(string) bool) []string {
	keys := make([]string, 0, len(m.pairs))
	for k := range m.pairs {
		if !firstKeySet.Has(k) && addKeyPred(k) {
			keys = append(keys, k)
		}
	}
	slices.Sort(keys)
	return keys
}

// Delete removes a key from the data.
func (m *Meta) Delete(key string) {
	if key != api.KeyID {
		delete(m.pairs, key)
	}
}

// Equal compares to metas for equality.
func (m *Meta) Equal(o *Meta, allowComputed bool) bool {
	if m == nil && o == nil {
		return true
	}
	if m == nil || o == nil || m.Zid != o.Zid {
		return false
	}
	tested := make(strfun.Set, len(m.pairs))
	for k, v := range m.pairs {
		tested.Set(k)
		if !equalValue(k, v, o, allowComputed) {
			return false
		}
	}
	for k, v := range o.pairs {
		if !tested.Has(k) && !equalValue(k, v, m, allowComputed) {
			return false
		}
	}
	return true
}

func equalValue(key, val string, other *Meta, allowComputed bool) bool {
	if allowComputed || !IsComputed(key) {
		if valO, found := other.pairs[key]; !found || val != valO {
			return false
		}
	}
	return true
}

// Sanitize all metadata keys and values, so that they can be written safely into a file.
func (m *Meta) Sanitize() {
	if m == nil {
		return
	}
	for k, v := range m.pairs {
		m.pairs[RemoveNonGraphic(k)] = RemoveNonGraphic(v)
	}
}

// RemoveNonGraphic changes the given string not to include non-graphical characters.
// It is needed to sanitize meta data.
func RemoveNonGraphic(s string) string {
	if s == "" {
		return ""
	}
	pos := 0
	var sb strings.Builder
	for pos < len(s) {
		nextPos := strings.IndexFunc(s[pos:], func(r rune) bool { return !unicode.IsGraphic(r) })
		if nextPos < 0 {
			break
		}
		sb.WriteString(s[pos:nextPos])
		sb.WriteByte(' ')
		_, size := utf8.DecodeRuneInString(s[nextPos:])
		pos = nextPos + size
	}
	if pos == 0 {
		return strings.TrimSpace(s)
	}
	sb.WriteString(s[pos:])
	return strings.TrimSpace(sb.String())
}

Added zettel/meta/meta_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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func TestKeyIsValid(t *testing.T) {
	t.Parallel()
	validKeys := []string{"0", "a", "0-", "title", "title-----", strings.Repeat("r", 255)}
	for _, key := range validKeys {
		if !KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as invalid key", key)
		}
	}
	invalidKeys := []string{"", "-", "-a", "Title", "a_b", strings.Repeat("e", 256)}
	for _, key := range invalidKeys {
		if KeyIsValid(key) {
			t.Errorf("Key %q wrongly identified as valid key", key)
		}
	}
}

func TestTitleHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); ok && got != "" {
		t.Errorf("Title is not empty, but %q", got)
	}
	const st = "A simple text"
	addToMeta(m, api.KeyTitle, " "+st+"  ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != st {
		t.Errorf("Title is not %q, but %q", st, got)
	}
	addToMeta(m, api.KeyTitle, "  "+st+"\t")
	const exp = st + " " + st
	if got, ok := m.Get(api.KeyTitle); !ok || got != exp {
		t.Errorf("Title is not %q, but %q", exp, got)
	}

	m = New(testID)
	const at = "A Title"
	addToMeta(m, api.KeyTitle, at)
	addToMeta(m, api.KeyTitle, " ")
	if got, ok := m.Get(api.KeyTitle); !ok || got != at {
		t.Errorf("Title is not %q, but %q", at, got)
	}
}

func checkTags(t *testing.T, exp []string, m *Meta) {
	t.Helper()
	got, _ := m.GetList(api.KeyTags)
	for i, tag := range exp {
		if i < len(got) {
			if tag != got[i] {
				t.Errorf("Pos=%d, expected %q, got %q", i, exp[i], got[i])
			}
		} else {
			t.Errorf("Expected %q, but is missing", exp[i])
		}
	}
	if len(exp) < len(got) {
		t.Errorf("Extra tags: %q", got[len(exp):])
	}
}

func TestTagsHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	checkTags(t, []string{}, m)

	addToMeta(m, api.KeyTags, "")
	checkTags(t, []string{}, m)

	addToMeta(m, api.KeyTags, "  #t1 #t2  #t3 #t4  ")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4"}, m)

	addToMeta(m, api.KeyTags, "#t5")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)

	addToMeta(m, api.KeyTags, "t6")
	checkTags(t, []string{"#t1", "#t2", "#t3", "#t4", "#t5"}, m)
}

func TestSyntax(t *testing.T) {
	t.Parallel()
	m := New(testID)
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	addToMeta(m, api.KeySyntax, "MarkDown")
	const exp = "markdown"
	if got, ok := m.Get(api.KeySyntax); !ok || got != exp {
		t.Errorf("Syntax is not %q, but %q", exp, got)
	}
	addToMeta(m, api.KeySyntax, " ")
	if got, _ := m.Get(api.KeySyntax); got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
}

func checkHeader(t *testing.T, exp map[string]string, gotP []Pair) {
	t.Helper()
	got := make(map[string]string, len(gotP))
	for _, p := range gotP {
		got[p.Key] = p.Value
		if _, ok := exp[p.Key]; !ok {
			t.Errorf("Key %q is not expected, but has value %q", p.Key, p.Value)
		}
	}
	for k, v := range exp {
		if gv, ok := got[k]; !ok || v != gv {
			if ok {
				t.Errorf("Key %q is not %q, but %q", k, v, got[k])
			} else {
				t.Errorf("Key %q missing, should have value %q", k, v)
			}
		}
	}
}

func TestDefaultHeader(t *testing.T) {
	t.Parallel()
	m := New(testID)
	addToMeta(m, "h1", "d1")
	addToMeta(m, "H2", "D2")
	addToMeta(m, "H1", "D1.1")
	exp := map[string]string{"h1": "d1 D1.1", "h2": "D2"}
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "", "d0")
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h3", "")
	exp["h3"] = ""
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h3", "  ")
	checkHeader(t, exp, m.Pairs())
	addToMeta(m, "h4", " ")
	exp["h4"] = ""
	checkHeader(t, exp, m.Pairs())
}

func TestDelete(t *testing.T) {
	t.Parallel()
	m := New(testID)
	m.Set("key", "val")
	if got, ok := m.Get("key"); !ok || got != "val" {
		t.Errorf("Value != %q, got: %v/%q", "val", ok, got)
	}
	m.Set("key", "")
	if got, ok := m.Get("key"); !ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
	m.Delete("key")
	if got, ok := m.Get("key"); ok || got != "" {
		t.Errorf("Value != %q, got: %v/%q", "", ok, got)
	}
}

func TestEqual(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		pairs1, pairs2 []string
		allowComputed  bool
		exp            bool
	}{
		{nil, nil, true, true},
		{nil, nil, false, true},
		{[]string{"a", "a"}, nil, false, false},
		{[]string{"a", "a"}, nil, true, false},
		{[]string{api.KeyFolge, "0"}, nil, true, false},
		{[]string{api.KeyFolge, "0"}, nil, false, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, true, true},
		{[]string{api.KeyFolge, "0"}, []string{api.KeyFolge, "0"}, false, true},
	}
	for i, tc := range testcases {
		m1 := pairs2meta(tc.pairs1)
		m2 := pairs2meta(tc.pairs2)
		got := m1.Equal(m2, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =?= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
		got = m2.Equal(m1, tc.allowComputed)
		if tc.exp != got {
			t.Errorf("%d: %v =!= %v: expected=%v, but got=%v", i, tc.pairs1, tc.pairs2, tc.exp, got)
		}
	}

	// Pathologic cases
	var m1, m2 *Meta
	if !m1.Equal(m2, true) {
		t.Error("Nil metas should be treated equal")
	}
	m1 = New(testID)
	if m1.Equal(m2, true) {
		t.Error("Empty meta should not be equal to nil")
	}
	if m2.Equal(m1, true) {
		t.Error("Nil meta should should not be equal to empty")
	}
	m2 = New(testID + 1)
	if m1.Equal(m2, true) {
		t.Error("Different ID should differentiate")
	}
	if m2.Equal(m1, true) {
		t.Error("Different ID should differentiate")
	}
}

func pairs2meta(pairs []string) *Meta {
	m := New(testID)
	for i := 0; i < len(pairs); i += 2 {
		m.Set(pairs[i], pairs[i+1])
	}
	return m
}

func TestRemoveNonGraphic(t *testing.T) {
	testCases := []struct {
		inp string
		exp string
	}{
		{"", ""},
		{" ", ""},
		{"a", "a"},
		{"a ", "a"},
		{"a b", "a b"},
		{"\n", ""},
		{"a\n", "a"},
		{"a\nb", "a b"},
		{"a\tb", "a b"},
	}
	for i, tc := range testCases {
		got := RemoveNonGraphic(tc.inp)
		if tc.exp != got {
			t.Errorf("%q/%d: expected %q, but got %q", tc.inp, i, tc.exp, got)
		}
	}
}

Added zettel/meta/parse.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta

import (
	"strings"

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

// NewFromInput parses the meta data of a zettel.
func NewFromInput(zid id.Zid, inp *input.Input) *Meta {
	if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
		skipToEOL(inp)
		inp.EatEOL()
	}
	meta := New(zid)
	for {
		inp.SkipSpace()
		switch inp.Ch {
		case '\r':
			if inp.Peek() == '\n' {
				inp.Next()
			}
			fallthrough
		case '\n':
			inp.Next()
			return meta
		case input.EOS:
			return meta
		case '%':
			skipToEOL(inp)
			inp.EatEOL()
			continue
		}
		parseHeader(meta, inp)
		if inp.Ch == '-' && inp.PeekN(0) == '-' && inp.PeekN(1) == '-' {
			skipToEOL(inp)
			inp.EatEOL()
			meta.YamlSep = true
			return meta
		}
	}
}

func parseHeader(m *Meta, inp *input.Input) {
	pos := inp.Pos
	for isHeader(inp.Ch) {
		inp.Next()
	}
	key := inp.Src[pos:inp.Pos]
	inp.SkipSpace()
	if inp.Ch == ':' {
		inp.Next()
	}
	var val []byte
	for {
		inp.SkipSpace()
		pos = inp.Pos
		skipToEOL(inp)
		val = append(val, inp.Src[pos:inp.Pos]...)
		inp.EatEOL()
		if !inp.IsSpace() {
			break
		}
		val = append(val, ' ')
	}
	addToMeta(m, string(key), string(val))
}

func skipToEOL(inp *input.Input) {
	for {
		switch inp.Ch {
		case '\n', '\r', input.EOS:
			return
		}
		inp.Next()
	}
}

// Return true iff rune is valid for header key.
func isHeader(ch rune) bool {
	return ('a' <= ch && ch <= 'z') ||
		('0' <= ch && ch <= '9') ||
		ch == '-' ||
		('A' <= ch && ch <= 'Z')
}

type predValidElem func(string) bool

func addToSet(set strfun.Set, elems []string, useElem predValidElem) {
	for _, s := range elems {
		if len(s) > 0 && useElem(s) {
			set.Set(s)
		}
	}
}

func addSet(m *Meta, key, val string, useElem predValidElem) {
	newElems := strings.Fields(val)
	oldElems, ok := m.GetList(key)
	if !ok {
		oldElems = nil
	}

	set := make(strfun.Set, len(newElems)+len(oldElems))
	addToSet(set, newElems, useElem)
	if len(set) == 0 {
		// Nothing to add. Maybe because of rejected elements.
		return
	}
	addToSet(set, oldElems, useElem)
	m.SetList(key, maps.Keys(set))
}

func addData(m *Meta, k, v string) {
	if o, ok := m.Get(k); !ok || o == "" {
		m.Set(k, v)
	} else if v != "" {
		m.Set(k, o+" "+v)
	}
}

func addToMeta(m *Meta, key, val string) {
	v := trimValue(val)
	key = strings.ToLower(key)
	if !KeyIsValid(key) {
		return
	}
	switch key {
	case "", api.KeyID:
		// Empty key and 'id' key will be ignored
		return
	}

	switch Type(key) {
	case TypeTagSet:
		addSet(m, key, strings.ToLower(v), func(s string) bool { return s[0] == '#' && len(s) > 1 })
	case TypeWord:
		m.Set(key, strings.ToLower(v))
	case TypeID:
		if _, err := id.Parse(v); err == nil {
			m.Set(key, v)
		}
	case TypeIDSet:
		addSet(m, key, v, func(s string) bool {
			_, err := id.Parse(s)
			return err == nil
		})
	case TypeTimestamp:
		if _, ok := TimeValue(v); ok {
			m.Set(key, v)
		}
	default:
		addData(m, key, v)
	}
}

Added zettel/meta/parse_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
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta_test

import (
	"strings"
	"testing"

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

func parseMetaStr(src string) *meta.Meta {
	return meta.NewFromInput(testID, input.NewInput([]byte(src)))
}

func TestEmpty(t *testing.T) {
	t.Parallel()
	m := parseMetaStr("")
	if got, ok := m.Get(api.KeySyntax); ok || got != "" {
		t.Errorf("Syntax is not %q, but %q", "", got)
	}
	if got, ok := m.GetList(api.KeyTags); ok || len(got) > 0 {
		t.Errorf("Tags are not nil, but %v", got)
	}
}

func TestTitle(t *testing.T) {
	t.Parallel()
	td := []struct{ s, e string }{
		{api.KeyTitle + ": a title", "a title"},
		{api.KeyTitle + ": a\n\t title", "a title"},
		{api.KeyTitle + ": a\n\t title\r\n  x", "a title x"},
		{api.KeyTitle + " AbC", "AbC"},
		{api.KeyTitle + " AbC\n ded", "AbC ded"},
		{api.KeyTitle + ": o\ntitle: p", "o p"},
		{api.KeyTitle + ": O\n\ntitle: P", "O"},
		{api.KeyTitle + ": b\r\ntitle: c", "b c"},
		{api.KeyTitle + ": B\r\n\r\ntitle: C", "B"},
		{api.KeyTitle + ": r\rtitle: q", "r q"},
		{api.KeyTitle + ": R\r\rtitle: Q", "R"},
	}
	for i, tc := range td {
		m := parseMetaStr(tc.s)
		if got, ok := m.Get(api.KeyTitle); !ok || got != tc.e {
			t.Log(m)
			t.Errorf("TC=%d: expected %q, got %q", i, tc.e, got)
		}
	}
}

func TestTags(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		src string
		exp string
	}{
		{"", ""},
		{api.KeyTags + ":", ""},
		{api.KeyTags + ": c", ""},
		{api.KeyTags + ": #", ""},
		{api.KeyTags + ": #c", "c"},
		{api.KeyTags + ": #c #", "c"},
		{api.KeyTags + ": #c #b", "b c"},
		{api.KeyTags + ": #c # #", "c"},
		{api.KeyTags + ": #c # #b", "b c"},
	}
	for i, tc := range testcases {
		m := parseMetaStr(tc.src)
		tagsString, found := m.Get(api.KeyTags)
		if !found {
			if tc.exp != "" {
				t.Errorf("%d / %q: no %s found", i, tc.src, api.KeyTags)
			}
			continue
		}
		tags := meta.TagsFromValue(tagsString)
		if tc.exp == "" && len(tags) > 0 {
			t.Errorf("%d / %q: expected no %s, but got %v", i, tc.src, api.KeyTags, tags)
			continue
		}
		got := strings.Join(tags, " ")
		if tc.exp != got {
			t.Errorf("%d / %q: expected %q, got: %q", i, tc.src, tc.exp, got)
		}
	}
}

func TestNewFromInput(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		input string
		exp   []meta.Pair
	}{
		{"", []meta.Pair{}},
		{" a:b", []meta.Pair{{"a", "b"}}},
		{"%a:b", []meta.Pair{}},
		{"a:b\r\n\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"a:b\r\n%c:d", []meta.Pair{{"a", "b"}}},
		{"% a:b\r\n c:d", []meta.Pair{{"c", "d"}}},
		{"---\r\na:b\r\n", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n--\r\nc:d", []meta.Pair{{"a", "b"}, {"c", "d"}}},
		{"---\r\na:b\r\n---\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"---\r\na:b\r\n----\r\nc:d", []meta.Pair{{"a", "b"}}},
		{"new-title:\nnew-url:", []meta.Pair{{"new-title", ""}, {"new-url", ""}}},
	}
	for i, tc := range testcases {
		meta := parseMetaStr(tc.input)
		if got := meta.Pairs(); !equalPairs(tc.exp, got) {
			t.Errorf("TC=%d: expected=%v, got=%v", i, tc.exp, got)
		}
	}

	// Test, whether input position is correct.
	inp := input.NewInput([]byte("---\na:b\n---\nX"))
	m := meta.NewFromInput(testID, inp)
	exp := []meta.Pair{{"a", "b"}}
	if got := m.Pairs(); !equalPairs(exp, got) {
		t.Errorf("Expected=%v, got=%v", exp, got)
	}
	expCh := 'X'
	if gotCh := inp.Ch; gotCh != expCh {
		t.Errorf("Expected=%v, got=%v", expCh, gotCh)
	}
}

func equalPairs(one, two []meta.Pair) bool {
	if len(one) != len(two) {
		return false
	}
	for i := range len(one) {
		if one[i].Key != two[i].Key || one[i].Value != two[i].Value {
			return false
		}
	}
	return true
}

func TestPrecursorIDSet(t *testing.T) {
	t.Parallel()
	var testdata = []struct {
		inp string
		exp string
	}{
		{"", ""},
		{"123", ""},
		{"12345678901234", "12345678901234"},
		{"123 12345678901234", "12345678901234"},
		{"12345678901234 123", "12345678901234"},
		{"01234567890123 123 12345678901234", "01234567890123 12345678901234"},
		{"12345678901234 01234567890123", "01234567890123 12345678901234"},
	}
	for i, tc := range testdata {
		m := parseMetaStr(api.KeyPrecursor + ": " + tc.inp)
		if got, ok := m.Get(api.KeyPrecursor); (!ok && tc.exp != "") || tc.exp != got {
			t.Errorf("TC=%d: expected %q, but got %q when parsing %q", i, tc.exp, got, tc.inp)
		}
	}
}

Added zettel/meta/type.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta

import (
	"strconv"
	"strings"
	"sync"
	"time"

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

// DescriptionType is a description of a specific key type.
type DescriptionType struct {
	Name  string
	IsSet bool
}

// String returns the string representation of the given type
func (t DescriptionType) String() string { return t.Name }

var registeredTypes = make(map[string]*DescriptionType)

func registerType(name string, isSet bool) *DescriptionType {
	if _, ok := registeredTypes[name]; ok {
		panic("Type '" + name + "' already registered")
	}
	t := &DescriptionType{name, isSet}
	registeredTypes[name] = t
	return t
}

// Supported key types.
var (
	TypeCredential   = registerType(api.MetaCredential, false)
	TypeEmpty        = registerType(api.MetaEmpty, false)
	TypeID           = registerType(api.MetaID, false)
	TypeIDSet        = registerType(api.MetaIDSet, true)
	TypeNumber       = registerType(api.MetaNumber, false)
	TypeString       = registerType(api.MetaString, false)
	TypeTagSet       = registerType(api.MetaTagSet, true)
	TypeTimestamp    = registerType(api.MetaTimestamp, false)
	TypeURL          = registerType(api.MetaURL, false)
	TypeWord         = registerType(api.MetaWord, false)
	TypeZettelmarkup = registerType(api.MetaZettelmarkup, false)
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeUnknown is returned.
func (*Meta) Type(key string) *DescriptionType {
	return Type(key)
}

// Some constants for key suffixes that determine a type.
const (
	SuffixKeyRole = "-role"
	SuffixKeyURL  = "-url"
)

var (
	cachedTypedKeys = make(map[string]*DescriptionType)
	mxTypedKey      sync.RWMutex
	suffixTypes     = map[string]*DescriptionType{
		"-date":       TypeTimestamp,
		"-number":     TypeNumber,
		SuffixKeyRole: TypeWord,
		"-time":       TypeTimestamp,
		"-title":      TypeZettelmarkup,
		SuffixKeyURL:  TypeURL,
		"-zettel":     TypeID,
		"-zid":        TypeID,
		"-zids":       TypeIDSet,
	}
)

// Type returns a type hint for the given key. If no type hint is specified,
// TypeEmpty is returned.
func Type(key string) *DescriptionType {
	if k, ok := registeredKeys[key]; ok {
		return k.Type
	}
	mxTypedKey.RLock()
	k, found := cachedTypedKeys[key]
	mxTypedKey.RUnlock()
	if found {
		return k
	}

	for suffix, t := range suffixTypes {
		if strings.HasSuffix(key, suffix) {
			mxTypedKey.Lock()
			defer mxTypedKey.Unlock()
			// Double check to avoid races
			if _, found = cachedTypedKeys[key]; !found {
				cachedTypedKeys[key] = t
			}
			return t
		}
	}
	return TypeEmpty
}

// SetList stores the given string list value under the given key.
func (m *Meta) SetList(key string, values []string) {
	if key != api.KeyID {
		for i, val := range values {
			values[i] = trimValue(val)
		}
		m.pairs[key] = strings.Join(values, " ")
	}
}

// SetWord stores the given word under the given key.
func (m *Meta) SetWord(key, word string) {
	if slist := ListFromValue(word); len(slist) > 0 {
		m.Set(key, slist[0])
	}
}

// SetNow stores the current timestamp under the given key.
func (m *Meta) SetNow(key string) {
	m.Set(key, time.Now().Local().Format(id.TimestampLayout))
}

// BoolValue returns the value interpreted as a bool.
func BoolValue(value string) bool {
	if len(value) > 0 {
		switch value[0] {
		case '0', 'f', 'F', 'n', 'N':
			return false
		}
	}
	return true
}

// GetBool returns the boolean value of the given key.
func (m *Meta) GetBool(key string) bool {
	if value, ok := m.Get(key); ok {
		return BoolValue(value)
	}
	return false
}

// TimeValue returns the time value of the given value.
func TimeValue(value string) (time.Time, bool) {
	if t, err := time.Parse(id.TimestampLayout, ExpandTimestamp(value)); err == nil {
		return t, true
	}
	return time.Time{}, false
}

// ExpandTimestamp makes a short-form timestamp larger.
func ExpandTimestamp(value string) string {
	switch l := len(value); l {
	case 4: // YYYY
		return value + "0101000000"
	case 6: // YYYYMM
		return value + "01000000"
	case 8, 10, 12: // YYYYMMDD, YYYYMMDDhh, YYYYMMDDhhmm
		return value + "000000"[:14-l]
	case 14: // YYYYMMDDhhmmss
		return value
	default:
		if l > 14 {
			return value[:14]
		}
		return value
	}
}

// ListFromValue transforms a string value into a list value.
func ListFromValue(value string) []string {
	return strings.Fields(value)
}

// GetList retrieves the string list value of a given key. The bool value
// signals, whether there was a value stored or not.
func (m *Meta) GetList(key string) ([]string, bool) {
	value, ok := m.Get(key)
	if !ok {
		return nil, false
	}
	return ListFromValue(value), true
}

// TagsFromValue returns the value as a sequence of normalized tags.
func TagsFromValue(value string) []string {
	tags := ListFromValue(strings.ToLower(value))
	for i, tag := range tags {
		if len(tag) > 1 && tag[0] == '#' {
			tags[i] = tag[1:]
		}
	}
	return tags
}

// CleanTag removes the number character ('#') from a tag value and lowercases it.
func CleanTag(tag string) string {
	if len(tag) > 1 && tag[0] == '#' {
		return tag[1:]
	}
	return tag
}

// NormalizeTag adds a missing prefix "#" to the tag
func NormalizeTag(tag string) string {
	if len(tag) > 0 && tag[0] == '#' {
		return tag
	}
	return "#" + tag
}

// GetNumber retrieves the numeric value of a given key.
func (m *Meta) GetNumber(key string, def int64) int64 {
	if value, ok := m.Get(key); ok {
		if num, err := strconv.ParseInt(value, 10, 64); err == nil {
			return num
		}
	}
	return def
}

Added zettel/meta/type_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta_test

import (
	"strconv"
	"testing"
	"time"

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

func TestNow(t *testing.T) {
	t.Parallel()
	m := meta.New(id.Invalid)
	m.SetNow("key")
	val, ok := m.Get("key")
	if !ok {
		t.Error("Unable to get value of key")
	}
	if len(val) != 14 {
		t.Errorf("Value is not 14 digits long: %q", val)
	}
	if _, err := strconv.ParseInt(val, 10, 64); err != nil {
		t.Errorf("Unable to parse %q as an int64: %v", val, err)
	}
	if _, ok = meta.TimeValue(val); !ok {
		t.Errorf("Unable to get time from value %q", val)
	}
}

func TestTimeValue(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		value string
		valid bool
		exp   time.Time
	}{
		{"", false, time.Time{}},
		{"1", false, time.Time{}},
		{"00000000000000", false, time.Time{}},
		{"98765432109876", false, time.Time{}},
		{"20201221111905", true, time.Date(2020, time.December, 21, 11, 19, 5, 0, time.UTC)},
		{"2023", true, time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC)},
		{"20231", false, time.Time{}},
		{"202310", true, time.Date(2023, time.October, 1, 0, 0, 0, 0, time.UTC)},
		{"2023103", false, time.Time{}},
		{"20231030", true, time.Date(2023, time.October, 30, 0, 0, 0, 0, time.UTC)},
		{"202310301", false, time.Time{}},
		{"2023103016", true, time.Date(2023, time.October, 30, 16, 0, 0, 0, time.UTC)},
		{"20231030165", false, time.Time{}},
		{"202310301654", true, time.Date(2023, time.October, 30, 16, 54, 0, 0, time.UTC)},
		{"2023103016541", false, time.Time{}},
		{"20231030165417", true, time.Date(2023, time.October, 30, 16, 54, 17, 0, time.UTC)},
		{"2023103916541700", false, time.Time{}},
	}
	for i, tc := range testCases {
		got, ok := meta.TimeValue(tc.value)
		if ok != tc.valid {
			t.Errorf("%d: parsing of %q should be %v, but got %v", i, tc.value, tc.valid, ok)
			continue
		}
		if got != tc.exp {
			t.Errorf("%d: parsing of %q should return %v, but got %v", i, tc.value, tc.exp, got)
		}
	}
}

Added zettel/meta/values.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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta

import (
	"fmt"

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

// Supported syntax values.
const (
	SyntaxCSS      = api.ValueSyntaxCSS
	SyntaxDraw     = api.ValueSyntaxDraw
	SyntaxGif      = api.ValueSyntaxGif
	SyntaxHTML     = api.ValueSyntaxHTML
	SyntaxJPEG     = "jpeg"
	SyntaxJPG      = "jpg"
	SyntaxMarkdown = api.ValueSyntaxMarkdown
	SyntaxMD       = api.ValueSyntaxMD
	SyntaxNone     = api.ValueSyntaxNone
	SyntaxPlain    = "plain"
	SyntaxPNG      = "png"
	SyntaxSVG      = api.ValueSyntaxSVG
	SyntaxSxn      = api.ValueSyntaxSxn
	SyntaxText     = api.ValueSyntaxText
	SyntaxTxt      = "txt"
	SyntaxWebp     = "webp"
	SyntaxZmk      = api.ValueSyntaxZmk

	DefaultSyntax = SyntaxPlain
)

// Visibility enumerates the variations of the 'visibility' meta key.
type Visibility int

// Supported values for visibility.
const (
	_ Visibility = iota
	VisibilityUnknown
	VisibilityPublic
	VisibilityCreator
	VisibilityLogin
	VisibilityOwner
	VisibilityExpert
)

var visMap = map[string]Visibility{
	api.ValueVisibilityPublic:  VisibilityPublic,
	api.ValueVisibilityCreator: VisibilityCreator,
	api.ValueVisibilityLogin:   VisibilityLogin,
	api.ValueVisibilityOwner:   VisibilityOwner,
	api.ValueVisibilityExpert:  VisibilityExpert,
}
var revVisMap = map[Visibility]string{}

func init() {
	for k, v := range visMap {
		revVisMap[v] = k
	}
}

// GetVisibility returns the visibility value of the given string
func GetVisibility(val string) Visibility {
	if vis, ok := visMap[val]; ok {
		return vis
	}
	return VisibilityUnknown
}

func (v Visibility) String() string {
	if s, ok := revVisMap[v]; ok {
		return s
	}
	return fmt.Sprintf("Unknown (%d)", v)
}

// UserRole enumerates the supported values of meta key 'user-role'.
type UserRole int

// Supported values for user roles.
const (
	_ UserRole = iota
	UserRoleUnknown
	UserRoleCreator
	UserRoleReader
	UserRoleWriter
	UserRoleOwner
)

var urMap = map[string]UserRole{
	api.ValueUserRoleCreator: UserRoleCreator,
	api.ValueUserRoleReader:  UserRoleReader,
	api.ValueUserRoleWriter:  UserRoleWriter,
	api.ValueUserRoleOwner:   UserRoleOwner,
}

// GetUserRole role returns the user role of the given string.
func GetUserRole(val string) UserRole {
	if ur, ok := urMap[val]; ok {
		return ur
	}
	return UserRoleUnknown
}

Added zettel/meta/write.go.





























































1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// Copyright (c) 2020-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 meta

import "io"

// Write writes metadata to a writer, excluding computed and propery values.
func (m *Meta) Write(w io.Writer) (int, error) {
	return m.doWrite(w, IsComputed)
}

// WriteComputed writes metadata to a writer, including computed values,
// but excluding property values.
func (m *Meta) WriteComputed(w io.Writer) (int, error) {
	return m.doWrite(w, IsProperty)
}

func (m *Meta) doWrite(w io.Writer, ignoreKeyPred func(string) bool) (length int, err error) {
	for _, p := range m.ComputedPairs() {
		key := p.Key
		if ignoreKeyPred(key) {
			continue
		}
		if err != nil {
			break
		}
		var l int
		l, err = io.WriteString(w, key)
		length += l
		if err == nil {
			l, err = w.Write(colonSpace)
			length += l
		}
		if err == nil {
			l, err = io.WriteString(w, p.Value)
			length += l
		}
		if err == nil {
			l, err = w.Write(newline)
			length += l
		}
	}
	return length, err
}

var (
	colonSpace = []byte{':', ' '}
	newline    = []byte{'\n'}
)

Added zettel/meta/write_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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 meta_test

import (
	"strings"
	"testing"

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

const testID = id.Zid(98765432101234)

func newMeta(title string, tags []string, syntax string) *meta.Meta {
	m := meta.New(testID)
	if title != "" {
		m.Set(api.KeyTitle, title)
	}
	if tags != nil {
		m.Set(api.KeyTags, strings.Join(tags, " "))
	}
	if syntax != "" {
		m.Set(api.KeySyntax, syntax)
	}
	return m
}
func assertWriteMeta(t *testing.T, m *meta.Meta, expected string) {
	t.Helper()
	var sb strings.Builder
	m.Write(&sb)
	if got := sb.String(); got != expected {
		t.Errorf("\nExp: %q\ngot: %q", expected, got)
	}
}

func TestWriteMeta(t *testing.T) {
	t.Parallel()
	assertWriteMeta(t, newMeta("", nil, ""), "")

	m := newMeta("TITLE", []string{"#t1", "#t2"}, "syntax")
	assertWriteMeta(t, m, "title: TITLE\ntags: #t1 #t2\nsyntax: syntax\n")

	m = newMeta("TITLE", nil, "")
	m.Set("user", "zettel")
	m.Set("auth", "basic")
	assertWriteMeta(t, m, "title: TITLE\nauth: basic\nuser: zettel\n")
}

Added zettel/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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
//-----------------------------------------------------------------------------
// 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 zettel provides specific types, constants, and functions for zettel.
package zettel

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

// Zettel is the main data object of a zettelstore.
type Zettel struct {
	Meta    *meta.Meta // Some additional meta-data.
	Content Content    // The content of the zettel itself.
}

// Length returns the number of bytes to store the zettel (in a zettel view,
// not in a technical view).
func (z Zettel) Length() int { return z.Meta.Length() + z.Content.Length() }

// Equal compares two zettel for equality.
func (z Zettel) Equal(o Zettel, allowComputed bool) bool {
	return z.Meta.Equal(o.Meta, allowComputed) && z.Content.Equal(&o.Content)
}